mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-29 16:02:51 -05:00
core: ensure all NM tests use mock backend + re-orgs + dep updates
This commit is contained in:
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/hyprland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
)
|
||||
|
||||
@@ -26,7 +25,7 @@ func (h *HyprlandProvider) Name() string {
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
section, err := hyprland.ParseKeys(h.configPath)
|
||||
section, err := ParseHyprlandKeys(h.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
|
||||
}
|
||||
@@ -41,7 +40,7 @@ func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) convertSection(section *hyprland.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||
func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||
currentSubcat := subcategory
|
||||
if section.Name != "" {
|
||||
currentSubcat = section.Name
|
||||
@@ -86,7 +85,7 @@ func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) convertKeybind(kb *hyprland.KeyBinding, subcategory string) keybinds.Keybind {
|
||||
func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string) keybinds.Keybind {
|
||||
key := h.formatKey(kb)
|
||||
desc := kb.Comment
|
||||
|
||||
@@ -108,7 +107,7 @@ func (h *HyprlandProvider) generateDescription(dispatcher, params string) string
|
||||
return dispatcher
|
||||
}
|
||||
|
||||
func (h *HyprlandProvider) formatKey(kb *hyprland.KeyBinding) string {
|
||||
func (h *HyprlandProvider) formatKey(kb *HyprlandKeyBinding) string {
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
|
||||
331
core/internal/keybinds/providers/hyprland_parser.go
Normal file
331
core/internal/keybinds/providers/hyprland_parser.go
Normal file
@@ -0,0 +1,331 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TitleRegex = "#+!"
|
||||
HideComment = "[hidden]"
|
||||
CommentBindPattern = "#/#"
|
||||
)
|
||||
|
||||
var ModSeparators = []rune{'+', ' '}
|
||||
|
||||
type HyprlandKeyBinding struct {
|
||||
Mods []string `json:"mods"`
|
||||
Key string `json:"key"`
|
||||
Dispatcher string `json:"dispatcher"`
|
||||
Params string `json:"params"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type HyprlandSection struct {
|
||||
Children []HyprlandSection `json:"children"`
|
||||
Keybinds []HyprlandKeyBinding `json:"keybinds"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type HyprlandParser struct {
|
||||
contentLines []string
|
||||
readingLine int
|
||||
}
|
||||
|
||||
func NewHyprlandParser() *HyprlandParser {
|
||||
return &HyprlandParser{
|
||||
contentLines: []string{},
|
||||
readingLine: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HyprlandParser) ReadContent(directory string) error {
|
||||
expandedDir := os.ExpandEnv(directory)
|
||||
expandedDir = filepath.Clean(expandedDir)
|
||||
if strings.HasPrefix(expandedDir, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedDir = filepath.Join(home, expandedDir[1:])
|
||||
}
|
||||
|
||||
info, err := os.Stat(expandedDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
confFiles, err := filepath.Glob(filepath.Join(expandedDir, "*.conf"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(confFiles) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
var combinedContent []string
|
||||
for _, confFile := range confFiles {
|
||||
if fileInfo, err := os.Stat(confFile); err == nil && fileInfo.Mode().IsRegular() {
|
||||
data, err := os.ReadFile(confFile)
|
||||
if err == nil {
|
||||
combinedContent = append(combinedContent, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(combinedContent) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fullContent := strings.Join(combinedContent, "\n")
|
||||
p.contentLines = strings.Split(fullContent, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func hyprlandAutogenerateComment(dispatcher, params string) string {
|
||||
switch dispatcher {
|
||||
case "resizewindow":
|
||||
return "Resize window"
|
||||
|
||||
case "movewindow":
|
||||
if params == "" {
|
||||
return "Move window"
|
||||
}
|
||||
dirMap := map[string]string{
|
||||
"l": "left",
|
||||
"r": "right",
|
||||
"u": "up",
|
||||
"d": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "move in " + dir + " direction"
|
||||
}
|
||||
return "move in null direction"
|
||||
|
||||
case "pin":
|
||||
return "pin (show on all workspaces)"
|
||||
|
||||
case "splitratio":
|
||||
return "Window split ratio " + params
|
||||
|
||||
case "togglefloating":
|
||||
return "Float/unfloat window"
|
||||
|
||||
case "resizeactive":
|
||||
return "Resize window by " + params
|
||||
|
||||
case "killactive":
|
||||
return "Close window"
|
||||
|
||||
case "fullscreen":
|
||||
fsMap := map[string]string{
|
||||
"0": "fullscreen",
|
||||
"1": "maximization",
|
||||
"2": "fullscreen on Hyprland's side",
|
||||
}
|
||||
if fs, ok := fsMap[params]; ok {
|
||||
return "Toggle " + fs
|
||||
}
|
||||
return "Toggle null"
|
||||
|
||||
case "fakefullscreen":
|
||||
return "Toggle fake fullscreen"
|
||||
|
||||
case "workspace":
|
||||
switch params {
|
||||
case "+1":
|
||||
return "focus right"
|
||||
case "-1":
|
||||
return "focus left"
|
||||
}
|
||||
return "focus workspace " + params
|
||||
case "movefocus":
|
||||
dirMap := map[string]string{
|
||||
"l": "left",
|
||||
"r": "right",
|
||||
"u": "up",
|
||||
"d": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "move focus " + dir
|
||||
}
|
||||
return "move focus null"
|
||||
|
||||
case "swapwindow":
|
||||
dirMap := map[string]string{
|
||||
"l": "left",
|
||||
"r": "right",
|
||||
"u": "up",
|
||||
"d": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "swap in " + dir + " direction"
|
||||
}
|
||||
return "swap in null direction"
|
||||
|
||||
case "movetoworkspace":
|
||||
switch params {
|
||||
case "+1":
|
||||
return "move to right workspace (non-silent)"
|
||||
case "-1":
|
||||
return "move to left workspace (non-silent)"
|
||||
}
|
||||
return "move to workspace " + params + " (non-silent)"
|
||||
case "movetoworkspacesilent":
|
||||
switch params {
|
||||
case "+1":
|
||||
return "move to right workspace"
|
||||
case "-1":
|
||||
return "move to right workspace"
|
||||
}
|
||||
return "move to workspace " + params
|
||||
|
||||
case "togglespecialworkspace":
|
||||
return "toggle special"
|
||||
|
||||
case "exec":
|
||||
return params
|
||||
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HyprlandParser) getKeybindAtLine(lineNumber int) *HyprlandKeyBinding {
|
||||
line := p.contentLines[lineNumber]
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keys := parts[1]
|
||||
keyParts := strings.SplitN(keys, "#", 2)
|
||||
keys = keyParts[0]
|
||||
|
||||
var comment string
|
||||
if len(keyParts) > 1 {
|
||||
comment = strings.TrimSpace(keyParts[1])
|
||||
}
|
||||
|
||||
keyFields := strings.SplitN(keys, ",", 5)
|
||||
if len(keyFields) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(keyFields[0])
|
||||
key := strings.TrimSpace(keyFields[1])
|
||||
dispatcher := strings.TrimSpace(keyFields[2])
|
||||
|
||||
var params string
|
||||
if len(keyFields) > 3 {
|
||||
paramParts := keyFields[3:]
|
||||
params = strings.TrimSpace(strings.Join(paramParts, ","))
|
||||
}
|
||||
|
||||
if comment != "" {
|
||||
if strings.HasPrefix(comment, HideComment) {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
comment = hyprlandAutogenerateComment(dispatcher, params)
|
||||
}
|
||||
|
||||
var modList []string
|
||||
if mods != "" {
|
||||
modstring := mods + string(ModSeparators[0])
|
||||
p := 0
|
||||
for index, char := range modstring {
|
||||
isModSep := false
|
||||
for _, sep := range ModSeparators {
|
||||
if char == sep {
|
||||
isModSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isModSep {
|
||||
if index-p > 1 {
|
||||
modList = append(modList, modstring[p:index])
|
||||
}
|
||||
p = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &HyprlandKeyBinding{
|
||||
Mods: modList,
|
||||
Key: key,
|
||||
Dispatcher: dispatcher,
|
||||
Params: params,
|
||||
Comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *HyprlandParser) getBindsRecursive(currentContent *HyprlandSection, scope int) *HyprlandSection {
|
||||
titleRegex := regexp.MustCompile(TitleRegex)
|
||||
|
||||
for p.readingLine < len(p.contentLines) {
|
||||
line := p.contentLines[p.readingLine]
|
||||
|
||||
loc := titleRegex.FindStringIndex(line)
|
||||
if loc != nil && loc[0] == 0 {
|
||||
headingScope := strings.Index(line, "!")
|
||||
|
||||
if headingScope <= scope {
|
||||
p.readingLine--
|
||||
return currentContent
|
||||
}
|
||||
|
||||
sectionName := strings.TrimSpace(line[headingScope+1:])
|
||||
p.readingLine++
|
||||
|
||||
childSection := &HyprlandSection{
|
||||
Children: []HyprlandSection{},
|
||||
Keybinds: []HyprlandKeyBinding{},
|
||||
Name: sectionName,
|
||||
}
|
||||
result := p.getBindsRecursive(childSection, headingScope)
|
||||
currentContent.Children = append(currentContent.Children, *result)
|
||||
|
||||
} else if strings.HasPrefix(line, CommentBindPattern) {
|
||||
keybind := p.getKeybindAtLine(p.readingLine)
|
||||
if keybind != nil {
|
||||
currentContent.Keybinds = append(currentContent.Keybinds, *keybind)
|
||||
}
|
||||
|
||||
} else if line == "" || !strings.HasPrefix(strings.TrimSpace(line), "bind") {
|
||||
|
||||
} else {
|
||||
keybind := p.getKeybindAtLine(p.readingLine)
|
||||
if keybind != nil {
|
||||
currentContent.Keybinds = append(currentContent.Keybinds, *keybind)
|
||||
}
|
||||
}
|
||||
|
||||
p.readingLine++
|
||||
}
|
||||
|
||||
return currentContent
|
||||
}
|
||||
|
||||
func (p *HyprlandParser) ParseKeys() *HyprlandSection {
|
||||
p.readingLine = 0
|
||||
rootSection := &HyprlandSection{
|
||||
Children: []HyprlandSection{},
|
||||
Keybinds: []HyprlandKeyBinding{},
|
||||
Name: "",
|
||||
}
|
||||
return p.getBindsRecursive(rootSection, 0)
|
||||
}
|
||||
|
||||
func ParseHyprlandKeys(path string) (*HyprlandSection, error) {
|
||||
parser := NewHyprlandParser()
|
||||
if err := parser.ReadContent(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parser.ParseKeys(), nil
|
||||
}
|
||||
396
core/internal/keybinds/providers/hyprland_parser_test.go
Normal file
396
core/internal/keybinds/providers/hyprland_parser_test.go
Normal file
@@ -0,0 +1,396 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHyprlandAutogenerateComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
dispatcher string
|
||||
params string
|
||||
expected string
|
||||
}{
|
||||
{"resizewindow", "", "Resize window"},
|
||||
{"movewindow", "", "Move window"},
|
||||
{"movewindow", "l", "move in left direction"},
|
||||
{"movewindow", "r", "move in right direction"},
|
||||
{"movewindow", "u", "move in up direction"},
|
||||
{"movewindow", "d", "move in down direction"},
|
||||
{"pin", "", "pin (show on all workspaces)"},
|
||||
{"splitratio", "0.5", "Window split ratio 0.5"},
|
||||
{"togglefloating", "", "Float/unfloat window"},
|
||||
{"resizeactive", "10 20", "Resize window by 10 20"},
|
||||
{"killactive", "", "Close window"},
|
||||
{"fullscreen", "0", "Toggle fullscreen"},
|
||||
{"fullscreen", "1", "Toggle maximization"},
|
||||
{"fullscreen", "2", "Toggle fullscreen on Hyprland's side"},
|
||||
{"fakefullscreen", "", "Toggle fake fullscreen"},
|
||||
{"workspace", "+1", "focus right"},
|
||||
{"workspace", "-1", "focus left"},
|
||||
{"workspace", "5", "focus workspace 5"},
|
||||
{"movefocus", "l", "move focus left"},
|
||||
{"movefocus", "r", "move focus right"},
|
||||
{"movefocus", "u", "move focus up"},
|
||||
{"movefocus", "d", "move focus down"},
|
||||
{"swapwindow", "l", "swap in left direction"},
|
||||
{"swapwindow", "r", "swap in right direction"},
|
||||
{"swapwindow", "u", "swap in up direction"},
|
||||
{"swapwindow", "d", "swap in down direction"},
|
||||
{"movetoworkspace", "+1", "move to right workspace (non-silent)"},
|
||||
{"movetoworkspace", "-1", "move to left workspace (non-silent)"},
|
||||
{"movetoworkspace", "3", "move to workspace 3 (non-silent)"},
|
||||
{"movetoworkspacesilent", "+1", "move to right workspace"},
|
||||
{"movetoworkspacesilent", "-1", "move to right workspace"},
|
||||
{"movetoworkspacesilent", "2", "move to workspace 2"},
|
||||
{"togglespecialworkspace", "", "toggle special"},
|
||||
{"exec", "firefox", "firefox"},
|
||||
{"unknown", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.dispatcher+"_"+tt.params, func(t *testing.T) {
|
||||
result := hyprlandAutogenerateComment(tt.dispatcher, tt.params)
|
||||
if result != tt.expected {
|
||||
t.Errorf("hyprlandAutogenerateComment(%q, %q) = %q, want %q",
|
||||
tt.dispatcher, tt.params, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *HyprlandKeyBinding
|
||||
}{
|
||||
{
|
||||
name: "basic_keybind",
|
||||
line: "bind = SUPER, Q, killactive",
|
||||
expected: &HyprlandKeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "Q",
|
||||
Dispatcher: "killactive",
|
||||
Params: "",
|
||||
Comment: "Close window",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_params",
|
||||
line: "bind = SUPER, left, movefocus, l",
|
||||
expected: &HyprlandKeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "left",
|
||||
Dispatcher: "movefocus",
|
||||
Params: "l",
|
||||
Comment: "move focus left",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_comment",
|
||||
line: "bind = SUPER, T, exec, kitty # Open terminal",
|
||||
expected: &HyprlandKeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "T",
|
||||
Dispatcher: "exec",
|
||||
Params: "kitty",
|
||||
Comment: "Open terminal",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_hidden",
|
||||
line: "bind = SUPER, H, exec, secret # [hidden]",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_mods",
|
||||
line: "bind = SUPER+SHIFT, F, fullscreen, 0",
|
||||
expected: &HyprlandKeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "F",
|
||||
Dispatcher: "fullscreen",
|
||||
Params: "0",
|
||||
Comment: "Toggle fullscreen",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_no_mods",
|
||||
line: "bind = , Print, exec, screenshot",
|
||||
expected: &HyprlandKeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
Dispatcher: "exec",
|
||||
Params: "screenshot",
|
||||
Comment: "screenshot",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewHyprlandParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Errorf("expected nil, got %+v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Errorf("expected %+v, got nil", tt.expected)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Key != tt.expected.Key {
|
||||
t.Errorf("Key = %q, want %q", result.Key, tt.expected.Key)
|
||||
}
|
||||
if result.Dispatcher != tt.expected.Dispatcher {
|
||||
t.Errorf("Dispatcher = %q, want %q", result.Dispatcher, tt.expected.Dispatcher)
|
||||
}
|
||||
if result.Params != tt.expected.Params {
|
||||
t.Errorf("Params = %q, want %q", result.Params, tt.expected.Params)
|
||||
}
|
||||
if result.Comment != tt.expected.Comment {
|
||||
t.Errorf("Comment = %q, want %q", result.Comment, tt.expected.Comment)
|
||||
}
|
||||
if len(result.Mods) != len(tt.expected.Mods) {
|
||||
t.Errorf("Mods length = %d, want %d", len(result.Mods), len(tt.expected.Mods))
|
||||
} else {
|
||||
for i := range result.Mods {
|
||||
if result.Mods[i] != tt.expected.Mods[i] {
|
||||
t.Errorf("Mods[%d] = %q, want %q", i, result.Mods[i], tt.expected.Mods[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandParseKeysWithSections(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "hyprland.conf")
|
||||
|
||||
content := `##! Window Management
|
||||
bind = SUPER, Q, killactive
|
||||
bind = SUPER, F, fullscreen, 0
|
||||
|
||||
###! Movement
|
||||
bind = SUPER, left, movefocus, l
|
||||
bind = SUPER, right, movefocus, r
|
||||
|
||||
##! Applications
|
||||
bind = SUPER, T, exec, kitty # Terminal
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseHyprlandKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseHyprlandKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Children) != 2 {
|
||||
t.Errorf("Expected 2 top-level sections, got %d", len(section.Children))
|
||||
}
|
||||
|
||||
if len(section.Children) >= 1 {
|
||||
windowMgmt := section.Children[0]
|
||||
if windowMgmt.Name != "Window Management" {
|
||||
t.Errorf("First section name = %q, want %q", windowMgmt.Name, "Window Management")
|
||||
}
|
||||
if len(windowMgmt.Keybinds) != 2 {
|
||||
t.Errorf("Window Management keybinds = %d, want 2", len(windowMgmt.Keybinds))
|
||||
}
|
||||
|
||||
if len(windowMgmt.Children) != 1 {
|
||||
t.Errorf("Window Management children = %d, want 1", len(windowMgmt.Children))
|
||||
} else {
|
||||
movement := windowMgmt.Children[0]
|
||||
if movement.Name != "Movement" {
|
||||
t.Errorf("Movement section name = %q, want %q", movement.Name, "Movement")
|
||||
}
|
||||
if len(movement.Keybinds) != 2 {
|
||||
t.Errorf("Movement keybinds = %d, want 2", len(movement.Keybinds))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(section.Children) >= 2 {
|
||||
apps := section.Children[1]
|
||||
if apps.Name != "Applications" {
|
||||
t.Errorf("Second section name = %q, want %q", apps.Name, "Applications")
|
||||
}
|
||||
if len(apps.Keybinds) != 1 {
|
||||
t.Errorf("Applications keybinds = %d, want 1", len(apps.Keybinds))
|
||||
}
|
||||
if len(apps.Keybinds) > 0 && apps.Keybinds[0].Comment != "Terminal" {
|
||||
t.Errorf("Applications keybind comment = %q, want %q", apps.Keybinds[0].Comment, "Terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandParseKeysWithCommentBinds(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "test.conf")
|
||||
|
||||
content := `#/# = SUPER, A, exec, app1
|
||||
bind = SUPER, B, exec, app2
|
||||
#/# = SUPER, C, exec, app3 # Custom comment
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseHyprlandKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseHyprlandKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) != 3 {
|
||||
t.Errorf("Expected 3 keybinds, got %d", len(section.Keybinds))
|
||||
}
|
||||
|
||||
if len(section.Keybinds) > 0 && section.Keybinds[0].Key != "A" {
|
||||
t.Errorf("First keybind key = %q, want %q", section.Keybinds[0].Key, "A")
|
||||
}
|
||||
if len(section.Keybinds) > 1 && section.Keybinds[1].Key != "B" {
|
||||
t.Errorf("Second keybind key = %q, want %q", section.Keybinds[1].Key, "B")
|
||||
}
|
||||
if len(section.Keybinds) > 2 && section.Keybinds[2].Comment != "Custom comment" {
|
||||
t.Errorf("Third keybind comment = %q, want %q", section.Keybinds[2].Comment, "Custom comment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandReadContentMultipleFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file1 := filepath.Join(tmpDir, "a.conf")
|
||||
file2 := filepath.Join(tmpDir, "b.conf")
|
||||
|
||||
content1 := "bind = SUPER, Q, killactive\n"
|
||||
content2 := "bind = SUPER, T, exec, kitty\n"
|
||||
|
||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file1: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file2: %v", err)
|
||||
}
|
||||
|
||||
parser := NewHyprlandParser()
|
||||
if err := parser.ReadContent(tmpDir); err != nil {
|
||||
t.Fatalf("ReadContent failed: %v", err)
|
||||
}
|
||||
|
||||
section := parser.ParseKeys()
|
||||
if len(section.Keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds from multiple files, got %d", len(section.Keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandReadContentErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "nonexistent_directory",
|
||||
path: "/nonexistent/path/that/does/not/exist",
|
||||
},
|
||||
{
|
||||
name: "empty_directory",
|
||||
path: t.TempDir(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseHyprlandKeys(tt.path)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandReadContentWithTildeExpansion(t *testing.T) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-hypr-"+t.Name())
|
||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||
t.Skip("Cannot create test directory in home")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
configFile := filepath.Join(tmpSubdir, "test.conf")
|
||||
if err := os.WriteFile(configFile, []byte("bind = SUPER, Q, killactive\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(homeDir, tmpSubdir)
|
||||
if err != nil {
|
||||
t.Skip("Cannot create relative path")
|
||||
}
|
||||
|
||||
parser := NewHyprlandParser()
|
||||
tildePathMatch := "~/" + relPath
|
||||
err = parser.ReadContent(tildePathMatch)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ReadContent with tilde path failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandKeybindWithParamsContainingCommas(t *testing.T) {
|
||||
parser := NewHyprlandParser()
|
||||
parser.contentLines = []string{"bind = SUPER, R, exec, notify-send 'Title' 'Message, with comma'"}
|
||||
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if result == nil {
|
||||
t.Fatal("Expected keybind, got nil")
|
||||
}
|
||||
|
||||
expected := "notify-send 'Title' 'Message, with comma'"
|
||||
if result.Params != expected {
|
||||
t.Errorf("Params = %q, want %q", result.Params, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHyprlandEmptyAndCommentLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "test.conf")
|
||||
|
||||
content := `
|
||||
# This is a comment
|
||||
bind = SUPER, Q, killactive
|
||||
|
||||
# Another comment
|
||||
|
||||
bind = SUPER, T, exec, kitty
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseHyprlandKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseHyprlandKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
|
||||
)
|
||||
|
||||
type MangoWCProvider struct {
|
||||
@@ -26,7 +25,7 @@ func (m *MangoWCProvider) Name() string {
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
keybinds_list, err := mangowc.ParseKeys(m.configPath)
|
||||
keybinds_list, err := ParseMangoWCKeys(m.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
|
||||
}
|
||||
@@ -83,7 +82,7 @@ func (m *MangoWCProvider) categorizeByCommand(command string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) convertKeybind(kb *mangowc.KeyBinding) keybinds.Keybind {
|
||||
func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding) keybinds.Keybind {
|
||||
key := m.formatKey(kb)
|
||||
desc := kb.Comment
|
||||
|
||||
@@ -104,7 +103,7 @@ func (m *MangoWCProvider) generateDescription(command, params string) string {
|
||||
return command
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) formatKey(kb *mangowc.KeyBinding) string {
|
||||
func (m *MangoWCProvider) formatKey(kb *MangoWCKeyBinding) string {
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
|
||||
305
core/internal/keybinds/providers/mangowc_parser.go
Normal file
305
core/internal/keybinds/providers/mangowc_parser.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MangoWCHideComment = "[hidden]"
|
||||
)
|
||||
|
||||
var MangoWCModSeparators = []rune{'+', ' '}
|
||||
|
||||
type MangoWCKeyBinding struct {
|
||||
Mods []string `json:"mods"`
|
||||
Key string `json:"key"`
|
||||
Command string `json:"command"`
|
||||
Params string `json:"params"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type MangoWCParser struct {
|
||||
contentLines []string
|
||||
readingLine int
|
||||
}
|
||||
|
||||
func NewMangoWCParser() *MangoWCParser {
|
||||
return &MangoWCParser{
|
||||
contentLines: []string{},
|
||||
readingLine: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) ReadContent(path string) error {
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
expandedPath = filepath.Clean(expandedPath)
|
||||
if strings.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
info, err := os.Stat(expandedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var files []string
|
||||
if info.IsDir() {
|
||||
confFiles, err := filepath.Glob(filepath.Join(expandedPath, "*.conf"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(confFiles) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
files = confFiles
|
||||
} else {
|
||||
files = []string{expandedPath}
|
||||
}
|
||||
|
||||
var combinedContent []string
|
||||
for _, file := range files {
|
||||
if fileInfo, err := os.Stat(file); err == nil && fileInfo.Mode().IsRegular() {
|
||||
data, err := os.ReadFile(file)
|
||||
if err == nil {
|
||||
combinedContent = append(combinedContent, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(combinedContent) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fullContent := strings.Join(combinedContent, "\n")
|
||||
p.contentLines = strings.Split(fullContent, "\n")
|
||||
return nil
|
||||
}
|
||||
|
||||
func mangowcAutogenerateComment(command, params string) string {
|
||||
switch command {
|
||||
case "spawn", "spawn_shell":
|
||||
return params
|
||||
case "killclient":
|
||||
return "Close window"
|
||||
case "quit":
|
||||
return "Exit MangoWC"
|
||||
case "reload_config":
|
||||
return "Reload configuration"
|
||||
case "focusstack":
|
||||
if params == "next" {
|
||||
return "Focus next window"
|
||||
}
|
||||
if params == "prev" {
|
||||
return "Focus previous window"
|
||||
}
|
||||
return "Focus stack " + params
|
||||
case "focusdir":
|
||||
dirMap := map[string]string{
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"up": "up",
|
||||
"down": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "Focus " + dir
|
||||
}
|
||||
return "Focus " + params
|
||||
case "exchange_client":
|
||||
dirMap := map[string]string{
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"up": "up",
|
||||
"down": "down",
|
||||
}
|
||||
if dir, ok := dirMap[params]; ok {
|
||||
return "Swap window " + dir
|
||||
}
|
||||
return "Swap window " + params
|
||||
case "togglefloating":
|
||||
return "Float/unfloat window"
|
||||
case "togglefullscreen":
|
||||
return "Toggle fullscreen"
|
||||
case "togglefakefullscreen":
|
||||
return "Toggle fake fullscreen"
|
||||
case "togglemaximizescreen":
|
||||
return "Toggle maximize"
|
||||
case "toggleglobal":
|
||||
return "Toggle global"
|
||||
case "toggleoverview":
|
||||
return "Toggle overview"
|
||||
case "toggleoverlay":
|
||||
return "Toggle overlay"
|
||||
case "minimized":
|
||||
return "Minimize window"
|
||||
case "restore_minimized":
|
||||
return "Restore minimized"
|
||||
case "toggle_scratchpad":
|
||||
return "Toggle scratchpad"
|
||||
case "setlayout":
|
||||
return "Set layout " + params
|
||||
case "switch_layout":
|
||||
return "Switch layout"
|
||||
case "view":
|
||||
parts := strings.Split(params, ",")
|
||||
if len(parts) > 0 {
|
||||
return "View tag " + parts[0]
|
||||
}
|
||||
return "View tag"
|
||||
case "tag":
|
||||
parts := strings.Split(params, ",")
|
||||
if len(parts) > 0 {
|
||||
return "Move to tag " + parts[0]
|
||||
}
|
||||
return "Move to tag"
|
||||
case "toggleview":
|
||||
parts := strings.Split(params, ",")
|
||||
if len(parts) > 0 {
|
||||
return "Toggle tag " + parts[0]
|
||||
}
|
||||
return "Toggle tag"
|
||||
case "viewtoleft", "viewtoleft_have_client":
|
||||
return "View left tag"
|
||||
case "viewtoright", "viewtoright_have_client":
|
||||
return "View right tag"
|
||||
case "tagtoleft":
|
||||
return "Move to left tag"
|
||||
case "tagtoright":
|
||||
return "Move to right tag"
|
||||
case "focusmon":
|
||||
return "Focus monitor " + params
|
||||
case "tagmon":
|
||||
return "Move to monitor " + params
|
||||
case "incgaps":
|
||||
if strings.HasPrefix(params, "-") {
|
||||
return "Decrease gaps"
|
||||
}
|
||||
return "Increase gaps"
|
||||
case "togglegaps":
|
||||
return "Toggle gaps"
|
||||
case "movewin":
|
||||
return "Move window by " + params
|
||||
case "resizewin":
|
||||
return "Resize window by " + params
|
||||
case "set_proportion":
|
||||
return "Set proportion " + params
|
||||
case "switch_proportion_preset":
|
||||
return "Switch proportion preset"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) getKeybindAtLine(lineNumber int) *MangoWCKeyBinding {
|
||||
if lineNumber >= len(p.contentLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := p.contentLines[lineNumber]
|
||||
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
bindType := matches[1]
|
||||
content := matches[2]
|
||||
|
||||
parts := strings.SplitN(content, "#", 2)
|
||||
keys := parts[0]
|
||||
|
||||
var comment string
|
||||
if len(parts) > 1 {
|
||||
comment = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(comment, MangoWCHideComment) {
|
||||
return nil
|
||||
}
|
||||
|
||||
keyFields := strings.SplitN(keys, ",", 4)
|
||||
if len(keyFields) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(keyFields[0])
|
||||
key := strings.TrimSpace(keyFields[1])
|
||||
command := strings.TrimSpace(keyFields[2])
|
||||
|
||||
var params string
|
||||
if len(keyFields) > 3 {
|
||||
params = strings.TrimSpace(keyFields[3])
|
||||
}
|
||||
|
||||
if comment == "" {
|
||||
comment = mangowcAutogenerateComment(command, params)
|
||||
}
|
||||
|
||||
var modList []string
|
||||
if mods != "" && !strings.EqualFold(mods, "none") {
|
||||
modstring := mods + string(MangoWCModSeparators[0])
|
||||
p := 0
|
||||
for index, char := range modstring {
|
||||
isModSep := false
|
||||
for _, sep := range MangoWCModSeparators {
|
||||
if char == sep {
|
||||
isModSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isModSep {
|
||||
if index-p > 1 {
|
||||
modList = append(modList, modstring[p:index])
|
||||
}
|
||||
p = index + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = bindType
|
||||
|
||||
return &MangoWCKeyBinding{
|
||||
Mods: modList,
|
||||
Key: key,
|
||||
Command: command,
|
||||
Params: params,
|
||||
Comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
||||
var keybinds []MangoWCKeyBinding
|
||||
|
||||
for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ {
|
||||
line := p.contentLines[lineNumber]
|
||||
if line == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(strings.TrimSpace(line), "bind") {
|
||||
continue
|
||||
}
|
||||
|
||||
keybind := p.getKeybindAtLine(lineNumber)
|
||||
if keybind != nil {
|
||||
keybinds = append(keybinds, *keybind)
|
||||
}
|
||||
}
|
||||
|
||||
return keybinds
|
||||
}
|
||||
|
||||
func ParseMangoWCKeys(path string) ([]MangoWCKeyBinding, error) {
|
||||
parser := NewMangoWCParser()
|
||||
if err := parser.ReadContent(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parser.ParseKeys(), nil
|
||||
}
|
||||
499
core/internal/keybinds/providers/mangowc_parser_test.go
Normal file
499
core/internal/keybinds/providers/mangowc_parser_test.go
Normal file
@@ -0,0 +1,499 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMangoWCAutogenerateComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
command string
|
||||
params string
|
||||
expected string
|
||||
}{
|
||||
{"spawn", "kitty", "kitty"},
|
||||
{"spawn_shell", "firefox", "firefox"},
|
||||
{"killclient", "", "Close window"},
|
||||
{"quit", "", "Exit MangoWC"},
|
||||
{"reload_config", "", "Reload configuration"},
|
||||
{"focusstack", "next", "Focus next window"},
|
||||
{"focusstack", "prev", "Focus previous window"},
|
||||
{"focusdir", "left", "Focus left"},
|
||||
{"focusdir", "right", "Focus right"},
|
||||
{"focusdir", "up", "Focus up"},
|
||||
{"focusdir", "down", "Focus down"},
|
||||
{"exchange_client", "left", "Swap window left"},
|
||||
{"exchange_client", "right", "Swap window right"},
|
||||
{"togglefloating", "", "Float/unfloat window"},
|
||||
{"togglefullscreen", "", "Toggle fullscreen"},
|
||||
{"togglefakefullscreen", "", "Toggle fake fullscreen"},
|
||||
{"togglemaximizescreen", "", "Toggle maximize"},
|
||||
{"toggleglobal", "", "Toggle global"},
|
||||
{"toggleoverview", "", "Toggle overview"},
|
||||
{"toggleoverlay", "", "Toggle overlay"},
|
||||
{"minimized", "", "Minimize window"},
|
||||
{"restore_minimized", "", "Restore minimized"},
|
||||
{"toggle_scratchpad", "", "Toggle scratchpad"},
|
||||
{"setlayout", "tile", "Set layout tile"},
|
||||
{"switch_layout", "", "Switch layout"},
|
||||
{"view", "1,0", "View tag 1"},
|
||||
{"tag", "2,0", "Move to tag 2"},
|
||||
{"toggleview", "3,0", "Toggle tag 3"},
|
||||
{"viewtoleft", "", "View left tag"},
|
||||
{"viewtoright", "", "View right tag"},
|
||||
{"viewtoleft_have_client", "", "View left tag"},
|
||||
{"viewtoright_have_client", "", "View right tag"},
|
||||
{"tagtoleft", "", "Move to left tag"},
|
||||
{"tagtoright", "", "Move to right tag"},
|
||||
{"focusmon", "left", "Focus monitor left"},
|
||||
{"tagmon", "right", "Move to monitor right"},
|
||||
{"incgaps", "1", "Increase gaps"},
|
||||
{"incgaps", "-1", "Decrease gaps"},
|
||||
{"togglegaps", "", "Toggle gaps"},
|
||||
{"movewin", "+0,-50", "Move window by +0,-50"},
|
||||
{"resizewin", "+0,+50", "Resize window by +0,+50"},
|
||||
{"set_proportion", "1.0", "Set proportion 1.0"},
|
||||
{"switch_proportion_preset", "", "Switch proportion preset"},
|
||||
{"unknown", "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.command+"_"+tt.params, func(t *testing.T) {
|
||||
result := mangowcAutogenerateComment(tt.command, tt.params)
|
||||
if result != tt.expected {
|
||||
t.Errorf("mangowcAutogenerateComment(%q, %q) = %q, want %q",
|
||||
tt.command, tt.params, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *MangoWCKeyBinding
|
||||
}{
|
||||
{
|
||||
name: "basic_keybind",
|
||||
line: "bind=ALT,q,killclient,",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "q",
|
||||
Command: "killclient",
|
||||
Params: "",
|
||||
Comment: "Close window",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_params",
|
||||
line: "bind=ALT,Left,focusdir,left",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "Left",
|
||||
Command: "focusdir",
|
||||
Params: "left",
|
||||
Comment: "Focus left",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_comment",
|
||||
line: "bind=Alt,t,spawn,kitty # Open terminal",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"Alt"},
|
||||
Key: "t",
|
||||
Command: "spawn",
|
||||
Params: "kitty",
|
||||
Comment: "Open terminal",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_hidden",
|
||||
line: "bind=SUPER,h,spawn,secret # [hidden]",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_mods",
|
||||
line: "bind=SUPER+SHIFT,Up,exchange_client,up",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "Up",
|
||||
Command: "exchange_client",
|
||||
Params: "up",
|
||||
Comment: "Swap window up",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_no_mods",
|
||||
line: "bind=NONE,Print,spawn,screenshot",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
Command: "spawn",
|
||||
Params: "screenshot",
|
||||
Comment: "screenshot",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_params",
|
||||
line: "bind=Ctrl,1,view,1,0",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"Ctrl"},
|
||||
Key: "1",
|
||||
Command: "view",
|
||||
Params: "1,0",
|
||||
Comment: "View tag 1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bindl_flag",
|
||||
line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER", "ALT"},
|
||||
Key: "l",
|
||||
Command: "spawn",
|
||||
Params: "dms ipc call lock lock",
|
||||
Comment: "dms ipc call lock lock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_spaces",
|
||||
line: "bind = SUPER, r, reload_config",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "r",
|
||||
Command: "reload_config",
|
||||
Params: "",
|
||||
Comment: "Reload configuration",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewMangoWCParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Errorf("expected nil, got %+v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Errorf("expected %+v, got nil", tt.expected)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Key != tt.expected.Key {
|
||||
t.Errorf("Key = %q, want %q", result.Key, tt.expected.Key)
|
||||
}
|
||||
if result.Command != tt.expected.Command {
|
||||
t.Errorf("Command = %q, want %q", result.Command, tt.expected.Command)
|
||||
}
|
||||
if result.Params != tt.expected.Params {
|
||||
t.Errorf("Params = %q, want %q", result.Params, tt.expected.Params)
|
||||
}
|
||||
if result.Comment != tt.expected.Comment {
|
||||
t.Errorf("Comment = %q, want %q", result.Comment, tt.expected.Comment)
|
||||
}
|
||||
if len(result.Mods) != len(tt.expected.Mods) {
|
||||
t.Errorf("Mods length = %d, want %d", len(result.Mods), len(tt.expected.Mods))
|
||||
} else {
|
||||
for i := range result.Mods {
|
||||
if result.Mods[i] != tt.expected.Mods[i] {
|
||||
t.Errorf("Mods[%d] = %q, want %q", i, result.Mods[i], tt.expected.Mods[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCParseKeys(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `# MangoWC Configuration
|
||||
blur=0
|
||||
border_radius=12
|
||||
|
||||
# Key Bindings
|
||||
bind=SUPER,r,reload_config
|
||||
bind=Alt,t,spawn,kitty # Terminal
|
||||
bind=ALT,q,killclient,
|
||||
bind=ALT,Left,focusdir,left
|
||||
|
||||
# Hidden binding
|
||||
bind=SUPER,h,spawn,secret # [hidden]
|
||||
|
||||
# Multiple modifiers
|
||||
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||
|
||||
# Workspace bindings
|
||||
bind=Ctrl,1,view,1,0
|
||||
bind=Ctrl,2,view,2,0
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
keybinds, err := ParseMangoWCKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMangoWCKeys failed: %v", err)
|
||||
}
|
||||
|
||||
expectedCount := 7
|
||||
if len(keybinds) != expectedCount {
|
||||
t.Errorf("Expected %d keybinds, got %d", expectedCount, len(keybinds))
|
||||
}
|
||||
|
||||
if len(keybinds) > 0 && keybinds[0].Command != "reload_config" {
|
||||
t.Errorf("First keybind command = %q, want %q", keybinds[0].Command, "reload_config")
|
||||
}
|
||||
|
||||
foundHidden := false
|
||||
for _, kb := range keybinds {
|
||||
if kb.Command == "spawn" && kb.Params == "secret" {
|
||||
foundHidden = true
|
||||
}
|
||||
}
|
||||
if foundHidden {
|
||||
t.Error("Hidden keybind should not be included in results")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCReadContentMultipleFiles(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
file1 := filepath.Join(tmpDir, "a.conf")
|
||||
file2 := filepath.Join(tmpDir, "b.conf")
|
||||
|
||||
content1 := "bind=ALT,q,killclient,\n"
|
||||
content2 := "bind=Alt,t,spawn,kitty\n"
|
||||
|
||||
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file1: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
|
||||
t.Fatalf("Failed to write file2: %v", err)
|
||||
}
|
||||
|
||||
parser := NewMangoWCParser()
|
||||
if err := parser.ReadContent(tmpDir); err != nil {
|
||||
t.Fatalf("ReadContent failed: %v", err)
|
||||
}
|
||||
|
||||
keybinds := parser.ParseKeys()
|
||||
if len(keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds from multiple files, got %d", len(keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCReadContentSingleFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := "bind=ALT,q,killclient,\n"
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
parser := NewMangoWCParser()
|
||||
if err := parser.ReadContent(configFile); err != nil {
|
||||
t.Fatalf("ReadContent failed: %v", err)
|
||||
}
|
||||
|
||||
keybinds := parser.ParseKeys()
|
||||
if len(keybinds) != 1 {
|
||||
t.Errorf("Expected 1 keybind, got %d", len(keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCReadContentErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "nonexistent_directory",
|
||||
path: "/nonexistent/path/that/does/not/exist",
|
||||
},
|
||||
{
|
||||
name: "empty_directory",
|
||||
path: t.TempDir(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseMangoWCKeys(tt.path)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCReadContentWithTildeExpansion(t *testing.T) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
|
||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||
t.Skip("Cannot create test directory in home")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
configFile := filepath.Join(tmpSubdir, "config.conf")
|
||||
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(homeDir, tmpSubdir)
|
||||
if err != nil {
|
||||
t.Skip("Cannot create relative path")
|
||||
}
|
||||
|
||||
parser := NewMangoWCParser()
|
||||
tildePathMatch := "~/" + relPath
|
||||
err = parser.ReadContent(tildePathMatch)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ReadContent with tilde path failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCEmptyAndCommentLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `
|
||||
# This is a comment
|
||||
bind=ALT,q,killclient,
|
||||
|
||||
# Another comment
|
||||
|
||||
bind=Alt,t,spawn,kitty
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
keybinds, err := ParseMangoWCKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMangoWCKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCInvalidBindLines(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
}{
|
||||
{
|
||||
name: "missing_parts",
|
||||
line: "bind=SUPER,q",
|
||||
},
|
||||
{
|
||||
name: "not_bind",
|
||||
line: "blur=0",
|
||||
},
|
||||
{
|
||||
name: "empty_line",
|
||||
line: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewMangoWCParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if result != nil {
|
||||
t.Errorf("expected nil for invalid line, got %+v", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCRealWorldConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config.conf")
|
||||
|
||||
content := `# Application Launchers
|
||||
bind=Alt,t,spawn,kitty
|
||||
bind=Alt,space,spawn,dms ipc call spotlight toggle
|
||||
bind=Alt,v,spawn,dms ipc call clipboard toggle
|
||||
|
||||
# exit
|
||||
bind=ALT+SHIFT,e,quit
|
||||
bind=ALT,q,killclient,
|
||||
|
||||
# switch window focus
|
||||
bind=SUPER,Tab,focusstack,next
|
||||
bind=ALT,Left,focusdir,left
|
||||
bind=ALT,Right,focusdir,right
|
||||
|
||||
# tag switch
|
||||
bind=SUPER,Left,viewtoleft,0
|
||||
bind=CTRL,Left,viewtoleft_have_client,0
|
||||
bind=SUPER,Right,viewtoright,0
|
||||
|
||||
bind=Ctrl,1,view,1,0
|
||||
bind=Ctrl,2,view,2,0
|
||||
bind=Ctrl,3,view,3,0
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
keybinds, err := ParseMangoWCKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseMangoWCKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(keybinds) < 14 {
|
||||
t.Errorf("Expected at least 14 keybinds, got %d", len(keybinds))
|
||||
}
|
||||
|
||||
foundSpawn := false
|
||||
foundQuit := false
|
||||
foundView := false
|
||||
|
||||
for _, kb := range keybinds {
|
||||
if kb.Command == "spawn" && kb.Params == "kitty" {
|
||||
foundSpawn = true
|
||||
}
|
||||
if kb.Command == "quit" {
|
||||
foundQuit = true
|
||||
}
|
||||
if kb.Command == "view" && kb.Params == "1,0" {
|
||||
foundView = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundSpawn {
|
||||
t.Error("Did not find spawn kitty keybind")
|
||||
}
|
||||
if !foundQuit {
|
||||
t.Error("Did not find quit keybind")
|
||||
}
|
||||
if !foundView {
|
||||
t.Error("Did not find view workspace 1 keybind")
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/mangowc"
|
||||
)
|
||||
|
||||
func TestMangoWCProviderName(t *testing.T) {
|
||||
@@ -88,12 +86,12 @@ func TestMangoWCCategorizeByCommand(t *testing.T) {
|
||||
func TestMangoWCFormatKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *mangowc.KeyBinding
|
||||
keybind *MangoWCKeyBinding
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single_mod",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
keybind: &MangoWCKeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "q",
|
||||
},
|
||||
@@ -101,7 +99,7 @@ func TestMangoWCFormatKey(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple_mods",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
keybind: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "Up",
|
||||
},
|
||||
@@ -109,7 +107,7 @@ func TestMangoWCFormatKey(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "no_mods",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
keybind: &MangoWCKeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
},
|
||||
@@ -131,13 +129,13 @@ func TestMangoWCFormatKey(t *testing.T) {
|
||||
func TestMangoWCConvertKeybind(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *mangowc.KeyBinding
|
||||
keybind *MangoWCKeyBinding
|
||||
wantKey string
|
||||
wantDesc string
|
||||
}{
|
||||
{
|
||||
name: "with_comment",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
keybind: &MangoWCKeyBinding{
|
||||
Mods: []string{"ALT"},
|
||||
Key: "t",
|
||||
Command: "spawn",
|
||||
@@ -149,7 +147,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "without_comment",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
keybind: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "r",
|
||||
Command: "reload_config",
|
||||
@@ -161,7 +159,7 @@ func TestMangoWCConvertKeybind(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "with_params_no_comment",
|
||||
keybind: &mangowc.KeyBinding{
|
||||
keybind: &MangoWCKeyBinding{
|
||||
Mods: []string{"CTRL"},
|
||||
Key: "1",
|
||||
Command: "view",
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
|
||||
)
|
||||
|
||||
type SwayProvider struct {
|
||||
@@ -26,7 +25,7 @@ func (s *SwayProvider) Name() string {
|
||||
}
|
||||
|
||||
func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
section, err := sway.ParseKeys(s.configPath)
|
||||
section, err := ParseSwayKeys(s.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sway config: %w", err)
|
||||
}
|
||||
@@ -41,7 +40,7 @@ func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SwayProvider) convertSection(section *sway.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||
func (s *SwayProvider) convertSection(section *SwaySection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
|
||||
currentSubcat := subcategory
|
||||
if section.Name != "" {
|
||||
currentSubcat = section.Name
|
||||
@@ -89,7 +88,7 @@ func (s *SwayProvider) categorizeByCommand(command string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) keybinds.Keybind {
|
||||
func (s *SwayProvider) convertKeybind(kb *SwayKeyBinding, subcategory string) keybinds.Keybind {
|
||||
key := s.formatKey(kb)
|
||||
desc := kb.Comment
|
||||
|
||||
@@ -104,7 +103,7 @@ func (s *SwayProvider) convertKeybind(kb *sway.KeyBinding, subcategory string) k
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SwayProvider) formatKey(kb *sway.KeyBinding) string {
|
||||
func (s *SwayProvider) formatKey(kb *SwayKeyBinding) string {
|
||||
parts := make([]string, 0, len(kb.Mods)+1)
|
||||
parts = append(parts, kb.Mods...)
|
||||
parts = append(parts, kb.Key)
|
||||
|
||||
367
core/internal/keybinds/providers/sway_parser.go
Normal file
367
core/internal/keybinds/providers/sway_parser.go
Normal file
@@ -0,0 +1,367 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
SwayTitleRegex = "#+!"
|
||||
SwayHideComment = "[hidden]"
|
||||
)
|
||||
|
||||
var SwayModSeparators = []rune{'+', ' '}
|
||||
|
||||
type SwayKeyBinding struct {
|
||||
Mods []string `json:"mods"`
|
||||
Key string `json:"key"`
|
||||
Command string `json:"command"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
|
||||
type SwaySection struct {
|
||||
Children []SwaySection `json:"children"`
|
||||
Keybinds []SwayKeyBinding `json:"keybinds"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SwayParser struct {
|
||||
contentLines []string
|
||||
readingLine int
|
||||
variables map[string]string
|
||||
}
|
||||
|
||||
func NewSwayParser() *SwayParser {
|
||||
return &SwayParser{
|
||||
contentLines: []string{},
|
||||
readingLine: 0,
|
||||
variables: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SwayParser) ReadContent(path string) error {
|
||||
expandedPath := os.ExpandEnv(path)
|
||||
expandedPath = filepath.Clean(expandedPath)
|
||||
if strings.HasPrefix(expandedPath, "~") {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expandedPath = filepath.Join(home, expandedPath[1:])
|
||||
}
|
||||
|
||||
info, err := os.Stat(expandedPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var files []string
|
||||
if info.IsDir() {
|
||||
mainConfig := filepath.Join(expandedPath, "config")
|
||||
if fileInfo, err := os.Stat(mainConfig); err == nil && fileInfo.Mode().IsRegular() {
|
||||
files = []string{mainConfig}
|
||||
} else {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
} else {
|
||||
files = []string{expandedPath}
|
||||
}
|
||||
|
||||
var combinedContent []string
|
||||
for _, file := range files {
|
||||
data, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
combinedContent = append(combinedContent, string(data))
|
||||
}
|
||||
|
||||
if len(combinedContent) == 0 {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
fullContent := strings.Join(combinedContent, "\n")
|
||||
p.contentLines = strings.Split(fullContent, "\n")
|
||||
p.parseVariables()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *SwayParser) parseVariables() {
|
||||
setRegex := regexp.MustCompile(`^\s*set\s+\$(\w+)\s+(.+)$`)
|
||||
for _, line := range p.contentLines {
|
||||
matches := setRegex.FindStringSubmatch(line)
|
||||
if len(matches) == 3 {
|
||||
varName := matches[1]
|
||||
varValue := strings.TrimSpace(matches[2])
|
||||
p.variables[varName] = varValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SwayParser) expandVariables(text string) string {
|
||||
result := text
|
||||
for varName, varValue := range p.variables {
|
||||
result = strings.ReplaceAll(result, "$"+varName, varValue)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func swayAutogenerateComment(command string) string {
|
||||
command = strings.TrimSpace(command)
|
||||
|
||||
if strings.HasPrefix(command, "exec ") {
|
||||
cmdPart := strings.TrimPrefix(command, "exec ")
|
||||
cmdPart = strings.TrimPrefix(cmdPart, "--no-startup-id ")
|
||||
return cmdPart
|
||||
}
|
||||
|
||||
switch {
|
||||
case command == "kill":
|
||||
return "Close window"
|
||||
case command == "exit":
|
||||
return "Exit Sway"
|
||||
case command == "reload":
|
||||
return "Reload configuration"
|
||||
case strings.HasPrefix(command, "fullscreen"):
|
||||
return "Toggle fullscreen"
|
||||
case strings.HasPrefix(command, "floating toggle"):
|
||||
return "Float/unfloat window"
|
||||
case strings.HasPrefix(command, "focus mode_toggle"):
|
||||
return "Toggle focus mode"
|
||||
case strings.HasPrefix(command, "focus parent"):
|
||||
return "Focus parent container"
|
||||
case strings.HasPrefix(command, "focus left"):
|
||||
return "Focus left"
|
||||
case strings.HasPrefix(command, "focus right"):
|
||||
return "Focus right"
|
||||
case strings.HasPrefix(command, "focus up"):
|
||||
return "Focus up"
|
||||
case strings.HasPrefix(command, "focus down"):
|
||||
return "Focus down"
|
||||
case strings.HasPrefix(command, "focus output"):
|
||||
return "Focus monitor"
|
||||
case strings.HasPrefix(command, "move left"):
|
||||
return "Move window left"
|
||||
case strings.HasPrefix(command, "move right"):
|
||||
return "Move window right"
|
||||
case strings.HasPrefix(command, "move up"):
|
||||
return "Move window up"
|
||||
case strings.HasPrefix(command, "move down"):
|
||||
return "Move window down"
|
||||
case strings.HasPrefix(command, "move container to workspace"):
|
||||
if strings.Contains(command, "prev") {
|
||||
return "Move to previous workspace"
|
||||
}
|
||||
if strings.Contains(command, "next") {
|
||||
return "Move to next workspace"
|
||||
}
|
||||
parts := strings.Fields(command)
|
||||
if len(parts) > 4 {
|
||||
return "Move to workspace " + parts[len(parts)-1]
|
||||
}
|
||||
return "Move to workspace"
|
||||
case strings.HasPrefix(command, "move workspace to output"):
|
||||
return "Move workspace to monitor"
|
||||
case strings.HasPrefix(command, "workspace"):
|
||||
if strings.Contains(command, "prev") {
|
||||
return "Previous workspace"
|
||||
}
|
||||
if strings.Contains(command, "next") {
|
||||
return "Next workspace"
|
||||
}
|
||||
parts := strings.Fields(command)
|
||||
if len(parts) > 1 {
|
||||
wsNum := parts[len(parts)-1]
|
||||
return "Workspace " + wsNum
|
||||
}
|
||||
return "Switch workspace"
|
||||
case strings.HasPrefix(command, "layout"):
|
||||
parts := strings.Fields(command)
|
||||
if len(parts) > 1 {
|
||||
return "Layout " + parts[1]
|
||||
}
|
||||
return "Change layout"
|
||||
case strings.HasPrefix(command, "split"):
|
||||
if strings.Contains(command, "h") {
|
||||
return "Split horizontal"
|
||||
}
|
||||
if strings.Contains(command, "v") {
|
||||
return "Split vertical"
|
||||
}
|
||||
return "Split container"
|
||||
case strings.HasPrefix(command, "resize"):
|
||||
return "Resize window"
|
||||
case strings.Contains(command, "scratchpad"):
|
||||
return "Toggle scratchpad"
|
||||
default:
|
||||
return command
|
||||
}
|
||||
}
|
||||
|
||||
func (p *SwayParser) getKeybindAtLine(lineNumber int) *SwayKeyBinding {
|
||||
if lineNumber >= len(p.contentLines) {
|
||||
return nil
|
||||
}
|
||||
|
||||
line := p.contentLines[lineNumber]
|
||||
|
||||
bindMatch := regexp.MustCompile(`^\s*(bindsym|bindcode)\s+(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
return nil
|
||||
}
|
||||
|
||||
content := matches[2]
|
||||
|
||||
parts := strings.SplitN(content, "#", 2)
|
||||
keys := strings.TrimSpace(parts[0])
|
||||
|
||||
var comment string
|
||||
if len(parts) > 1 {
|
||||
comment = strings.TrimSpace(parts[1])
|
||||
}
|
||||
|
||||
if strings.HasPrefix(comment, SwayHideComment) {
|
||||
return nil
|
||||
}
|
||||
|
||||
flags := ""
|
||||
if strings.HasPrefix(keys, "--") {
|
||||
spaceIdx := strings.Index(keys, " ")
|
||||
if spaceIdx > 0 {
|
||||
flags = keys[:spaceIdx]
|
||||
keys = strings.TrimSpace(keys[spaceIdx+1:])
|
||||
}
|
||||
}
|
||||
|
||||
keyParts := strings.Fields(keys)
|
||||
if len(keyParts) < 2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
keyCombo := keyParts[0]
|
||||
keyCombo = p.expandVariables(keyCombo)
|
||||
command := strings.Join(keyParts[1:], " ")
|
||||
command = p.expandVariables(command)
|
||||
|
||||
var modList []string
|
||||
var key string
|
||||
|
||||
modstring := keyCombo + string(SwayModSeparators[0])
|
||||
pos := 0
|
||||
for index, char := range modstring {
|
||||
isModSep := false
|
||||
for _, sep := range SwayModSeparators {
|
||||
if char == sep {
|
||||
isModSep = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isModSep {
|
||||
if index-pos > 0 {
|
||||
part := modstring[pos:index]
|
||||
if swayIsMod(part) {
|
||||
modList = append(modList, part)
|
||||
} else {
|
||||
key = part
|
||||
}
|
||||
}
|
||||
pos = index + 1
|
||||
}
|
||||
}
|
||||
|
||||
if comment == "" {
|
||||
comment = swayAutogenerateComment(command)
|
||||
}
|
||||
|
||||
_ = flags
|
||||
|
||||
return &SwayKeyBinding{
|
||||
Mods: modList,
|
||||
Key: key,
|
||||
Command: command,
|
||||
Comment: comment,
|
||||
}
|
||||
}
|
||||
|
||||
func swayIsMod(s string) bool {
|
||||
s = strings.ToLower(s)
|
||||
if s == "mod1" || s == "mod2" || s == "mod3" || s == "mod4" || s == "mod5" ||
|
||||
s == "shift" || s == "control" || s == "ctrl" || s == "alt" || s == "super" ||
|
||||
strings.HasPrefix(s, "$") {
|
||||
return true
|
||||
}
|
||||
|
||||
isNumeric := true
|
||||
for _, c := range s {
|
||||
if c < '0' || c > '9' {
|
||||
isNumeric = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isNumeric && len(s) >= 2 {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *SwayParser) getBindsRecursive(currentContent *SwaySection, scope int) *SwaySection {
|
||||
titleRegex := regexp.MustCompile(SwayTitleRegex)
|
||||
|
||||
for p.readingLine < len(p.contentLines) {
|
||||
line := p.contentLines[p.readingLine]
|
||||
|
||||
loc := titleRegex.FindStringIndex(line)
|
||||
if loc != nil && loc[0] == 0 {
|
||||
headingScope := strings.Index(line, "!")
|
||||
|
||||
if headingScope <= scope {
|
||||
p.readingLine--
|
||||
return currentContent
|
||||
}
|
||||
|
||||
sectionName := strings.TrimSpace(line[headingScope+1:])
|
||||
p.readingLine++
|
||||
|
||||
childSection := &SwaySection{
|
||||
Children: []SwaySection{},
|
||||
Keybinds: []SwayKeyBinding{},
|
||||
Name: sectionName,
|
||||
}
|
||||
result := p.getBindsRecursive(childSection, headingScope)
|
||||
currentContent.Children = append(currentContent.Children, *result)
|
||||
|
||||
} else if line == "" || (!strings.Contains(line, "bindsym") && !strings.Contains(line, "bindcode")) {
|
||||
|
||||
} else {
|
||||
keybind := p.getKeybindAtLine(p.readingLine)
|
||||
if keybind != nil {
|
||||
currentContent.Keybinds = append(currentContent.Keybinds, *keybind)
|
||||
}
|
||||
}
|
||||
|
||||
p.readingLine++
|
||||
}
|
||||
|
||||
return currentContent
|
||||
}
|
||||
|
||||
func (p *SwayParser) ParseKeys() *SwaySection {
|
||||
p.readingLine = 0
|
||||
rootSection := &SwaySection{
|
||||
Children: []SwaySection{},
|
||||
Keybinds: []SwayKeyBinding{},
|
||||
Name: "",
|
||||
}
|
||||
return p.getBindsRecursive(rootSection, 0)
|
||||
}
|
||||
|
||||
func ParseSwayKeys(path string) (*SwaySection, error) {
|
||||
parser := NewSwayParser()
|
||||
if err := parser.ReadContent(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parser.ParseKeys(), nil
|
||||
}
|
||||
471
core/internal/keybinds/providers/sway_parser_test.go
Normal file
471
core/internal/keybinds/providers/sway_parser_test.go
Normal file
@@ -0,0 +1,471 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSwayAutogenerateComment(t *testing.T) {
|
||||
tests := []struct {
|
||||
command string
|
||||
expected string
|
||||
}{
|
||||
{"exec kitty", "kitty"},
|
||||
{"exec --no-startup-id firefox", "firefox"},
|
||||
{"kill", "Close window"},
|
||||
{"exit", "Exit Sway"},
|
||||
{"reload", "Reload configuration"},
|
||||
{"fullscreen toggle", "Toggle fullscreen"},
|
||||
{"floating toggle", "Float/unfloat window"},
|
||||
{"focus mode_toggle", "Toggle focus mode"},
|
||||
{"focus parent", "Focus parent container"},
|
||||
{"focus left", "Focus left"},
|
||||
{"focus right", "Focus right"},
|
||||
{"focus up", "Focus up"},
|
||||
{"focus down", "Focus down"},
|
||||
{"focus output left", "Focus monitor"},
|
||||
{"move left", "Move window left"},
|
||||
{"move right", "Move window right"},
|
||||
{"move up", "Move window up"},
|
||||
{"move down", "Move window down"},
|
||||
{"move container to workspace number 1", "Move to workspace 1"},
|
||||
{"move container to workspace prev", "Move to previous workspace"},
|
||||
{"move container to workspace next", "Move to next workspace"},
|
||||
{"move workspace to output left", "Move workspace to monitor"},
|
||||
{"workspace number 1", "Workspace 1"},
|
||||
{"workspace prev", "Previous workspace"},
|
||||
{"workspace next", "Next workspace"},
|
||||
{"layout tabbed", "Layout tabbed"},
|
||||
{"layout stacking", "Layout stacking"},
|
||||
{"splith", "Split horizontal"},
|
||||
{"splitv", "Split vertical"},
|
||||
{"resize grow width 10 ppt", "Resize window"},
|
||||
{"move scratchpad", "Toggle scratchpad"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.command, func(t *testing.T) {
|
||||
result := swayAutogenerateComment(tt.command)
|
||||
if result != tt.expected {
|
||||
t.Errorf("swayAutogenerateComment(%q) = %q, want %q",
|
||||
tt.command, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *SwayKeyBinding
|
||||
}{
|
||||
{
|
||||
name: "basic_keybind",
|
||||
line: "bindsym Mod4+q kill",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "q",
|
||||
Command: "kill",
|
||||
Comment: "Close window",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_exec",
|
||||
line: "bindsym Mod4+t exec kitty",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "t",
|
||||
Command: "exec kitty",
|
||||
Comment: "kitty",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_comment",
|
||||
line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "Space",
|
||||
Command: "exec dms ipc call spotlight toggle",
|
||||
Comment: "Open launcher",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_hidden",
|
||||
line: "bindsym Mod4+h exec secret # [hidden]",
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "keybind_multiple_mods",
|
||||
line: "bindsym Mod4+Shift+e exit",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4", "Shift"},
|
||||
Key: "e",
|
||||
Command: "exit",
|
||||
Comment: "Exit Sway",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_no_mods",
|
||||
line: "bindsym Print exec grim screenshot.png",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
Command: "exec grim screenshot.png",
|
||||
Comment: "grim screenshot.png",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_flags",
|
||||
line: "bindsym --release Mod4+x exec notify-send released",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "x",
|
||||
Command: "exec notify-send released",
|
||||
Comment: "notify-send released",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_focus_direction",
|
||||
line: "bindsym Mod4+Left focus left",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "Left",
|
||||
Command: "focus left",
|
||||
Comment: "Focus left",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_workspace",
|
||||
line: "bindsym Mod4+1 workspace number 1",
|
||||
expected: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "1",
|
||||
Command: "workspace number 1",
|
||||
Comment: "Workspace 1",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewSwayParser()
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
t.Errorf("expected nil, got %+v", result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
t.Errorf("expected %+v, got nil", tt.expected)
|
||||
return
|
||||
}
|
||||
|
||||
if result.Key != tt.expected.Key {
|
||||
t.Errorf("Key = %q, want %q", result.Key, tt.expected.Key)
|
||||
}
|
||||
if result.Command != tt.expected.Command {
|
||||
t.Errorf("Command = %q, want %q", result.Command, tt.expected.Command)
|
||||
}
|
||||
if result.Comment != tt.expected.Comment {
|
||||
t.Errorf("Comment = %q, want %q", result.Comment, tt.expected.Comment)
|
||||
}
|
||||
if len(result.Mods) != len(tt.expected.Mods) {
|
||||
t.Errorf("Mods length = %d, want %d", len(result.Mods), len(tt.expected.Mods))
|
||||
} else {
|
||||
for i := range result.Mods {
|
||||
if result.Mods[i] != tt.expected.Mods[i] {
|
||||
t.Errorf("Mods[%d] = %q, want %q", i, result.Mods[i], tt.expected.Mods[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayVariableExpansion(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config")
|
||||
|
||||
content := `set $mod Mod4
|
||||
set $term kitty
|
||||
set $menu rofi
|
||||
|
||||
bindsym $mod+t exec $term
|
||||
bindsym $mod+d exec $menu
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseSwayKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSwayKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds, got %d", len(section.Keybinds))
|
||||
}
|
||||
|
||||
if len(section.Keybinds) > 0 {
|
||||
if section.Keybinds[0].Mods[0] != "Mod4" {
|
||||
t.Errorf("Expected Mod4, got %q", section.Keybinds[0].Mods[0])
|
||||
}
|
||||
if section.Keybinds[0].Command != "exec kitty" {
|
||||
t.Errorf("Expected 'exec kitty', got %q", section.Keybinds[0].Command)
|
||||
}
|
||||
}
|
||||
|
||||
if len(section.Keybinds) > 1 {
|
||||
if section.Keybinds[1].Command != "exec rofi" {
|
||||
t.Errorf("Expected 'exec rofi', got %q", section.Keybinds[1].Command)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayParseKeysWithSections(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config")
|
||||
|
||||
content := `set $mod Mod4
|
||||
|
||||
##! Window Management
|
||||
bindsym $mod+q kill
|
||||
bindsym $mod+f fullscreen toggle
|
||||
|
||||
###! Focus
|
||||
bindsym $mod+Left focus left
|
||||
bindsym $mod+Right focus right
|
||||
|
||||
##! Applications
|
||||
bindsym $mod+t exec kitty # Terminal
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseSwayKeys(tmpDir)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSwayKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Children) != 2 {
|
||||
t.Errorf("Expected 2 top-level sections, got %d", len(section.Children))
|
||||
}
|
||||
|
||||
if len(section.Children) >= 1 {
|
||||
windowMgmt := section.Children[0]
|
||||
if windowMgmt.Name != "Window Management" {
|
||||
t.Errorf("First section name = %q, want %q", windowMgmt.Name, "Window Management")
|
||||
}
|
||||
if len(windowMgmt.Keybinds) != 2 {
|
||||
t.Errorf("Window Management keybinds = %d, want 2", len(windowMgmt.Keybinds))
|
||||
}
|
||||
|
||||
if len(windowMgmt.Children) != 1 {
|
||||
t.Errorf("Window Management children = %d, want 1", len(windowMgmt.Children))
|
||||
} else {
|
||||
focus := windowMgmt.Children[0]
|
||||
if focus.Name != "Focus" {
|
||||
t.Errorf("Focus section name = %q, want %q", focus.Name, "Focus")
|
||||
}
|
||||
if len(focus.Keybinds) != 2 {
|
||||
t.Errorf("Focus keybinds = %d, want 2", len(focus.Keybinds))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(section.Children) >= 2 {
|
||||
apps := section.Children[1]
|
||||
if apps.Name != "Applications" {
|
||||
t.Errorf("Second section name = %q, want %q", apps.Name, "Applications")
|
||||
}
|
||||
if len(apps.Keybinds) != 1 {
|
||||
t.Errorf("Applications keybinds = %d, want 1", len(apps.Keybinds))
|
||||
}
|
||||
if len(apps.Keybinds) > 0 && apps.Keybinds[0].Comment != "Terminal" {
|
||||
t.Errorf("Applications keybind comment = %q, want %q", apps.Keybinds[0].Comment, "Terminal")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayReadContentErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
}{
|
||||
{
|
||||
name: "nonexistent_directory",
|
||||
path: "/nonexistent/path/that/does/not/exist",
|
||||
},
|
||||
{
|
||||
name: "empty_directory",
|
||||
path: t.TempDir(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseSwayKeys(tt.path)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayReadContentWithTildeExpansion(t *testing.T) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
t.Skip("Cannot get home directory")
|
||||
}
|
||||
|
||||
tmpSubdir := filepath.Join(homeDir, ".config", "test-sway-"+t.Name())
|
||||
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
|
||||
t.Skip("Cannot create test directory in home")
|
||||
}
|
||||
defer os.RemoveAll(tmpSubdir)
|
||||
|
||||
configFile := filepath.Join(tmpSubdir, "config")
|
||||
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(homeDir, tmpSubdir)
|
||||
if err != nil {
|
||||
t.Skip("Cannot create relative path")
|
||||
}
|
||||
|
||||
parser := NewSwayParser()
|
||||
tildePathMatch := "~/" + relPath
|
||||
err = parser.ReadContent(tildePathMatch)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("ReadContent with tilde path failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayEmptyAndCommentLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config")
|
||||
|
||||
content := `
|
||||
# This is a comment
|
||||
bindsym Mod4+q kill
|
||||
|
||||
# Another comment
|
||||
|
||||
bindsym Mod4+t exec kitty
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseSwayKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSwayKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) != 2 {
|
||||
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayRealWorldConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
configFile := filepath.Join(tmpDir, "config")
|
||||
|
||||
content := `set $mod Mod4
|
||||
set $term kitty
|
||||
|
||||
## Application Launchers
|
||||
bindsym $mod+t exec $term
|
||||
bindsym $mod+Space exec rofi
|
||||
|
||||
## Window Management
|
||||
bindsym $mod+q kill
|
||||
bindsym $mod+f fullscreen toggle
|
||||
|
||||
## Focus Navigation
|
||||
bindsym $mod+Left focus left
|
||||
bindsym $mod+Right focus right
|
||||
|
||||
## Workspace Navigation
|
||||
bindsym $mod+1 workspace number 1
|
||||
bindsym $mod+2 workspace number 2
|
||||
bindsym $mod+Shift+1 move container to workspace number 1
|
||||
`
|
||||
|
||||
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("Failed to write test config: %v", err)
|
||||
}
|
||||
|
||||
section, err := ParseSwayKeys(configFile)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseSwayKeys failed: %v", err)
|
||||
}
|
||||
|
||||
if len(section.Keybinds) < 9 {
|
||||
t.Errorf("Expected at least 9 keybinds, got %d", len(section.Keybinds))
|
||||
}
|
||||
|
||||
foundExec := false
|
||||
foundKill := false
|
||||
foundWorkspace := false
|
||||
|
||||
for _, kb := range section.Keybinds {
|
||||
if kb.Command == "exec kitty" {
|
||||
foundExec = true
|
||||
}
|
||||
if kb.Command == "kill" {
|
||||
foundKill = true
|
||||
}
|
||||
if kb.Command == "workspace number 1" {
|
||||
foundWorkspace = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundExec {
|
||||
t.Error("Did not find exec kitty keybind")
|
||||
}
|
||||
if !foundKill {
|
||||
t.Error("Did not find kill keybind")
|
||||
}
|
||||
if !foundWorkspace {
|
||||
t.Error("Did not find workspace 1 keybind")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwayIsMod(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{"Mod4", true},
|
||||
{"Shift", true},
|
||||
{"Control", true},
|
||||
{"Alt", true},
|
||||
{"Super", true},
|
||||
{"$mod", true},
|
||||
{"Left", false},
|
||||
{"q", false},
|
||||
{"1", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := swayIsMod(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("swayIsMod(%q) = %v, want %v", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/sway"
|
||||
)
|
||||
|
||||
func TestSwayProviderName(t *testing.T) {
|
||||
@@ -76,12 +74,12 @@ func TestSwayCategorizeByCommand(t *testing.T) {
|
||||
func TestSwayFormatKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *sway.KeyBinding
|
||||
keybind *SwayKeyBinding
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "single_mod",
|
||||
keybind: &sway.KeyBinding{
|
||||
keybind: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "q",
|
||||
},
|
||||
@@ -89,7 +87,7 @@ func TestSwayFormatKey(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "multiple_mods",
|
||||
keybind: &sway.KeyBinding{
|
||||
keybind: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4", "Shift"},
|
||||
Key: "e",
|
||||
},
|
||||
@@ -97,7 +95,7 @@ func TestSwayFormatKey(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "no_mods",
|
||||
keybind: &sway.KeyBinding{
|
||||
keybind: &SwayKeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "Print",
|
||||
},
|
||||
@@ -119,13 +117,13 @@ func TestSwayFormatKey(t *testing.T) {
|
||||
func TestSwayConvertKeybind(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
keybind *sway.KeyBinding
|
||||
keybind *SwayKeyBinding
|
||||
wantKey string
|
||||
wantDesc string
|
||||
}{
|
||||
{
|
||||
name: "with_comment",
|
||||
keybind: &sway.KeyBinding{
|
||||
keybind: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "t",
|
||||
Command: "exec kitty",
|
||||
@@ -136,7 +134,7 @@ func TestSwayConvertKeybind(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "without_comment",
|
||||
keybind: &sway.KeyBinding{
|
||||
keybind: &SwayKeyBinding{
|
||||
Mods: []string{"Mod4"},
|
||||
Key: "r",
|
||||
Command: "reload",
|
||||
|
||||
Reference in New Issue
Block a user