diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds')
25 files changed, 0 insertions, 8874 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/discovery.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/discovery.go deleted file mode 100644 index 4990975..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/discovery.go +++ /dev/null @@ -1,107 +0,0 @@ -package keybinds - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -type DiscoveryConfig struct { - SearchPaths []string -} - -func DefaultDiscoveryConfig() *DiscoveryConfig { - var searchPaths []string - - configDir, err := os.UserConfigDir() - if err == nil && configDir != "" { - searchPaths = append(searchPaths, filepath.Join(configDir, "DankMaterialShell", "cheatsheets")) - } - - configDirs := os.Getenv("XDG_CONFIG_DIRS") - if configDirs != "" { - for dir := range strings.SplitSeq(configDirs, ":") { - if dir != "" { - searchPaths = append(searchPaths, filepath.Join(dir, "DankMaterialShell", "cheatsheets")) - } - } - } - - return &DiscoveryConfig{ - SearchPaths: searchPaths, - } -} - -func (d *DiscoveryConfig) FindJSONFiles() ([]string, error) { - var files []string - - for _, searchPath := range d.SearchPaths { - expandedPath, err := utils.ExpandPath(searchPath) - if err != nil { - continue - } - - if _, err := os.Stat(expandedPath); os.IsNotExist(err) { - continue - } - - entries, err := os.ReadDir(expandedPath) - if err != nil { - continue - } - - for _, entry := range entries { - if entry.IsDir() { - continue - } - - if !strings.HasSuffix(entry.Name(), ".json") { - continue - } - - fullPath := filepath.Join(expandedPath, entry.Name()) - files = append(files, fullPath) - } - } - - return files, nil -} - -type JSONProviderFactory func(filePath string) (Provider, error) - -var jsonProviderFactory JSONProviderFactory - -func SetJSONProviderFactory(factory JSONProviderFactory) { - jsonProviderFactory = factory -} - -func AutoDiscoverProviders(registry *Registry, config *DiscoveryConfig) error { - if config == nil { - config = DefaultDiscoveryConfig() - } - - if jsonProviderFactory == nil { - return nil - } - - files, err := config.FindJSONFiles() - if err != nil { - return fmt.Errorf("failed to discover JSON files: %w", err) - } - - for _, file := range files { - provider, err := jsonProviderFactory(file) - if err != nil { - continue - } - - if err := registry.Register(provider); err != nil { - continue - } - } - - return nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/discovery_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/discovery_test.go deleted file mode 100644 index a78724e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/discovery_test.go +++ /dev/null @@ -1,287 +0,0 @@ -package keybinds - -import ( - "os" - "path/filepath" - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -func TestDefaultDiscoveryConfig(t *testing.T) { - oldConfigHome := os.Getenv("XDG_CONFIG_HOME") - oldConfigDirs := os.Getenv("XDG_CONFIG_DIRS") - defer func() { - os.Setenv("XDG_CONFIG_HOME", oldConfigHome) - os.Setenv("XDG_CONFIG_DIRS", oldConfigDirs) - }() - - tests := []struct { - name string - configHome string - configDirs string - expectedCount int - checkFirstPath bool - firstPath string - }{ - { - name: "default with no XDG vars", - configHome: "", - configDirs: "", - expectedCount: 1, - checkFirstPath: true, - }, - { - name: "with XDG_CONFIG_HOME set", - configHome: "/custom/config", - configDirs: "", - expectedCount: 1, - checkFirstPath: true, - firstPath: "/custom/config/DankMaterialShell/cheatsheets", - }, - { - name: "with XDG_CONFIG_DIRS set", - configHome: "/home/user/.config", - configDirs: "/etc/xdg:/opt/config", - expectedCount: 3, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - os.Setenv("XDG_CONFIG_HOME", tt.configHome) - os.Setenv("XDG_CONFIG_DIRS", tt.configDirs) - - config := DefaultDiscoveryConfig() - - if config == nil { - t.Fatal("DefaultDiscoveryConfig returned nil") - } - - if len(config.SearchPaths) != tt.expectedCount { - t.Errorf("SearchPaths count = %d, want %d", len(config.SearchPaths), tt.expectedCount) - } - - if tt.checkFirstPath && len(config.SearchPaths) > 0 { - if tt.firstPath != "" && config.SearchPaths[0] != tt.firstPath { - t.Errorf("SearchPaths[0] = %q, want %q", config.SearchPaths[0], tt.firstPath) - } - } - }) - } -} - -func TestFindJSONFiles(t *testing.T) { - tmpDir := t.TempDir() - - file1 := filepath.Join(tmpDir, "tmux.json") - file2 := filepath.Join(tmpDir, "vim.json") - txtFile := filepath.Join(tmpDir, "readme.txt") - subdir := filepath.Join(tmpDir, "subdir") - - if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil { - t.Fatalf("Failed to create file1: %v", err) - } - if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil { - t.Fatalf("Failed to create file2: %v", err) - } - if err := os.WriteFile(txtFile, []byte("text"), 0o644); err != nil { - t.Fatalf("Failed to create txt file: %v", err) - } - if err := os.MkdirAll(subdir, 0o755); err != nil { - t.Fatalf("Failed to create subdir: %v", err) - } - - config := &DiscoveryConfig{ - SearchPaths: []string{tmpDir}, - } - - files, err := config.FindJSONFiles() - if err != nil { - t.Fatalf("FindJSONFiles failed: %v", err) - } - - if len(files) != 2 { - t.Errorf("expected 2 JSON files, got %d", len(files)) - } - - found := make(map[string]bool) - for _, f := range files { - found[filepath.Base(f)] = true - } - - if !found["tmux.json"] { - t.Error("tmux.json not found") - } - if !found["vim.json"] { - t.Error("vim.json not found") - } - if found["readme.txt"] { - t.Error("readme.txt should not be included") - } -} - -func TestFindJSONFilesNonexistentPath(t *testing.T) { - config := &DiscoveryConfig{ - SearchPaths: []string{"/nonexistent/path"}, - } - - files, err := config.FindJSONFiles() - if err != nil { - t.Fatalf("FindJSONFiles failed: %v", err) - } - - if len(files) != 0 { - t.Errorf("expected 0 files for nonexistent path, got %d", len(files)) - } -} - -func TestFindJSONFilesMultiplePaths(t *testing.T) { - tmpDir1 := t.TempDir() - tmpDir2 := t.TempDir() - - file1 := filepath.Join(tmpDir1, "app1.json") - file2 := filepath.Join(tmpDir2, "app2.json") - - if err := os.WriteFile(file1, []byte("{}"), 0o644); err != nil { - t.Fatalf("Failed to create file1: %v", err) - } - if err := os.WriteFile(file2, []byte("{}"), 0o644); err != nil { - t.Fatalf("Failed to create file2: %v", err) - } - - config := &DiscoveryConfig{ - SearchPaths: []string{tmpDir1, tmpDir2}, - } - - files, err := config.FindJSONFiles() - if err != nil { - t.Fatalf("FindJSONFiles failed: %v", err) - } - - if len(files) != 2 { - t.Errorf("expected 2 JSON files from multiple paths, got %d", len(files)) - } -} - -func TestAutoDiscoverProviders(t *testing.T) { - tmpDir := t.TempDir() - - jsonContent := `{ - "title": "Test App", - "provider": "testapp", - "binds": {} -}` - - file := filepath.Join(tmpDir, "testapp.json") - if err := os.WriteFile(file, []byte(jsonContent), 0o644); err != nil { - t.Fatalf("Failed to create test file: %v", err) - } - - config := &DiscoveryConfig{ - SearchPaths: []string{tmpDir}, - } - - registry := NewRegistry() - - factoryCalled := false - SetJSONProviderFactory(func(filePath string) (Provider, error) { - factoryCalled = true - return &mockProvider{name: "testapp"}, nil - }) - - err := AutoDiscoverProviders(registry, config) - if err != nil { - t.Fatalf("AutoDiscoverProviders failed: %v", err) - } - - if !factoryCalled { - t.Error("factory was not called") - } - - provider, err := registry.Get("testapp") - if err != nil { - t.Fatalf("provider not registered: %v", err) - } - - if provider.Name() != "testapp" { - t.Errorf("provider name = %q, want %q", provider.Name(), "testapp") - } -} - -func TestAutoDiscoverProvidersNilConfig(t *testing.T) { - registry := NewRegistry() - - SetJSONProviderFactory(func(filePath string) (Provider, error) { - return &mockProvider{name: "test"}, nil - }) - - err := AutoDiscoverProviders(registry, nil) - if err != nil { - t.Fatalf("AutoDiscoverProviders with nil config failed: %v", err) - } -} - -func TestAutoDiscoverProvidersNoFactory(t *testing.T) { - tmpDir := t.TempDir() - - file := filepath.Join(tmpDir, "test.json") - if err := os.WriteFile(file, []byte("{}"), 0o644); err != nil { - t.Fatalf("Failed to create test file: %v", err) - } - - config := &DiscoveryConfig{ - SearchPaths: []string{tmpDir}, - } - - registry := NewRegistry() - - SetJSONProviderFactory(nil) - - err := AutoDiscoverProviders(registry, config) - if err != nil { - t.Fatalf("AutoDiscoverProviders should not fail without factory: %v", err) - } - - providers := registry.List() - if len(providers) != 0 { - t.Errorf("expected 0 providers without factory, got %d", len(providers)) - } -} - -func TestExpandPathInDiscovery(t *testing.T) { - home, err := os.UserHomeDir() - if err != nil { - t.Skip("Cannot get home directory") - } - - tests := []struct { - name string - input string - expected string - }{ - { - name: "tilde expansion", - input: "~/test", - expected: filepath.Join(home, "test"), - }, - { - name: "absolute path", - input: "/tmp/test", - expected: "/tmp/test", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := utils.ExpandPath(tt.input) - if err != nil { - t.Fatalf("expandPath failed: %v", err) - } - - if result != tt.expected { - t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland.go deleted file mode 100644 index e43a9eb..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland.go +++ /dev/null @@ -1,497 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -type HyprlandProvider struct { - configPath string - dmsBindsIncluded bool - parsed bool -} - -func NewHyprlandProvider(configPath string) *HyprlandProvider { - if configPath == "" { - configPath = defaultHyprlandConfigDir() - } - return &HyprlandProvider{ - configPath: configPath, - } -} - -func defaultHyprlandConfigDir() string { - configDir, err := os.UserConfigDir() - if err != nil { - return "" - } - return filepath.Join(configDir, "hypr") -} - -func (h *HyprlandProvider) Name() string { - return "hyprland" -} - -func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { - result, err := ParseHyprlandKeysWithDMS(h.configPath) - if err != nil { - return nil, fmt.Errorf("failed to parse hyprland config: %w", err) - } - - h.dmsBindsIncluded = result.DMSBindsIncluded - h.parsed = true - - categorizedBinds := make(map[string][]keybinds.Keybind) - h.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs) - - sheet := &keybinds.CheatSheet{ - Title: "Hyprland Keybinds", - Provider: h.Name(), - Binds: categorizedBinds, - DMSBindsIncluded: result.DMSBindsIncluded, - } - - if result.DMSStatus != nil { - sheet.DMSStatus = &keybinds.DMSBindsStatus{ - Exists: result.DMSStatus.Exists, - Included: result.DMSStatus.Included, - IncludePosition: result.DMSStatus.IncludePosition, - TotalIncludes: result.DMSStatus.TotalIncludes, - BindsAfterDMS: result.DMSStatus.BindsAfterDMS, - Effective: result.DMSStatus.Effective, - OverriddenBy: result.DMSStatus.OverriddenBy, - StatusMessage: result.DMSStatus.StatusMessage, - } - } - - return sheet, nil -} - -func (h *HyprlandProvider) HasDMSBindsIncluded() bool { - if h.parsed { - return h.dmsBindsIncluded - } - - result, err := ParseHyprlandKeysWithDMS(h.configPath) - if err != nil { - return false - } - - h.dmsBindsIncluded = result.DMSBindsIncluded - h.parsed = true - return h.dmsBindsIncluded -} - -func (h *HyprlandProvider) convertSection(section *HyprlandSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*HyprlandKeyBinding) { - currentSubcat := subcategory - if section.Name != "" { - currentSubcat = section.Name - } - - for _, kb := range section.Keybinds { - category := h.categorizeByDispatcher(kb.Dispatcher) - bind := h.convertKeybind(&kb, currentSubcat, conflicts) - categorizedBinds[category] = append(categorizedBinds[category], bind) - } - - for _, child := range section.Children { - h.convertSection(&child, currentSubcat, categorizedBinds, conflicts) - } -} - -func (h *HyprlandProvider) categorizeByDispatcher(dispatcher string) string { - switch { - case strings.Contains(dispatcher, "workspace"): - return "Workspace" - case strings.Contains(dispatcher, "monitor"): - return "Monitor" - case strings.Contains(dispatcher, "window") || - strings.Contains(dispatcher, "focus") || - strings.Contains(dispatcher, "move") || - strings.Contains(dispatcher, "swap") || - strings.Contains(dispatcher, "resize") || - dispatcher == "killactive" || - dispatcher == "fullscreen" || - dispatcher == "togglefloating" || - dispatcher == "pin" || - dispatcher == "fakefullscreen" || - dispatcher == "splitratio" || - dispatcher == "resizeactive": - return "Window" - case dispatcher == "exec": - return "Execute" - case dispatcher == "exit" || strings.Contains(dispatcher, "dpms"): - return "System" - default: - return "Other" - } -} - -func (h *HyprlandProvider) convertKeybind(kb *HyprlandKeyBinding, subcategory string, conflicts map[string]*HyprlandKeyBinding) keybinds.Keybind { - keyStr := h.formatKey(kb) - rawAction := h.formatRawAction(kb.Dispatcher, kb.Params) - desc := kb.Comment - - if desc == "" { - desc = rawAction - } - - source := "config" - if strings.Contains(kb.Source, "dms/binds.conf") { - source = "dms" - } - - bind := keybinds.Keybind{ - Key: keyStr, - Description: desc, - Action: rawAction, - Subcategory: subcategory, - Source: source, - Flags: kb.Flags, - } - - if source == "dms" && conflicts != nil { - normalizedKey := strings.ToLower(keyStr) - if conflictKb, ok := conflicts[normalizedKey]; ok { - bind.Conflict = &keybinds.Keybind{ - Key: keyStr, - Description: conflictKb.Comment, - Action: h.formatRawAction(conflictKb.Dispatcher, conflictKb.Params), - Source: "config", - } - } - } - - return bind -} - -func (h *HyprlandProvider) formatRawAction(dispatcher, params string) string { - if params != "" { - return dispatcher + " " + params - } - return dispatcher -} - -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) - return strings.Join(parts, "+") -} - -func (h *HyprlandProvider) GetOverridePath() string { - expanded, err := utils.ExpandPath(h.configPath) - if err != nil { - return filepath.Join(h.configPath, "dms", "binds.conf") - } - return filepath.Join(expanded, "dms", "binds.conf") -} - -func (h *HyprlandProvider) validateAction(action string) error { - action = strings.TrimSpace(action) - switch { - case action == "": - return fmt.Errorf("action cannot be empty") - case action == "exec" || action == "exec ": - return fmt.Errorf("exec dispatcher requires arguments") - case strings.HasPrefix(action, "exec "): - rest := strings.TrimSpace(strings.TrimPrefix(action, "exec ")) - if rest == "" { - return fmt.Errorf("exec dispatcher requires arguments") - } - } - return nil -} - -func (h *HyprlandProvider) SetBind(key, action, description string, options map[string]any) error { - if err := h.validateAction(action); err != nil { - return err - } - - overridePath := h.GetOverridePath() - - if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil { - return fmt.Errorf("failed to create dms directory: %w", err) - } - - existingBinds, err := h.loadOverrideBinds() - if err != nil { - existingBinds = make(map[string]*hyprlandOverrideBind) - } - - // Extract flags from options - var flags string - if options != nil { - if f, ok := options["flags"].(string); ok { - flags = f - } - } - - normalizedKey := strings.ToLower(key) - existingBinds[normalizedKey] = &hyprlandOverrideBind{ - Key: key, - Action: action, - Description: description, - Flags: flags, - Options: options, - } - - return h.writeOverrideBinds(existingBinds) -} - -func (h *HyprlandProvider) RemoveBind(key string) error { - existingBinds, err := h.loadOverrideBinds() - if err != nil { - return nil - } - - normalizedKey := strings.ToLower(key) - delete(existingBinds, normalizedKey) - return h.writeOverrideBinds(existingBinds) -} - -type hyprlandOverrideBind struct { - Key string - Action string - Description string - Flags string // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press - Options map[string]any -} - -func (h *HyprlandProvider) loadOverrideBinds() (map[string]*hyprlandOverrideBind, error) { - overridePath := h.GetOverridePath() - binds := make(map[string]*hyprlandOverrideBind) - - data, err := os.ReadFile(overridePath) - if os.IsNotExist(err) { - return binds, nil - } - if err != nil { - return nil, err - } - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - if !strings.HasPrefix(line, "bind") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - continue - } - - // Extract flags from bind type - bindType := strings.TrimSpace(parts[0]) - flags := extractBindFlags(bindType) - hasDescFlag := strings.Contains(flags, "d") - - content := strings.TrimSpace(parts[1]) - commentParts := strings.SplitN(content, "#", 2) - bindContent := strings.TrimSpace(commentParts[0]) - - var comment string - if len(commentParts) > 1 { - comment = strings.TrimSpace(commentParts[1]) - } - - // For bindd, format is: mods, key, description, dispatcher, params - var minFields, descIndex, dispatcherIndex int - if hasDescFlag { - minFields = 4 - descIndex = 2 - dispatcherIndex = 3 - } else { - minFields = 3 - dispatcherIndex = 2 - } - - fields := strings.SplitN(bindContent, ",", minFields+2) - if len(fields) < minFields { - continue - } - - mods := strings.TrimSpace(fields[0]) - keyName := strings.TrimSpace(fields[1]) - - var dispatcher, params string - if hasDescFlag { - if comment == "" { - comment = strings.TrimSpace(fields[descIndex]) - } - dispatcher = strings.TrimSpace(fields[dispatcherIndex]) - if len(fields) > dispatcherIndex+1 { - paramParts := fields[dispatcherIndex+1:] - params = strings.TrimSpace(strings.Join(paramParts, ",")) - } - } else { - dispatcher = strings.TrimSpace(fields[dispatcherIndex]) - if len(fields) > dispatcherIndex+1 { - paramParts := fields[dispatcherIndex+1:] - params = strings.TrimSpace(strings.Join(paramParts, ",")) - } - } - - keyStr := h.buildKeyString(mods, keyName) - normalizedKey := strings.ToLower(keyStr) - action := dispatcher - if params != "" { - action = dispatcher + " " + params - } - - binds[normalizedKey] = &hyprlandOverrideBind{ - Key: keyStr, - Action: action, - Description: comment, - Flags: flags, - } - } - - return binds, nil -} - -func (h *HyprlandProvider) buildKeyString(mods, key string) string { - if mods == "" { - return key - } - - modList := strings.FieldsFunc(mods, func(r rune) bool { - return r == '+' || r == ' ' - }) - - parts := append(modList, key) - return strings.Join(parts, "+") -} - -func (h *HyprlandProvider) getBindSortPriority(action string) int { - switch { - case strings.HasPrefix(action, "exec") && strings.Contains(action, "dms"): - return 0 - case strings.Contains(action, "workspace"): - return 1 - case strings.Contains(action, "window") || strings.Contains(action, "focus") || - strings.Contains(action, "move") || strings.Contains(action, "swap") || - strings.Contains(action, "resize"): - return 2 - case strings.Contains(action, "monitor"): - return 3 - case strings.HasPrefix(action, "exec"): - return 4 - case action == "exit" || strings.Contains(action, "dpms"): - return 5 - default: - return 6 - } -} - -func (h *HyprlandProvider) writeOverrideBinds(binds map[string]*hyprlandOverrideBind) error { - overridePath := h.GetOverridePath() - content := h.generateBindsContent(binds) - return os.WriteFile(overridePath, []byte(content), 0o644) -} - -func (h *HyprlandProvider) generateBindsContent(binds map[string]*hyprlandOverrideBind) string { - if len(binds) == 0 { - return "" - } - - bindList := make([]*hyprlandOverrideBind, 0, len(binds)) - for _, bind := range binds { - bindList = append(bindList, bind) - } - - sort.Slice(bindList, func(i, j int) bool { - pi, pj := h.getBindSortPriority(bindList[i].Action), h.getBindSortPriority(bindList[j].Action) - if pi != pj { - return pi < pj - } - return bindList[i].Key < bindList[j].Key - }) - - var sb strings.Builder - for _, bind := range bindList { - h.writeBindLine(&sb, bind) - } - - return sb.String() -} - -func (h *HyprlandProvider) writeBindLine(sb *strings.Builder, bind *hyprlandOverrideBind) { - mods, key := h.parseKeyString(bind.Key) - dispatcher, params := h.parseAction(bind.Action) - - // Write bind type with flags (e.g., "bind", "binde", "bindel") - sb.WriteString("bind") - if bind.Flags != "" { - sb.WriteString(bind.Flags) - } - sb.WriteString(" = ") - sb.WriteString(mods) - sb.WriteString(", ") - sb.WriteString(key) - sb.WriteString(", ") - - // For bindd (description flag), include description before dispatcher - if strings.Contains(bind.Flags, "d") && bind.Description != "" { - sb.WriteString(bind.Description) - sb.WriteString(", ") - } - - sb.WriteString(dispatcher) - - if params != "" { - sb.WriteString(", ") - sb.WriteString(params) - } - - // Only add comment if not using bindd (which has inline description) - if bind.Description != "" && !strings.Contains(bind.Flags, "d") { - sb.WriteString(" # ") - sb.WriteString(bind.Description) - } - - sb.WriteString("\n") -} - -func (h *HyprlandProvider) parseKeyString(keyStr string) (mods, key string) { - parts := strings.Split(keyStr, "+") - switch len(parts) { - case 0: - return "", keyStr - case 1: - return "", parts[0] - default: - return strings.Join(parts[:len(parts)-1], " "), parts[len(parts)-1] - } -} - -func (h *HyprlandProvider) parseAction(action string) (dispatcher, params string) { - parts := strings.SplitN(action, " ", 2) - switch len(parts) { - case 0: - return action, "" - case 1: - dispatcher = parts[0] - default: - dispatcher = parts[0] - params = parts[1] - } - - // Convert internal spawn format to Hyprland's exec - if dispatcher == "spawn" { - dispatcher = "exec" - } - - return dispatcher, params -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_parser.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_parser.go deleted file mode 100644 index 04b5ada..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_parser.go +++ /dev/null @@ -1,627 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -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"` - Source string `json:"source"` - Flags string `json:"flags"` // Bind flags: l=locked, r=release, e=repeat, n=non-consuming, m=mouse, t=transparent, i=ignore-mods, s=separate, d=description, o=long-press -} - -type HyprlandSection struct { - Children []HyprlandSection `json:"children"` - Keybinds []HyprlandKeyBinding `json:"keybinds"` - Name string `json:"name"` -} - -type HyprlandParser struct { - contentLines []string - readingLine int - configDir string - currentSource string - dmsBindsExists bool - dmsBindsIncluded bool - includeCount int - dmsIncludePos int - bindsAfterDMS int - dmsBindKeys map[string]bool - configBindKeys map[string]bool - conflictingConfigs map[string]*HyprlandKeyBinding - bindMap map[string]*HyprlandKeyBinding - bindOrder []string - processedFiles map[string]bool - dmsProcessed bool -} - -func NewHyprlandParser(configDir string) *HyprlandParser { - return &HyprlandParser{ - contentLines: []string{}, - readingLine: 0, - configDir: configDir, - dmsIncludePos: -1, - dmsBindKeys: make(map[string]bool), - configBindKeys: make(map[string]bool), - conflictingConfigs: make(map[string]*HyprlandKeyBinding), - bindMap: make(map[string]*HyprlandKeyBinding), - bindOrder: []string{}, - processedFiles: make(map[string]bool), - } -} - -func (p *HyprlandParser) ReadContent(directory string) error { - expandedDir, err := utils.ExpandPath(directory) - if err != nil { - return err - } - - 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] - return p.parseBindLine(line) -} - -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(path) - if err := parser.ReadContent(path); err != nil { - return nil, err - } - return parser.ParseKeys(), nil -} - -type HyprlandParseResult struct { - Section *HyprlandSection - DMSBindsIncluded bool - DMSStatus *HyprlandDMSStatus - ConflictingConfigs map[string]*HyprlandKeyBinding -} - -type HyprlandDMSStatus struct { - Exists bool - Included bool - IncludePosition int - TotalIncludes int - BindsAfterDMS int - Effective bool - OverriddenBy int - StatusMessage string -} - -func (p *HyprlandParser) buildDMSStatus() *HyprlandDMSStatus { - status := &HyprlandDMSStatus{ - Exists: p.dmsBindsExists, - Included: p.dmsBindsIncluded, - IncludePosition: p.dmsIncludePos, - TotalIncludes: p.includeCount, - BindsAfterDMS: p.bindsAfterDMS, - } - - switch { - case !p.dmsBindsExists: - status.Effective = false - status.StatusMessage = "dms/binds.conf does not exist" - case !p.dmsBindsIncluded: - status.Effective = false - status.StatusMessage = "dms/binds.conf is not sourced in config" - case p.bindsAfterDMS > 0: - status.Effective = true - status.OverriddenBy = p.bindsAfterDMS - status.StatusMessage = "Some DMS binds may be overridden by config binds" - default: - status.Effective = true - status.StatusMessage = "DMS binds are active" - } - - return status -} - -func (p *HyprlandParser) formatBindKey(kb *HyprlandKeyBinding) string { - parts := make([]string, 0, len(kb.Mods)+1) - parts = append(parts, kb.Mods...) - parts = append(parts, kb.Key) - return strings.Join(parts, "+") -} - -func (p *HyprlandParser) normalizeKey(key string) string { - return strings.ToLower(key) -} - -func (p *HyprlandParser) addBind(kb *HyprlandKeyBinding) bool { - key := p.formatBindKey(kb) - normalizedKey := p.normalizeKey(key) - isDMSBind := strings.Contains(kb.Source, "dms/binds.conf") - - if isDMSBind { - p.dmsBindKeys[normalizedKey] = true - } else if p.dmsBindKeys[normalizedKey] { - p.bindsAfterDMS++ - p.conflictingConfigs[normalizedKey] = kb - p.configBindKeys[normalizedKey] = true - return false - } else { - p.configBindKeys[normalizedKey] = true - } - - if _, exists := p.bindMap[normalizedKey]; !exists { - p.bindOrder = append(p.bindOrder, key) - } - p.bindMap[normalizedKey] = kb - return true -} - -func (p *HyprlandParser) ParseWithDMS() (*HyprlandSection, error) { - expandedDir, err := utils.ExpandPath(p.configDir) - if err != nil { - return nil, err - } - - dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf") - if _, err := os.Stat(dmsBindsPath); err == nil { - p.dmsBindsExists = true - } - - mainConfig := filepath.Join(expandedDir, "hyprland.conf") - section, err := p.parseFileWithSource(mainConfig, "") - if err != nil { - return nil, err - } - - if p.dmsBindsExists && !p.dmsProcessed { - p.parseDMSBindsDirectly(dmsBindsPath, section) - } - - return section, nil -} - -func (p *HyprlandParser) parseFileWithSource(filePath, sectionName string) (*HyprlandSection, error) { - absPath, err := filepath.Abs(filePath) - if err != nil { - return nil, err - } - - if p.processedFiles[absPath] { - return &HyprlandSection{Name: sectionName}, nil - } - p.processedFiles[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return nil, err - } - - prevSource := p.currentSource - p.currentSource = absPath - - section := &HyprlandSection{Name: sectionName} - lines := strings.Split(string(data), "\n") - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - - if strings.HasPrefix(trimmed, "source") { - p.handleSource(trimmed, section, filepath.Dir(absPath)) - continue - } - - if !strings.HasPrefix(trimmed, "bind") { - continue - } - - kb := p.parseBindLine(line) - if kb == nil { - continue - } - kb.Source = p.currentSource - if p.addBind(kb) { - section.Keybinds = append(section.Keybinds, *kb) - } - } - - p.currentSource = prevSource - return section, nil -} - -func (p *HyprlandParser) handleSource(line string, section *HyprlandSection, baseDir string) { - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - return - } - - sourcePath := strings.TrimSpace(parts[1]) - isDMSSource := sourcePath == "dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf") - - p.includeCount++ - if isDMSSource { - p.dmsBindsIncluded = true - p.dmsIncludePos = p.includeCount - p.dmsProcessed = true - } - - fullPath := sourcePath - if !filepath.IsAbs(sourcePath) { - fullPath = filepath.Join(baseDir, sourcePath) - } - - expanded, err := utils.ExpandPath(fullPath) - if err != nil { - return - } - - includedSection, err := p.parseFileWithSource(expanded, "") - if err != nil { - return - } - - section.Children = append(section.Children, *includedSection) -} - -func (p *HyprlandParser) parseDMSBindsDirectly(dmsBindsPath string, section *HyprlandSection) { - data, err := os.ReadFile(dmsBindsPath) - if err != nil { - return - } - - prevSource := p.currentSource - p.currentSource = dmsBindsPath - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if !strings.HasPrefix(trimmed, "bind") { - continue - } - - kb := p.parseBindLine(line) - if kb == nil { - continue - } - kb.Source = dmsBindsPath - if p.addBind(kb) { - section.Keybinds = append(section.Keybinds, *kb) - } - } - - p.currentSource = prevSource - p.dmsProcessed = true -} - -func (p *HyprlandParser) parseBindLine(line string) *HyprlandKeyBinding { - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - return nil - } - - // Extract bind type and flags from the left side of "=" - bindType := strings.TrimSpace(parts[0]) - flags := extractBindFlags(bindType) - hasDescFlag := strings.Contains(flags, "d") - - keys := parts[1] - keyParts := strings.SplitN(keys, "#", 2) - keys = keyParts[0] - - var comment string - if len(keyParts) > 1 { - comment = strings.TrimSpace(keyParts[1]) - } - - // For bindd, the format is: bindd = MODS, key, description, dispatcher, params - // For regular binds: bind = MODS, key, dispatcher, params - var minFields, descIndex, dispatcherIndex int - if hasDescFlag { - minFields = 4 // mods, key, description, dispatcher - descIndex = 2 - dispatcherIndex = 3 - } else { - minFields = 3 // mods, key, dispatcher - dispatcherIndex = 2 - } - - keyFields := strings.SplitN(keys, ",", minFields+2) // Allow for params - if len(keyFields) < minFields { - return nil - } - - mods := strings.TrimSpace(keyFields[0]) - key := strings.TrimSpace(keyFields[1]) - - var dispatcher, params string - if hasDescFlag { - // bindd format: description is in the bind itself - if comment == "" { - comment = strings.TrimSpace(keyFields[descIndex]) - } - dispatcher = strings.TrimSpace(keyFields[dispatcherIndex]) - if len(keyFields) > dispatcherIndex+1 { - paramParts := keyFields[dispatcherIndex+1:] - params = strings.TrimSpace(strings.Join(paramParts, ",")) - } - } else { - dispatcher = strings.TrimSpace(keyFields[dispatcherIndex]) - if len(keyFields) > dispatcherIndex+1 { - paramParts := keyFields[dispatcherIndex+1:] - params = strings.TrimSpace(strings.Join(paramParts, ",")) - } - } - - if comment != "" && strings.HasPrefix(comment, HideComment) { - return nil - } - - if comment == "" { - comment = hyprlandAutogenerateComment(dispatcher, params) - } - - var modList []string - if mods != "" { - modstring := mods + string(ModSeparators[0]) - idx := 0 - for index, char := range modstring { - isModSep := false - for _, sep := range ModSeparators { - if char == sep { - isModSep = true - break - } - } - if isModSep { - if index-idx > 1 { - modList = append(modList, modstring[idx:index]) - } - idx = index + 1 - } - } - } - - return &HyprlandKeyBinding{ - Mods: modList, - Key: key, - Dispatcher: dispatcher, - Params: params, - Comment: comment, - Flags: flags, - } -} - -// extractBindFlags extracts the flags from a bind type string -// e.g., "binde" -> "e", "bindel" -> "el", "bindd" -> "d" -func extractBindFlags(bindType string) string { - bindType = strings.TrimSpace(bindType) - if !strings.HasPrefix(bindType, "bind") { - return "" - } - return bindType[4:] // Everything after "bind" -} - -func ParseHyprlandKeysWithDMS(path string) (*HyprlandParseResult, error) { - parser := NewHyprlandParser(path) - section, err := parser.ParseWithDMS() - if err != nil { - return nil, err - } - - return &HyprlandParseResult{ - Section: section, - DMSBindsIncluded: parser.dmsBindsIncluded, - DMSStatus: parser.buildDMSStatus(), - ConflictingConfigs: parser.conflictingConfigs, - }, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_parser_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_parser_test.go deleted file mode 100644 index c2872e5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_parser_test.go +++ /dev/null @@ -1,519 +0,0 @@ -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), 0o644); 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), 0o644); 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), 0o644); err != nil { - t.Fatalf("Failed to write file1: %v", err) - } - if err := os.WriteFile(file2, []byte(content2), 0o644); 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, 0o755); 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"), 0o644); 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), 0o644); 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)) - } -} - -func TestExtractBindFlags(t *testing.T) { - tests := []struct { - bindType string - expected string - }{ - {"bind", ""}, - {"binde", "e"}, - {"bindl", "l"}, - {"bindr", "r"}, - {"bindd", "d"}, - {"bindo", "o"}, - {"bindel", "el"}, - {"bindler", "ler"}, - {"bindem", "em"}, - {" bind ", ""}, - {" binde ", "e"}, - {"notbind", ""}, - {"", ""}, - } - - for _, tt := range tests { - t.Run(tt.bindType, func(t *testing.T) { - result := extractBindFlags(tt.bindType) - if result != tt.expected { - t.Errorf("extractBindFlags(%q) = %q, want %q", tt.bindType, result, tt.expected) - } - }) - } -} - -func TestHyprlandBindFlags(t *testing.T) { - tests := []struct { - name string - line string - expectedFlags string - expectedKey string - expectedDisp string - expectedDesc string - }{ - { - name: "regular bind", - line: "bind = SUPER, Q, killactive", - expectedFlags: "", - expectedKey: "Q", - expectedDisp: "killactive", - expectedDesc: "Close window", - }, - { - name: "binde (repeat on hold)", - line: "binde = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+", - expectedFlags: "e", - expectedKey: "XF86AudioRaiseVolume", - expectedDisp: "exec", - expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+", - }, - { - name: "bindl (locked/inhibitor bypass)", - line: "bindl = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-", - expectedFlags: "l", - expectedKey: "XF86AudioLowerVolume", - expectedDisp: "exec", - expectedDesc: "wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-", - }, - { - name: "bindr (release trigger)", - line: "bindr = SUPER, SUPER_L, exec, pkill wofi || wofi", - expectedFlags: "r", - expectedKey: "SUPER_L", - expectedDisp: "exec", - expectedDesc: "pkill wofi || wofi", - }, - { - name: "bindd (description)", - line: "bindd = SUPER, Q, Open my favourite terminal, exec, kitty", - expectedFlags: "d", - expectedKey: "Q", - expectedDisp: "exec", - expectedDesc: "Open my favourite terminal", - }, - { - name: "bindo (long press)", - line: "bindo = SUPER, XF86AudioNext, exec, playerctl next", - expectedFlags: "o", - expectedKey: "XF86AudioNext", - expectedDisp: "exec", - expectedDesc: "playerctl next", - }, - { - name: "bindel (combined flags)", - line: "bindel = , XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+", - expectedFlags: "el", - expectedKey: "XF86AudioRaiseVolume", - expectedDisp: "exec", - expectedDesc: "wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - parser := NewHyprlandParser("") - parser.contentLines = []string{tt.line} - result := parser.getKeybindAtLine(0) - - if result == nil { - t.Fatal("Expected keybind, got nil") - } - - if result.Flags != tt.expectedFlags { - t.Errorf("Flags = %q, want %q", result.Flags, tt.expectedFlags) - } - if result.Key != tt.expectedKey { - t.Errorf("Key = %q, want %q", result.Key, tt.expectedKey) - } - if result.Dispatcher != tt.expectedDisp { - t.Errorf("Dispatcher = %q, want %q", result.Dispatcher, tt.expectedDisp) - } - if result.Comment != tt.expectedDesc { - t.Errorf("Comment = %q, want %q", result.Comment, tt.expectedDesc) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_test.go deleted file mode 100644 index 90dfe97..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/hyprland_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestNewHyprlandProvider(t *testing.T) { - t.Run("custom path", func(t *testing.T) { - p := NewHyprlandProvider("/custom/path") - if p == nil { - t.Fatal("NewHyprlandProvider returned nil") - } - if p.configPath != "/custom/path" { - t.Errorf("configPath = %q, want %q", p.configPath, "/custom/path") - } - }) - - t.Run("empty path defaults", func(t *testing.T) { - p := NewHyprlandProvider("") - if p == nil { - t.Fatal("NewHyprlandProvider returned nil") - } - configDir, err := os.UserConfigDir() - if err != nil { - t.Fatalf("UserConfigDir failed: %v", err) - } - expected := filepath.Join(configDir, "hypr") - if p.configPath != expected { - t.Errorf("configPath = %q, want %q", p.configPath, expected) - } - }) -} - -func TestHyprlandProviderName(t *testing.T) { - p := NewHyprlandProvider("") - if p.Name() != "hyprland" { - t.Errorf("Name() = %q, want %q", p.Name(), "hyprland") - } -} - -func TestHyprlandProviderGetCheatSheet(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 -bind = SUPER, 1, workspace, 1 -` - - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - p := NewHyprlandProvider(tmpDir) - sheet, err := p.GetCheatSheet() - - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - if sheet.Title != "Hyprland Keybinds" { - t.Errorf("Title = %q, want %q", sheet.Title, "Hyprland Keybinds") - } - - if sheet.Provider != "hyprland" { - t.Errorf("Provider = %q, want %q", sheet.Provider, "hyprland") - } - - if len(sheet.Binds) == 0 { - t.Error("expected categorized bindings, got none") - } - - if windowBinds, ok := sheet.Binds["Window"]; !ok || len(windowBinds) == 0 { - t.Error("expected Window category with bindings") - } - - if execBinds, ok := sheet.Binds["Execute"]; !ok || len(execBinds) == 0 { - t.Error("expected Execute category with bindings") - } - - if wsBinds, ok := sheet.Binds["Workspace"]; !ok || len(wsBinds) == 0 { - t.Error("expected Workspace category with bindings") - } -} - -func TestHyprlandProviderGetCheatSheetError(t *testing.T) { - p := NewHyprlandProvider("/nonexistent/path") - _, err := p.GetCheatSheet() - - if err == nil { - t.Error("expected error for nonexistent path, got nil") - } -} - -func TestFormatKey(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "hyprland.conf") - - tests := []struct { - name string - content string - expected string - category string - }{ - { - name: "single mod", - content: "bind = SUPER, Q, killactive", - expected: "SUPER+Q", - category: "Window", - }, - { - name: "multiple mods", - content: "bind = SUPER+SHIFT, F, fullscreen, 0", - expected: "SUPER+SHIFT+F", - category: "Window", - }, - { - name: "no mods", - content: "bind = , Print, exec, screenshot", - expected: "Print", - category: "Execute", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - p := NewHyprlandProvider(tmpDir) - sheet, err := p.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - categoryBinds, ok := sheet.Binds[tt.category] - if !ok || len(categoryBinds) == 0 { - t.Fatalf("expected binds in category %q", tt.category) - } - - if categoryBinds[0].Key != tt.expected { - t.Errorf("Key = %q, want %q", categoryBinds[0].Key, tt.expected) - } - }) - } -} - -func TestDescriptionFallback(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "hyprland.conf") - - tests := []struct { - name string - content string - wantDesc string - }{ - { - name: "autogenerated description for known dispatcher", - content: "bind = SUPER, Q, killactive", - wantDesc: "Close window", - }, - { - name: "custom comment overrides autogeneration", - content: "bind = SUPER, T, exec, kitty # Open terminal", - wantDesc: "Open terminal", - }, - { - name: "fallback for unknown dispatcher without params", - content: "bind = SUPER, W, unknowndispatcher", - wantDesc: "unknowndispatcher", - }, - { - name: "fallback for unknown dispatcher with params", - content: "bind = SUPER, X, customdispatcher, arg1", - wantDesc: "customdispatcher arg1", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err := os.WriteFile(configFile, []byte(tt.content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - p := NewHyprlandProvider(tmpDir) - sheet, err := p.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - found := false - for _, binds := range sheet.Binds { - for _, bind := range binds { - if bind.Description == tt.wantDesc { - found = true - break - } - } - if found { - break - } - } - - if !found { - t.Errorf("expected description %q not found in any bind", tt.wantDesc) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/jsonfile.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/jsonfile.go deleted file mode 100644 index 08cf449..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/jsonfile.go +++ /dev/null @@ -1,119 +0,0 @@ -package providers - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -type JSONFileProvider struct { - filePath string - name string -} - -func NewJSONFileProvider(filePath string) (*JSONFileProvider, error) { - if filePath == "" { - return nil, fmt.Errorf("file path cannot be empty") - } - - expandedPath, err := utils.ExpandPath(filePath) - if err != nil { - return nil, fmt.Errorf("failed to expand path: %w", err) - } - - name := filepath.Base(expandedPath) - name = name[:len(name)-len(filepath.Ext(name))] - - return &JSONFileProvider{ - filePath: expandedPath, - name: name, - }, nil -} - -func (j *JSONFileProvider) Name() string { - return j.name -} - -func (j *JSONFileProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { - data, err := os.ReadFile(j.filePath) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - - var rawData map[string]any - if err := json.Unmarshal(data, &rawData); err != nil { - return nil, fmt.Errorf("failed to parse JSON: %w", err) - } - - title, _ := rawData["title"].(string) - provider, _ := rawData["provider"].(string) - if provider == "" { - provider = j.name - } - - categorizedBinds := make(map[string][]keybinds.Keybind) - - bindsRaw, ok := rawData["binds"] - if !ok { - return nil, fmt.Errorf("missing 'binds' field") - } - - switch binds := bindsRaw.(type) { - case map[string]any: - for category, categoryBindsRaw := range binds { - categoryBindsList, ok := categoryBindsRaw.([]any) - if !ok { - continue - } - - var keybindsList []keybinds.Keybind - categoryBindsJSON, _ := json.Marshal(categoryBindsList) - if err := json.Unmarshal(categoryBindsJSON, &keybindsList); err != nil { - continue - } - - categorizedBinds[category] = keybindsList - } - - case []any: - flatBindsJSON, _ := json.Marshal(binds) - var flatBinds []struct { - Key string `json:"key"` - Description string `json:"desc"` - Action string `json:"action,omitempty"` - Category string `json:"cat,omitempty"` - Subcategory string `json:"subcat,omitempty"` - } - if err := json.Unmarshal(flatBindsJSON, &flatBinds); err != nil { - return nil, fmt.Errorf("failed to parse flat binds array: %w", err) - } - - for _, bind := range flatBinds { - category := bind.Category - if category == "" { - category = "Other" - } - - kb := keybinds.Keybind{ - Key: bind.Key, - Description: bind.Description, - Action: bind.Action, - Subcategory: bind.Subcategory, - } - categorizedBinds[category] = append(categorizedBinds[category], kb) - } - - default: - return nil, fmt.Errorf("'binds' must be either an object (categorized) or array (flat)") - } - - return &keybinds.CheatSheet{ - Title: title, - Provider: provider, - Binds: categorizedBinds, - }, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/jsonfile_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/jsonfile_test.go deleted file mode 100644 index ded4385..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/jsonfile_test.go +++ /dev/null @@ -1,281 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -func TestNewJSONFileProvider(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "test.json") - - if err := os.WriteFile(testFile, []byte("{}"), 0o644); err != nil { - t.Fatalf("Failed to create test file: %v", err) - } - - tests := []struct { - name string - filePath string - expectError bool - wantName string - }{ - { - name: "valid file", - filePath: testFile, - expectError: false, - wantName: "test", - }, - { - name: "empty path", - filePath: "", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p, err := NewJSONFileProvider(tt.filePath) - - if tt.expectError { - if err == nil { - t.Error("expected error, got nil") - } - return - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - return - } - - if p.Name() != tt.wantName { - t.Errorf("Name() = %q, want %q", p.Name(), tt.wantName) - } - }) - } -} - -func TestJSONFileProviderGetCheatSheet(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "tmux.json") - - content := `{ - "title": "Tmux Binds", - "provider": "tmux", - "binds": { - "Pane": [ - { - "key": "Ctrl+Alt+J", - "desc": "Resize split downward", - "subcat": "Sizing" - }, - { - "key": "Ctrl+K", - "desc": "Move Focus Up", - "subcat": "Navigation" - } - ] - } -}` - - if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - p, err := NewJSONFileProvider(testFile) - if err != nil { - t.Fatalf("NewJSONFileProvider failed: %v", err) - } - - sheet, err := p.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - if sheet.Title != "Tmux Binds" { - t.Errorf("Title = %q, want %q", sheet.Title, "Tmux Binds") - } - - if sheet.Provider != "tmux" { - t.Errorf("Provider = %q, want %q", sheet.Provider, "tmux") - } - - paneBinds, ok := sheet.Binds["Pane"] - if !ok { - t.Fatal("expected Pane category") - } - - if len(paneBinds) != 2 { - t.Errorf("len(Pane binds) = %d, want 2", len(paneBinds)) - } - - if len(paneBinds) > 0 { - bind := paneBinds[0] - if bind.Key != "Ctrl+Alt+J" { - t.Errorf("Pane[0].Key = %q, want %q", bind.Key, "Ctrl+Alt+J") - } - if bind.Description != "Resize split downward" { - t.Errorf("Pane[0].Description = %q, want %q", bind.Description, "Resize split downward") - } - if bind.Subcategory != "Sizing" { - t.Errorf("Pane[0].Subcategory = %q, want %q", bind.Subcategory, "Sizing") - } - } -} - -func TestJSONFileProviderGetCheatSheetNoProvider(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "custom.json") - - content := `{ - "title": "Custom Binds", - "binds": {} -}` - - if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - p, err := NewJSONFileProvider(testFile) - if err != nil { - t.Fatalf("NewJSONFileProvider failed: %v", err) - } - - sheet, err := p.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - if sheet.Provider != "custom" { - t.Errorf("Provider = %q, want %q (should default to filename)", sheet.Provider, "custom") - } -} - -func TestJSONFileProviderFlatArrayBackwardsCompat(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "legacy.json") - - content := `{ - "title": "Legacy Format", - "provider": "legacy", - "binds": [ - { - "key": "Ctrl+S", - "desc": "Save file", - "cat": "File", - "subcat": "Operations" - }, - { - "key": "Ctrl+O", - "desc": "Open file", - "cat": "File" - }, - { - "key": "Ctrl+Q", - "desc": "Quit", - "subcat": "Exit" - } - ] -}` - - if err := os.WriteFile(testFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - p, err := NewJSONFileProvider(testFile) - if err != nil { - t.Fatalf("NewJSONFileProvider failed: %v", err) - } - - sheet, err := p.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - fileBinds, ok := sheet.Binds["File"] - if !ok || len(fileBinds) != 2 { - t.Errorf("expected 2 binds in File category, got %d", len(fileBinds)) - } - - otherBinds, ok := sheet.Binds["Other"] - if !ok || len(otherBinds) != 1 { - t.Errorf("expected 1 bind in Other category (no cat specified), got %d", len(otherBinds)) - } - - if len(fileBinds) > 0 { - if fileBinds[0].Subcategory != "Operations" { - t.Errorf("expected subcategory %q, got %q", "Operations", fileBinds[0].Subcategory) - } - } -} - -func TestJSONFileProviderInvalidJSON(t *testing.T) { - tmpDir := t.TempDir() - testFile := filepath.Join(tmpDir, "invalid.json") - - if err := os.WriteFile(testFile, []byte("not valid json"), 0o644); err != nil { - t.Fatalf("Failed to write test file: %v", err) - } - - p, err := NewJSONFileProvider(testFile) - if err != nil { - t.Fatalf("NewJSONFileProvider failed: %v", err) - } - - _, err = p.GetCheatSheet() - if err == nil { - t.Error("expected error for invalid JSON, got nil") - } -} - -func TestJSONFileProviderNonexistentFile(t *testing.T) { - p, err := NewJSONFileProvider("/nonexistent/file.json") - if err != nil { - t.Fatalf("NewJSONFileProvider failed: %v", err) - } - - _, err = p.GetCheatSheet() - if err == nil { - t.Error("expected error for nonexistent file, got nil") - } -} - -func TestExpandPath(t *testing.T) { - home, err := os.UserHomeDir() - if err != nil { - t.Skip("Cannot get home directory") - } - - tests := []struct { - name string - input string - expected string - }{ - { - name: "tilde expansion", - input: "~/test", - expected: filepath.Join(home, "test"), - }, - { - name: "no expansion needed", - input: "/absolute/path", - expected: "/absolute/path", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := utils.ExpandPath(tt.input) - if err != nil { - t.Fatalf("expandPath failed: %v", err) - } - - if result != tt.expected { - t.Errorf("utils.ExpandPath(%q) = %q, want %q", tt.input, result, tt.expected) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc.go deleted file mode 100644 index 348ee7c..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc.go +++ /dev/null @@ -1,442 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -type MangoWCProvider struct { - configPath string - dmsBindsIncluded bool - parsed bool -} - -func NewMangoWCProvider(configPath string) *MangoWCProvider { - if configPath == "" { - configPath = defaultMangoWCConfigDir() - } - return &MangoWCProvider{ - configPath: configPath, - } -} - -func defaultMangoWCConfigDir() string { - configDir, err := os.UserConfigDir() - if err != nil { - return "" - } - return filepath.Join(configDir, "mango") -} - -func (m *MangoWCProvider) Name() string { - return "mangowc" -} - -func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { - result, err := ParseMangoWCKeysWithDMS(m.configPath) - if err != nil { - return nil, fmt.Errorf("failed to parse mangowc config: %w", err) - } - - m.dmsBindsIncluded = result.DMSBindsIncluded - m.parsed = true - - categorizedBinds := make(map[string][]keybinds.Keybind) - for _, kb := range result.Keybinds { - category := m.categorizeByCommand(kb.Command) - bind := m.convertKeybind(&kb, result.ConflictingConfigs) - categorizedBinds[category] = append(categorizedBinds[category], bind) - } - - sheet := &keybinds.CheatSheet{ - Title: "MangoWC Keybinds", - Provider: m.Name(), - Binds: categorizedBinds, - DMSBindsIncluded: result.DMSBindsIncluded, - } - - if result.DMSStatus != nil { - sheet.DMSStatus = &keybinds.DMSBindsStatus{ - Exists: result.DMSStatus.Exists, - Included: result.DMSStatus.Included, - IncludePosition: result.DMSStatus.IncludePosition, - TotalIncludes: result.DMSStatus.TotalIncludes, - BindsAfterDMS: result.DMSStatus.BindsAfterDMS, - Effective: result.DMSStatus.Effective, - OverriddenBy: result.DMSStatus.OverriddenBy, - StatusMessage: result.DMSStatus.StatusMessage, - } - } - - return sheet, nil -} - -func (m *MangoWCProvider) HasDMSBindsIncluded() bool { - if m.parsed { - return m.dmsBindsIncluded - } - - result, err := ParseMangoWCKeysWithDMS(m.configPath) - if err != nil { - return false - } - - m.dmsBindsIncluded = result.DMSBindsIncluded - m.parsed = true - return m.dmsBindsIncluded -} - -func (m *MangoWCProvider) categorizeByCommand(command string) string { - switch { - case strings.Contains(command, "mon"): - return "Monitor" - case command == "toggleoverview": - return "Overview" - case command == "toggle_scratchpad": - return "Scratchpad" - case strings.Contains(command, "layout") || strings.Contains(command, "proportion"): - return "Layout" - case strings.Contains(command, "gaps"): - return "Gaps" - case strings.Contains(command, "view") || strings.Contains(command, "tag"): - return "Tags" - case command == "focusstack" || - command == "focusdir" || - command == "exchange_client" || - command == "killclient" || - command == "togglefloating" || - command == "togglefullscreen" || - command == "togglefakefullscreen" || - command == "togglemaximizescreen" || - command == "toggleglobal" || - command == "toggleoverlay" || - command == "minimized" || - command == "restore_minimized" || - command == "movewin" || - command == "resizewin": - return "Window" - case command == "spawn" || command == "spawn_shell": - return "Execute" - case command == "quit" || command == "reload_config": - return "System" - default: - return "Other" - } -} - -func (m *MangoWCProvider) convertKeybind(kb *MangoWCKeyBinding, conflicts map[string]*MangoWCKeyBinding) keybinds.Keybind { - keyStr := m.formatKey(kb) - rawAction := m.formatRawAction(kb.Command, kb.Params) - desc := kb.Comment - - if desc == "" { - desc = rawAction - } - - source := "config" - if strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(filepath.Separator)+"binds.conf") { - source = "dms" - } - - bind := keybinds.Keybind{ - Key: keyStr, - Description: desc, - Action: rawAction, - Source: source, - } - - if source == "dms" && conflicts != nil { - normalizedKey := strings.ToLower(keyStr) - if conflictKb, ok := conflicts[normalizedKey]; ok { - bind.Conflict = &keybinds.Keybind{ - Key: keyStr, - Description: conflictKb.Comment, - Action: m.formatRawAction(conflictKb.Command, conflictKb.Params), - Source: "config", - } - } - } - - return bind -} - -func (m *MangoWCProvider) formatRawAction(command, params string) string { - if params != "" { - return command + " " + params - } - return command -} - -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) - return strings.Join(parts, "+") -} - -func (m *MangoWCProvider) GetOverridePath() string { - expanded, err := utils.ExpandPath(m.configPath) - if err != nil { - return filepath.Join(m.configPath, "dms", "binds.conf") - } - return filepath.Join(expanded, "dms", "binds.conf") -} - -func (m *MangoWCProvider) validateAction(action string) error { - action = strings.TrimSpace(action) - switch { - case action == "": - return fmt.Errorf("action cannot be empty") - case action == "spawn" || action == "spawn ": - return fmt.Errorf("spawn command requires arguments") - case action == "spawn_shell" || action == "spawn_shell ": - return fmt.Errorf("spawn_shell command requires arguments") - case strings.HasPrefix(action, "spawn "): - rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn ")) - if rest == "" { - return fmt.Errorf("spawn command requires arguments") - } - case strings.HasPrefix(action, "spawn_shell "): - rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn_shell ")) - if rest == "" { - return fmt.Errorf("spawn_shell command requires arguments") - } - } - return nil -} - -func (m *MangoWCProvider) SetBind(key, action, description string, options map[string]any) error { - if err := m.validateAction(action); err != nil { - return err - } - - overridePath := m.GetOverridePath() - - if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil { - return fmt.Errorf("failed to create dms directory: %w", err) - } - - existingBinds, err := m.loadOverrideBinds() - if err != nil { - existingBinds = make(map[string]*mangowcOverrideBind) - } - - normalizedKey := strings.ToLower(key) - existingBinds[normalizedKey] = &mangowcOverrideBind{ - Key: key, - Action: action, - Description: description, - Options: options, - } - - return m.writeOverrideBinds(existingBinds) -} - -func (m *MangoWCProvider) RemoveBind(key string) error { - existingBinds, err := m.loadOverrideBinds() - if err != nil { - return nil - } - - normalizedKey := strings.ToLower(key) - delete(existingBinds, normalizedKey) - return m.writeOverrideBinds(existingBinds) -} - -type mangowcOverrideBind struct { - Key string - Action string - Description string - Options map[string]any -} - -func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) { - overridePath := m.GetOverridePath() - binds := make(map[string]*mangowcOverrideBind) - - data, err := os.ReadFile(overridePath) - if os.IsNotExist(err) { - return binds, nil - } - if err != nil { - return nil, err - } - - lines := strings.Split(string(data), "\n") - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" || strings.HasPrefix(line, "#") { - continue - } - - if !strings.HasPrefix(line, "bind") { - continue - } - - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - continue - } - - content := strings.TrimSpace(parts[1]) - commentParts := strings.SplitN(content, "#", 2) - bindContent := strings.TrimSpace(commentParts[0]) - - var comment string - if len(commentParts) > 1 { - comment = strings.TrimSpace(commentParts[1]) - } - - fields := strings.SplitN(bindContent, ",", 4) - if len(fields) < 3 { - continue - } - - mods := strings.TrimSpace(fields[0]) - keyName := strings.TrimSpace(fields[1]) - command := strings.TrimSpace(fields[2]) - - var params string - if len(fields) > 3 { - params = strings.TrimSpace(fields[3]) - } - - keyStr := m.buildKeyString(mods, keyName) - normalizedKey := strings.ToLower(keyStr) - action := command - if params != "" { - action = command + " " + params - } - - binds[normalizedKey] = &mangowcOverrideBind{ - Key: keyStr, - Action: action, - Description: comment, - } - } - - return binds, nil -} - -func (m *MangoWCProvider) buildKeyString(mods, key string) string { - if mods == "" || strings.EqualFold(mods, "none") { - return key - } - - modList := strings.FieldsFunc(mods, func(r rune) bool { - return r == '+' || r == ' ' - }) - - parts := append(modList, key) - return strings.Join(parts, "+") -} - -func (m *MangoWCProvider) getBindSortPriority(action string) int { - switch { - case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"): - return 0 - case strings.Contains(action, "view") || strings.Contains(action, "tag"): - return 1 - case strings.Contains(action, "focus") || strings.Contains(action, "exchange") || - strings.Contains(action, "resize") || strings.Contains(action, "move"): - return 2 - case strings.Contains(action, "mon"): - return 3 - case strings.HasPrefix(action, "spawn"): - return 4 - case action == "quit" || action == "reload_config": - return 5 - default: - return 6 - } -} - -func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error { - overridePath := m.GetOverridePath() - content := m.generateBindsContent(binds) - return os.WriteFile(overridePath, []byte(content), 0o644) -} - -func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string { - if len(binds) == 0 { - return "" - } - - bindList := make([]*mangowcOverrideBind, 0, len(binds)) - for _, bind := range binds { - bindList = append(bindList, bind) - } - - sort.Slice(bindList, func(i, j int) bool { - pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action) - if pi != pj { - return pi < pj - } - return bindList[i].Key < bindList[j].Key - }) - - var sb strings.Builder - for _, bind := range bindList { - m.writeBindLine(&sb, bind) - } - - return sb.String() -} - -func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) { - mods, key := m.parseKeyString(bind.Key) - command, params := m.parseAction(bind.Action) - - sb.WriteString("bind=") - if mods == "" { - sb.WriteString("none") - } else { - sb.WriteString(mods) - } - sb.WriteString(",") - sb.WriteString(key) - sb.WriteString(",") - sb.WriteString(command) - - if params != "" { - sb.WriteString(",") - sb.WriteString(params) - } - - if bind.Description != "" { - sb.WriteString(" # ") - sb.WriteString(bind.Description) - } - - sb.WriteString("\n") -} - -func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) { - parts := strings.Split(keyStr, "+") - switch len(parts) { - case 0: - return "", keyStr - case 1: - return "", parts[0] - default: - return strings.Join(parts[:len(parts)-1], "+"), parts[len(parts)-1] - } -} - -func (m *MangoWCProvider) parseAction(action string) (command, params string) { - parts := strings.SplitN(action, " ", 2) - switch len(parts) { - case 0: - return action, "" - case 1: - return parts[0], "" - default: - return parts[0], parts[1] - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_parser.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_parser.go deleted file mode 100644 index 5dac4b6..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_parser.go +++ /dev/null @@ -1,613 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -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"` - Source string `json:"source"` -} - -type MangoWCParser struct { - contentLines []string - readingLine int - configDir string - currentSource string - dmsBindsExists bool - dmsBindsIncluded bool - includeCount int - dmsIncludePos int - bindsAfterDMS int - dmsBindKeys map[string]bool - configBindKeys map[string]bool - conflictingConfigs map[string]*MangoWCKeyBinding - bindMap map[string]*MangoWCKeyBinding - bindOrder []string - processedFiles map[string]bool - dmsProcessed bool -} - -func NewMangoWCParser(configDir string) *MangoWCParser { - return &MangoWCParser{ - contentLines: []string{}, - readingLine: 0, - configDir: configDir, - dmsIncludePos: -1, - dmsBindKeys: make(map[string]bool), - configBindKeys: make(map[string]bool), - conflictingConfigs: make(map[string]*MangoWCKeyBinding), - bindMap: make(map[string]*MangoWCKeyBinding), - bindOrder: []string{}, - processedFiles: make(map[string]bool), - } -} - -func (p *MangoWCParser) ReadContent(path string) error { - expandedPath, err := utils.ExpandPath(path) - if err != nil { - return err - } - - 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(path) - if err := parser.ReadContent(path); err != nil { - return nil, err - } - return parser.ParseKeys(), nil -} - -type MangoWCParseResult struct { - Keybinds []MangoWCKeyBinding - DMSBindsIncluded bool - DMSStatus *MangoWCDMSStatus - ConflictingConfigs map[string]*MangoWCKeyBinding -} - -type MangoWCDMSStatus struct { - Exists bool - Included bool - IncludePosition int - TotalIncludes int - BindsAfterDMS int - Effective bool - OverriddenBy int - StatusMessage string -} - -func (p *MangoWCParser) buildDMSStatus() *MangoWCDMSStatus { - status := &MangoWCDMSStatus{ - Exists: p.dmsBindsExists, - Included: p.dmsBindsIncluded, - IncludePosition: p.dmsIncludePos, - TotalIncludes: p.includeCount, - BindsAfterDMS: p.bindsAfterDMS, - } - - switch { - case !p.dmsBindsExists: - status.Effective = false - status.StatusMessage = "dms/binds.conf does not exist" - case !p.dmsBindsIncluded: - status.Effective = false - status.StatusMessage = "dms/binds.conf is not sourced in config" - case p.bindsAfterDMS > 0: - status.Effective = true - status.OverriddenBy = p.bindsAfterDMS - status.StatusMessage = "Some DMS binds may be overridden by config binds" - default: - status.Effective = true - status.StatusMessage = "DMS binds are active" - } - - return status -} - -func (p *MangoWCParser) formatBindKey(kb *MangoWCKeyBinding) string { - parts := make([]string, 0, len(kb.Mods)+1) - parts = append(parts, kb.Mods...) - parts = append(parts, kb.Key) - return strings.Join(parts, "+") -} - -func (p *MangoWCParser) normalizeKey(key string) string { - return strings.ToLower(key) -} - -func (p *MangoWCParser) addBind(kb *MangoWCKeyBinding) { - key := p.formatBindKey(kb) - normalizedKey := p.normalizeKey(key) - isDMSBind := strings.Contains(kb.Source, "dms/binds.conf") || strings.Contains(kb.Source, "dms"+string(os.PathSeparator)+"binds.conf") - - if isDMSBind { - p.dmsBindKeys[normalizedKey] = true - } else if p.dmsBindKeys[normalizedKey] { - p.bindsAfterDMS++ - p.conflictingConfigs[normalizedKey] = kb - p.configBindKeys[normalizedKey] = true - return - } else { - p.configBindKeys[normalizedKey] = true - } - - if _, exists := p.bindMap[normalizedKey]; !exists { - p.bindOrder = append(p.bindOrder, key) - } - p.bindMap[normalizedKey] = kb -} - -func (p *MangoWCParser) ParseWithDMS() ([]MangoWCKeyBinding, error) { - expandedDir, err := utils.ExpandPath(p.configDir) - if err != nil { - return nil, err - } - - dmsBindsPath := filepath.Join(expandedDir, "dms", "binds.conf") - if _, err := os.Stat(dmsBindsPath); err == nil { - p.dmsBindsExists = true - } - - mainConfig := filepath.Join(expandedDir, "config.conf") - if _, err := os.Stat(mainConfig); os.IsNotExist(err) { - mainConfig = filepath.Join(expandedDir, "mango.conf") - } - - _, err = p.parseFileWithSource(mainConfig) - if err != nil { - return nil, err - } - - if p.dmsBindsExists && !p.dmsProcessed { - p.parseDMSBindsDirectly(dmsBindsPath) - } - - var keybinds []MangoWCKeyBinding - for _, key := range p.bindOrder { - normalizedKey := p.normalizeKey(key) - if kb, exists := p.bindMap[normalizedKey]; exists { - keybinds = append(keybinds, *kb) - } - } - - return keybinds, nil -} - -func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBinding, error) { - absPath, err := filepath.Abs(filePath) - if err != nil { - return nil, err - } - - if p.processedFiles[absPath] { - return nil, nil - } - p.processedFiles[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return nil, err - } - - prevSource := p.currentSource - p.currentSource = absPath - - var keybinds []MangoWCKeyBinding - lines := strings.Split(string(data), "\n") - - for lineNum, line := range lines { - trimmed := strings.TrimSpace(line) - - if strings.HasPrefix(trimmed, "source") { - p.handleSource(trimmed, filepath.Dir(absPath), &keybinds) - continue - } - - if !strings.HasPrefix(trimmed, "bind") { - continue - } - - kb := p.getKeybindAtLineContent(line, lineNum) - if kb == nil { - continue - } - kb.Source = p.currentSource - p.addBind(kb) - keybinds = append(keybinds, *kb) - } - - p.currentSource = prevSource - return keybinds, nil -} - -func (p *MangoWCParser) handleSource(line, baseDir string, keybinds *[]MangoWCKeyBinding) { - parts := strings.SplitN(line, "=", 2) - if len(parts) < 2 { - return - } - - sourcePath := strings.TrimSpace(parts[1]) - isDMSSource := sourcePath == "dms/binds.conf" || sourcePath == "./dms/binds.conf" || strings.HasSuffix(sourcePath, "/dms/binds.conf") - - p.includeCount++ - if isDMSSource { - p.dmsBindsIncluded = true - p.dmsIncludePos = p.includeCount - p.dmsProcessed = true - } - - expanded, err := utils.ExpandPath(sourcePath) - if err != nil { - return - } - - fullPath := expanded - if !filepath.IsAbs(expanded) { - fullPath = filepath.Join(baseDir, expanded) - } - - includedBinds, err := p.parseFileWithSource(fullPath) - if err != nil { - return - } - - *keybinds = append(*keybinds, includedBinds...) -} - -func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyBinding { - keybinds, err := p.parseFileWithSource(dmsBindsPath) - if err != nil { - return nil - } - p.dmsProcessed = true - return keybinds -} - -func (p *MangoWCParser) getKeybindAtLineContent(line string, _ int) *MangoWCKeyBinding { - bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`) - matches := bindMatch.FindStringSubmatch(line) - if len(matches) < 3 { - return nil - } - - 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]) - idx := 0 - for index, char := range modstring { - isModSep := false - for _, sep := range MangoWCModSeparators { - if char == sep { - isModSep = true - break - } - } - if isModSep { - if index-idx > 1 { - modList = append(modList, modstring[idx:index]) - } - idx = index + 1 - } - } - } - - return &MangoWCKeyBinding{ - Mods: modList, - Key: key, - Command: command, - Params: params, - Comment: comment, - } -} - -func ParseMangoWCKeysWithDMS(path string) (*MangoWCParseResult, error) { - parser := NewMangoWCParser(path) - keybinds, err := parser.ParseWithDMS() - if err != nil { - return nil, err - } - - return &MangoWCParseResult{ - Keybinds: keybinds, - DMSBindsIncluded: parser.dmsBindsIncluded, - DMSStatus: parser.buildDMSStatus(), - ConflictingConfigs: parser.conflictingConfigs, - }, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_parser_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_parser_test.go deleted file mode 100644 index 016f50e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_parser_test.go +++ /dev/null @@ -1,499 +0,0 @@ -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), 0o644); 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), 0o644); err != nil { - t.Fatalf("Failed to write file1: %v", err) - } - if err := os.WriteFile(file2, []byte(content2), 0o644); 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), 0o644); 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, 0o755); 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"), 0o644); 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), 0o644); 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), 0o644); 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") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_test.go deleted file mode 100644 index d417ce1..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/mangowc_test.go +++ /dev/null @@ -1,320 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestMangoWCProviderName(t *testing.T) { - provider := NewMangoWCProvider("") - if provider.Name() != "mangowc" { - t.Errorf("Name() = %q, want %q", provider.Name(), "mangowc") - } -} - -func TestMangoWCProviderDefaultPath(t *testing.T) { - provider := NewMangoWCProvider("") - configDir, err := os.UserConfigDir() - if err != nil { - // Fall back to testing for non-empty path - if provider.configPath == "" { - t.Error("configPath should not be empty") - } - return - } - expected := filepath.Join(configDir, "mango") - if provider.configPath != expected { - t.Errorf("configPath = %q, want %q", provider.configPath, expected) - } -} - -func TestMangoWCProviderCustomPath(t *testing.T) { - customPath := "/custom/path" - provider := NewMangoWCProvider(customPath) - if provider.configPath != customPath { - t.Errorf("configPath = %q, want %q", provider.configPath, customPath) - } -} - -func TestMangoWCCategorizeByCommand(t *testing.T) { - tests := []struct { - command string - expected string - }{ - {"view", "Tags"}, - {"tag", "Tags"}, - {"toggleview", "Tags"}, - {"viewtoleft", "Tags"}, - {"viewtoright", "Tags"}, - {"viewtoleft_have_client", "Tags"}, - {"tagtoleft", "Tags"}, - {"tagtoright", "Tags"}, - {"focusmon", "Monitor"}, - {"tagmon", "Monitor"}, - {"focusstack", "Window"}, - {"focusdir", "Window"}, - {"exchange_client", "Window"}, - {"killclient", "Window"}, - {"togglefloating", "Window"}, - {"togglefullscreen", "Window"}, - {"togglefakefullscreen", "Window"}, - {"togglemaximizescreen", "Window"}, - {"toggleglobal", "Window"}, - {"toggleoverlay", "Window"}, - {"minimized", "Window"}, - {"restore_minimized", "Window"}, - {"movewin", "Window"}, - {"resizewin", "Window"}, - {"toggleoverview", "Overview"}, - {"toggle_scratchpad", "Scratchpad"}, - {"setlayout", "Layout"}, - {"switch_layout", "Layout"}, - {"set_proportion", "Layout"}, - {"switch_proportion_preset", "Layout"}, - {"incgaps", "Gaps"}, - {"togglegaps", "Gaps"}, - {"spawn", "Execute"}, - {"spawn_shell", "Execute"}, - {"quit", "System"}, - {"reload_config", "System"}, - {"unknown_command", "Other"}, - } - - provider := NewMangoWCProvider("") - for _, tt := range tests { - t.Run(tt.command, func(t *testing.T) { - result := provider.categorizeByCommand(tt.command) - if result != tt.expected { - t.Errorf("categorizeByCommand(%q) = %q, want %q", tt.command, result, tt.expected) - } - }) - } -} - -func TestMangoWCFormatKey(t *testing.T) { - tests := []struct { - name string - keybind *MangoWCKeyBinding - expected string - }{ - { - name: "single_mod", - keybind: &MangoWCKeyBinding{ - Mods: []string{"ALT"}, - Key: "q", - }, - expected: "ALT+q", - }, - { - name: "multiple_mods", - keybind: &MangoWCKeyBinding{ - Mods: []string{"SUPER", "SHIFT"}, - Key: "Up", - }, - expected: "SUPER+SHIFT+Up", - }, - { - name: "no_mods", - keybind: &MangoWCKeyBinding{ - Mods: []string{}, - Key: "Print", - }, - expected: "Print", - }, - } - - provider := NewMangoWCProvider("") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := provider.formatKey(tt.keybind) - if result != tt.expected { - t.Errorf("formatKey() = %q, want %q", result, tt.expected) - } - }) - } -} - -func TestMangoWCConvertKeybind(t *testing.T) { - tests := []struct { - name string - keybind *MangoWCKeyBinding - wantKey string - wantDesc string - }{ - { - name: "with_comment", - keybind: &MangoWCKeyBinding{ - Mods: []string{"ALT"}, - Key: "t", - Command: "spawn", - Params: "kitty", - Comment: "Open terminal", - }, - wantKey: "ALT+t", - wantDesc: "Open terminal", - }, - { - name: "without_comment", - keybind: &MangoWCKeyBinding{ - Mods: []string{"SUPER"}, - Key: "r", - Command: "reload_config", - Params: "", - Comment: "", - }, - wantKey: "SUPER+r", - wantDesc: "reload_config", - }, - { - name: "with_params_no_comment", - keybind: &MangoWCKeyBinding{ - Mods: []string{"CTRL"}, - Key: "1", - Command: "view", - Params: "1,0", - Comment: "", - }, - wantKey: "CTRL+1", - wantDesc: "view 1,0", - }, - } - - provider := NewMangoWCProvider("") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := provider.convertKeybind(tt.keybind, nil) - if result.Key != tt.wantKey { - t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey) - } - if result.Description != tt.wantDesc { - t.Errorf("convertKeybind().Description = %q, want %q", result.Description, tt.wantDesc) - } - }) - } -} - -func TestMangoWCGetCheatSheet(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.conf") - - content := `# MangoWC Configuration -blur=0 - -# Key Bindings -bind=SUPER,r,reload_config -bind=Alt,t,spawn,kitty # Terminal -bind=ALT,q,killclient, - -# Window management -bind=ALT,Left,focusdir,left -bind=ALT,Right,focusdir,right -bind=SUPER+SHIFT,Up,exchange_client,up - -# Tags -bind=Ctrl,1,view,1,0 -bind=Ctrl,2,view,2,0 -bind=Alt,1,tag,1,0 - -# Layout -bind=SUPER,n,switch_layout - -# Gaps -bind=ALT+SHIFT,X,incgaps,1 -` - - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - provider := NewMangoWCProvider(tmpDir) - sheet, err := provider.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - if sheet == nil { - t.Fatal("Expected non-nil CheatSheet") - } - - if sheet.Title != "MangoWC Keybinds" { - t.Errorf("Title = %q, want %q", sheet.Title, "MangoWC Keybinds") - } - - if sheet.Provider != "mangowc" { - t.Errorf("Provider = %q, want %q", sheet.Provider, "mangowc") - } - - categories := []string{"System", "Execute", "Window", "Tags", "Layout", "Gaps"} - for _, category := range categories { - if _, exists := sheet.Binds[category]; !exists { - t.Errorf("Expected category %q to exist", category) - } - } - - if len(sheet.Binds["System"]) < 1 { - t.Error("Expected at least 1 System keybind") - } - if len(sheet.Binds["Execute"]) < 1 { - t.Error("Expected at least 1 Execute keybind") - } - if len(sheet.Binds["Window"]) < 3 { - t.Error("Expected at least 3 Window keybinds") - } - if len(sheet.Binds["Tags"]) < 3 { - t.Error("Expected at least 3 Tags keybinds") - } -} - -func TestMangoWCGetCheatSheetError(t *testing.T) { - provider := NewMangoWCProvider("/nonexistent/path") - _, err := provider.GetCheatSheet() - if err == nil { - t.Error("Expected error for nonexistent path, got nil") - } -} - -func TestMangoWCIntegration(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.conf") - - content := `bind=Alt,t,spawn,kitty # Open terminal -bind=ALT,q,killclient, -bind=SUPER,r,reload_config # Reload config -bind=ALT,Left,focusdir,left -bind=Ctrl,1,view,1,0 -` - - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - provider := NewMangoWCProvider(tmpDir) - sheet, err := provider.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - totalBinds := 0 - for _, binds := range sheet.Binds { - totalBinds += len(binds) - } - - expectedBinds := 5 - if totalBinds != expectedBinds { - t.Errorf("Expected %d total keybinds, got %d", expectedBinds, totalBinds) - } - - foundTerminal := false - for _, binds := range sheet.Binds { - for _, bind := range binds { - if bind.Description == "Open terminal" && bind.Key == "Alt+t" { - foundTerminal = true - } - } - } - - if !foundTerminal { - t.Error("Did not find terminal keybind with correct key and description") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/miracle.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/miracle.go deleted file mode 100644 index 43fc58e..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/miracle.go +++ /dev/null @@ -1,95 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -type MiracleProvider struct { - configPath string -} - -func NewMiracleProvider(configPath string) *MiracleProvider { - if configPath == "" { - configDir, err := os.UserConfigDir() - if err == nil { - configPath = filepath.Join(configDir, "miracle-wm") - } - } - return &MiracleProvider{configPath: configPath} -} - -func (m *MiracleProvider) Name() string { - return "miracle" -} - -func (m *MiracleProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { - config, err := ParseMiracleConfig(m.configPath) - if err != nil { - return nil, fmt.Errorf("failed to parse miracle-wm config: %w", err) - } - - bindings := MiracleConfigToBindings(config) - categorizedBinds := make(map[string][]keybinds.Keybind) - - for _, kb := range bindings { - category := m.categorizeAction(kb.Action) - bind := keybinds.Keybind{ - Key: m.formatKey(kb), - Description: kb.Comment, - Action: kb.Action, - } - categorizedBinds[category] = append(categorizedBinds[category], bind) - } - - return &keybinds.CheatSheet{ - Title: "Miracle WM Keybinds", - Provider: m.Name(), - Binds: categorizedBinds, - }, nil -} - -func (m *MiracleProvider) GetOverridePath() string { - expanded, err := utils.ExpandPath(m.configPath) - if err != nil { - return filepath.Join(m.configPath, "config.yaml") - } - return filepath.Join(expanded, "config.yaml") -} - -func (m *MiracleProvider) formatKey(kb MiracleKeyBinding) string { - parts := make([]string, 0, len(kb.Mods)+1) - parts = append(parts, kb.Mods...) - parts = append(parts, kb.Key) - return strings.Join(parts, "+") -} - -func (m *MiracleProvider) categorizeAction(action string) string { - switch { - case strings.HasPrefix(action, "select_workspace_") || strings.HasPrefix(action, "move_to_workspace_"): - return "Workspace" - case strings.Contains(action, "select_") || strings.Contains(action, "move_"): - return "Window" - case action == "toggle_resize" || strings.HasPrefix(action, "resize_"): - return "Window" - case action == "fullscreen" || action == "toggle_floating" || action == "quit_active_window" || action == "toggle_pinned_to_workspace": - return "Window" - case action == "toggle_tabbing" || action == "toggle_stacking" || action == "request_vertical" || action == "request_horizontal": - return "Layout" - case action == "quit_compositor": - return "System" - case action == "terminal": - return "Execute" - case strings.HasPrefix(action, "magnifier_"): - return "Accessibility" - case strings.HasPrefix(action, "dms ") || strings.Contains(action, "dms ipc"): - return "Execute" - default: - return "Execute" - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/miracle_parser.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/miracle_parser.go deleted file mode 100644 index 425c64a..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/miracle_parser.go +++ /dev/null @@ -1,320 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" - "gopkg.in/yaml.v3" -) - -type MiracleConfig struct { - Terminal string `yaml:"terminal"` - ActionKey string `yaml:"action_key"` - DefaultActionOverrides []MiracleActionOverride `yaml:"default_action_overrides"` - CustomActions []MiracleCustomAction `yaml:"custom_actions"` -} - -type MiracleActionOverride struct { - Name string `yaml:"name"` - Action string `yaml:"action"` - Modifiers []string `yaml:"modifiers"` - Key string `yaml:"key"` -} - -type MiracleCustomAction struct { - Command string `yaml:"command"` - Action string `yaml:"action"` - Modifiers []string `yaml:"modifiers"` - Key string `yaml:"key"` -} - -type MiracleKeyBinding struct { - Mods []string - Key string - Action string - Comment string -} - -var miracleDefaultBinds = []MiracleKeyBinding{ - {Mods: []string{"Super"}, Key: "Return", Action: "terminal", Comment: "Open terminal"}, - {Mods: []string{"Super"}, Key: "v", Action: "request_vertical", Comment: "Layout windows vertically"}, - {Mods: []string{"Super"}, Key: "h", Action: "request_horizontal", Comment: "Layout windows horizontally"}, - {Mods: []string{"Super"}, Key: "Up", Action: "select_up", Comment: "Select window above"}, - {Mods: []string{"Super"}, Key: "Down", Action: "select_down", Comment: "Select window below"}, - {Mods: []string{"Super"}, Key: "Left", Action: "select_left", Comment: "Select window left"}, - {Mods: []string{"Super"}, Key: "Right", Action: "select_right", Comment: "Select window right"}, - {Mods: []string{"Super", "Shift"}, Key: "Up", Action: "move_up", Comment: "Move window up"}, - {Mods: []string{"Super", "Shift"}, Key: "Down", Action: "move_down", Comment: "Move window down"}, - {Mods: []string{"Super", "Shift"}, Key: "Left", Action: "move_left", Comment: "Move window left"}, - {Mods: []string{"Super", "Shift"}, Key: "Right", Action: "move_right", Comment: "Move window right"}, - {Mods: []string{"Super"}, Key: "r", Action: "toggle_resize", Comment: "Toggle resize mode"}, - {Mods: []string{"Super"}, Key: "f", Action: "fullscreen", Comment: "Toggle fullscreen"}, - {Mods: []string{"Super", "Shift"}, Key: "q", Action: "quit_active_window", Comment: "Close window"}, - {Mods: []string{"Super", "Shift"}, Key: "e", Action: "quit_compositor", Comment: "Exit compositor"}, - {Mods: []string{"Super"}, Key: "Space", Action: "toggle_floating", Comment: "Toggle floating"}, - {Mods: []string{"Super", "Shift"}, Key: "p", Action: "toggle_pinned_to_workspace", Comment: "Toggle pinned to workspace"}, - {Mods: []string{"Super"}, Key: "w", Action: "toggle_tabbing", Comment: "Toggle tabbing layout"}, - {Mods: []string{"Super"}, Key: "s", Action: "toggle_stacking", Comment: "Toggle stacking layout"}, - {Mods: []string{"Super"}, Key: "1", Action: "select_workspace_0", Comment: "Workspace 1"}, - {Mods: []string{"Super"}, Key: "2", Action: "select_workspace_1", Comment: "Workspace 2"}, - {Mods: []string{"Super"}, Key: "3", Action: "select_workspace_2", Comment: "Workspace 3"}, - {Mods: []string{"Super"}, Key: "4", Action: "select_workspace_3", Comment: "Workspace 4"}, - {Mods: []string{"Super"}, Key: "5", Action: "select_workspace_4", Comment: "Workspace 5"}, - {Mods: []string{"Super"}, Key: "6", Action: "select_workspace_5", Comment: "Workspace 6"}, - {Mods: []string{"Super"}, Key: "7", Action: "select_workspace_6", Comment: "Workspace 7"}, - {Mods: []string{"Super"}, Key: "8", Action: "select_workspace_7", Comment: "Workspace 8"}, - {Mods: []string{"Super"}, Key: "9", Action: "select_workspace_8", Comment: "Workspace 9"}, - {Mods: []string{"Super"}, Key: "0", Action: "select_workspace_9", Comment: "Workspace 10"}, - {Mods: []string{"Super", "Shift"}, Key: "1", Action: "move_to_workspace_0", Comment: "Move to workspace 1"}, - {Mods: []string{"Super", "Shift"}, Key: "2", Action: "move_to_workspace_1", Comment: "Move to workspace 2"}, - {Mods: []string{"Super", "Shift"}, Key: "3", Action: "move_to_workspace_2", Comment: "Move to workspace 3"}, - {Mods: []string{"Super", "Shift"}, Key: "4", Action: "move_to_workspace_3", Comment: "Move to workspace 4"}, - {Mods: []string{"Super", "Shift"}, Key: "5", Action: "move_to_workspace_4", Comment: "Move to workspace 5"}, - {Mods: []string{"Super", "Shift"}, Key: "6", Action: "move_to_workspace_5", Comment: "Move to workspace 6"}, - {Mods: []string{"Super", "Shift"}, Key: "7", Action: "move_to_workspace_6", Comment: "Move to workspace 7"}, - {Mods: []string{"Super", "Shift"}, Key: "8", Action: "move_to_workspace_7", Comment: "Move to workspace 8"}, - {Mods: []string{"Super", "Shift"}, Key: "9", Action: "move_to_workspace_8", Comment: "Move to workspace 9"}, - {Mods: []string{"Super", "Shift"}, Key: "0", Action: "move_to_workspace_9", Comment: "Move to workspace 10"}, -} - -func ParseMiracleConfig(configPath string) (*MiracleConfig, error) { - expanded, err := utils.ExpandPath(configPath) - if err != nil { - return nil, err - } - - info, err := os.Stat(expanded) - if err != nil { - return nil, err - } - - var configFile string - if info.IsDir() { - configFile = filepath.Join(expanded, "config.yaml") - } else { - configFile = expanded - } - - data, err := os.ReadFile(configFile) - if err != nil { - return nil, err - } - - var config MiracleConfig - if err := yaml.Unmarshal(data, &config); err != nil { - return nil, err - } - - if config.ActionKey == "" { - config.ActionKey = "meta" - } - - return &config, nil -} - -func resolveMiracleModifier(mod, actionKey string) string { - switch mod { - case "primary": - return resolveActionKey(actionKey) - case "alt", "alt_left", "alt_right": - return "Alt" - case "shift", "shift_left", "shift_right": - return "Shift" - case "ctrl", "ctrl_left", "ctrl_right": - return "Ctrl" - case "meta", "meta_left", "meta_right": - return "Super" - default: - return mod - } -} - -func resolveActionKey(actionKey string) string { - switch actionKey { - case "meta": - return "Super" - case "alt": - return "Alt" - case "ctrl": - return "Ctrl" - default: - return "Super" - } -} - -func miracleKeyCodeToName(keyCode string) string { - name := strings.TrimPrefix(keyCode, "KEY_") - name = strings.ToLower(name) - - switch name { - case "enter": - return "Return" - case "space": - return "Space" - case "up": - return "Up" - case "down": - return "Down" - case "left": - return "Left" - case "right": - return "Right" - case "tab": - return "Tab" - case "escape", "esc": - return "Escape" - case "delete": - return "Delete" - case "backspace": - return "BackSpace" - case "home": - return "Home" - case "end": - return "End" - case "pageup": - return "Page_Up" - case "pagedown": - return "Page_Down" - case "print": - return "Print" - case "pause": - return "Pause" - case "volumeup": - return "XF86AudioRaiseVolume" - case "volumedown": - return "XF86AudioLowerVolume" - case "mute": - return "XF86AudioMute" - case "micmute": - return "XF86AudioMicMute" - case "brightnessup": - return "XF86MonBrightnessUp" - case "brightnessdown": - return "XF86MonBrightnessDown" - case "kbdillumup": - return "XF86KbdBrightnessUp" - case "kbdillumdown": - return "XF86KbdBrightnessDown" - case "comma": - return "comma" - case "minus": - return "minus" - case "equal": - return "equal" - } - - if len(name) == 1 { - return name - } - - return name -} - -func MiracleConfigToBindings(config *MiracleConfig) []MiracleKeyBinding { - overridden := make(map[string]bool) - var bindings []MiracleKeyBinding - - for _, override := range config.DefaultActionOverrides { - mods := make([]string, 0, len(override.Modifiers)) - for _, mod := range override.Modifiers { - mods = append(mods, resolveMiracleModifier(mod, config.ActionKey)) - } - - bindings = append(bindings, MiracleKeyBinding{ - Mods: mods, - Key: miracleKeyCodeToName(override.Key), - Action: override.Name, - Comment: miracleActionDescription(override.Name), - }) - overridden[override.Name] = true - } - - for _, def := range miracleDefaultBinds { - if overridden[def.Action] { - continue - } - bindings = append(bindings, def) - } - - for _, custom := range config.CustomActions { - mods := make([]string, 0, len(custom.Modifiers)) - for _, mod := range custom.Modifiers { - mods = append(mods, resolveMiracleModifier(mod, config.ActionKey)) - } - - bindings = append(bindings, MiracleKeyBinding{ - Mods: mods, - Key: miracleKeyCodeToName(custom.Key), - Action: custom.Command, - Comment: custom.Command, - }) - } - - return bindings -} - -func miracleActionDescription(action string) string { - switch action { - case "terminal": - return "Open terminal" - case "request_vertical": - return "Layout windows vertically" - case "request_horizontal": - return "Layout windows horizontally" - case "select_up": - return "Select window above" - case "select_down": - return "Select window below" - case "select_left": - return "Select window left" - case "select_right": - return "Select window right" - case "move_up": - return "Move window up" - case "move_down": - return "Move window down" - case "move_left": - return "Move window left" - case "move_right": - return "Move window right" - case "toggle_resize": - return "Toggle resize mode" - case "fullscreen": - return "Toggle fullscreen" - case "quit_active_window": - return "Close window" - case "quit_compositor": - return "Exit compositor" - case "toggle_floating": - return "Toggle floating" - case "toggle_pinned_to_workspace": - return "Toggle pinned to workspace" - case "toggle_tabbing": - return "Toggle tabbing layout" - case "toggle_stacking": - return "Toggle stacking layout" - case "magnifier_on": - return "Enable magnifier" - case "magnifier_off": - return "Disable magnifier" - case "magnifier_increase_size": - return "Increase magnifier area" - case "magnifier_decrease_size": - return "Decrease magnifier area" - case "magnifier_increase_scale": - return "Increase magnifier scale" - case "magnifier_decrease_scale": - return "Decrease magnifier scale" - } - - if num, ok := strings.CutPrefix(action, "select_workspace_"); ok { - return "Workspace " + num - } - if num, ok := strings.CutPrefix(action, "move_to_workspace_"); ok { - return "Move to workspace " + num - } - - return action -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri.go deleted file mode 100644 index aec20b5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri.go +++ /dev/null @@ -1,650 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "sort" - "strconv" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" - "github.com/sblinch/kdl-go" - "github.com/sblinch/kdl-go/document" -) - -type NiriProvider struct { - configDir string - dmsBindsIncluded bool - parsed bool -} - -func NewNiriProvider(configDir string) *NiriProvider { - if configDir == "" { - configDir = defaultNiriConfigDir() - } - return &NiriProvider{ - configDir: configDir, - } -} - -func defaultNiriConfigDir() string { - configDir, err := os.UserConfigDir() - if err != nil { - return "" - } - return filepath.Join(configDir, "niri") -} - -func (n *NiriProvider) Name() string { - return "niri" -} - -func (n *NiriProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { - result, err := ParseNiriKeys(n.configDir) - if err != nil { - return nil, fmt.Errorf("failed to parse niri config: %w", err) - } - - n.dmsBindsIncluded = result.DMSBindsIncluded - n.parsed = true - - categorizedBinds := make(map[string][]keybinds.Keybind) - n.convertSection(result.Section, "", categorizedBinds, result.ConflictingConfigs) - - sheet := &keybinds.CheatSheet{ - Title: "Niri Keybinds", - Provider: n.Name(), - Binds: categorizedBinds, - DMSBindsIncluded: result.DMSBindsIncluded, - } - - if result.DMSStatus != nil { - sheet.DMSStatus = &keybinds.DMSBindsStatus{ - Exists: result.DMSStatus.Exists, - Included: result.DMSStatus.Included, - IncludePosition: result.DMSStatus.IncludePosition, - TotalIncludes: result.DMSStatus.TotalIncludes, - BindsAfterDMS: result.DMSStatus.BindsAfterDMS, - Effective: result.DMSStatus.Effective, - OverriddenBy: result.DMSStatus.OverriddenBy, - StatusMessage: result.DMSStatus.StatusMessage, - } - } - - return sheet, nil -} - -func (n *NiriProvider) HasDMSBindsIncluded() bool { - if n.parsed { - return n.dmsBindsIncluded - } - - result, err := ParseNiriKeys(n.configDir) - if err != nil { - return false - } - - n.dmsBindsIncluded = result.DMSBindsIncluded - n.parsed = true - return n.dmsBindsIncluded -} - -func (n *NiriProvider) convertSection(section *NiriSection, subcategory string, categorizedBinds map[string][]keybinds.Keybind, conflicts map[string]*NiriKeyBinding) { - currentSubcat := subcategory - if section.Name != "" { - currentSubcat = section.Name - } - - for _, kb := range section.Keybinds { - category := n.categorizeByAction(kb.Action) - bind := n.convertKeybind(&kb, currentSubcat, conflicts) - categorizedBinds[category] = append(categorizedBinds[category], bind) - } - - for _, child := range section.Children { - n.convertSection(&child, currentSubcat, categorizedBinds, conflicts) - } -} - -func (n *NiriProvider) categorizeByAction(action string) string { - switch { - case action == "next-window" || action == "previous-window": - return "Alt-Tab" - case strings.Contains(action, "screenshot"): - return "Screenshot" - case action == "show-hotkey-overlay" || action == "toggle-overview": - return "Overview" - case action == "quit" || - action == "power-off-monitors" || - action == "power-on-monitors" || - action == "suspend" || - action == "do-screen-transition" || - action == "toggle-keyboard-shortcuts-inhibit" || - strings.Contains(action, "dpms"): - return "System" - case action == "spawn": - return "Execute" - case strings.Contains(action, "workspace"): - return "Workspace" - case strings.HasPrefix(action, "focus-monitor") || - strings.HasPrefix(action, "move-column-to-monitor") || - strings.HasPrefix(action, "move-window-to-monitor"): - return "Monitor" - case strings.Contains(action, "window") || - strings.Contains(action, "focus") || - strings.Contains(action, "move") || - strings.Contains(action, "swap") || - strings.Contains(action, "resize") || - strings.Contains(action, "column"): - return "Window" - default: - return "Other" - } -} - -func (n *NiriProvider) convertKeybind(kb *NiriKeyBinding, subcategory string, conflicts map[string]*NiriKeyBinding) keybinds.Keybind { - rawAction := n.formatRawAction(kb.Action, kb.Args) - keyStr := n.formatKey(kb) - - source := "config" - if strings.Contains(kb.Source, "dms/binds.kdl") { - source = "dms" - } - - bind := keybinds.Keybind{ - Key: keyStr, - Description: kb.Description, - Action: rawAction, - Subcategory: subcategory, - Source: source, - HideOnOverlay: kb.HideOnOverlay, - CooldownMs: kb.CooldownMs, - AllowWhenLocked: kb.AllowWhenLocked, - AllowInhibiting: kb.AllowInhibiting, - Repeat: kb.Repeat, - } - - if source == "dms" && conflicts != nil { - if conflictKb, ok := conflicts[keyStr]; ok { - bind.Conflict = &keybinds.Keybind{ - Key: keyStr, - Description: conflictKb.Description, - Action: n.formatRawAction(conflictKb.Action, conflictKb.Args), - Source: "config", - } - } - } - - return bind -} - -func (n *NiriProvider) formatRawAction(action string, args []string) string { - if len(args) == 0 { - return action - } - - if action == "spawn" && len(args) >= 3 && args[1] == "-c" { - switch args[0] { - case "sh", "bash": - cmd := strings.Join(args[2:], " ") - return fmt.Sprintf("spawn %s -c \"%s\"", args[0], strings.ReplaceAll(cmd, "\"", "\\\"")) - } - } - - quotedArgs := make([]string, len(args)) - for i, arg := range args { - if arg == "" { - quotedArgs[i] = `""` - } else { - quotedArgs[i] = arg - } - } - return action + " " + strings.Join(quotedArgs, " ") -} - -func (n *NiriProvider) formatKey(kb *NiriKeyBinding) string { - parts := make([]string, 0, len(kb.Mods)+1) - parts = append(parts, kb.Mods...) - parts = append(parts, kb.Key) - return strings.Join(parts, "+") -} - -func (n *NiriProvider) GetOverridePath() string { - return filepath.Join(n.configDir, "dms", "binds.kdl") -} - -func (n *NiriProvider) validateAction(action string) error { - action = strings.TrimSpace(action) - switch { - case action == "": - return fmt.Errorf("action cannot be empty") - case action == "spawn" || action == "spawn ": - return fmt.Errorf("spawn command requires arguments") - case strings.HasPrefix(action, "spawn "): - rest := strings.TrimSpace(strings.TrimPrefix(action, "spawn ")) - switch rest { - case "": - return fmt.Errorf("spawn command requires arguments") - case "sh -c \"\"", "sh -c ''", "bash -c \"\"", "bash -c ''": - return fmt.Errorf("shell command cannot be empty") - } - } - return nil -} - -func (n *NiriProvider) SetBind(key, action, description string, options map[string]any) error { - if err := n.validateAction(action); err != nil { - return err - } - - overridePath := n.GetOverridePath() - - if err := os.MkdirAll(filepath.Dir(overridePath), 0o755); err != nil { - return fmt.Errorf("failed to create dms directory: %w", err) - } - - existingBinds, err := n.loadOverrideBinds() - if err != nil { - existingBinds = make(map[string]*overrideBind) - } - - existingBinds[key] = &overrideBind{ - Key: key, - Action: action, - Description: description, - Options: options, - } - - return n.writeOverrideBinds(existingBinds) -} - -func (n *NiriProvider) RemoveBind(key string) error { - existingBinds, err := n.loadOverrideBinds() - if err != nil { - return nil - } - - delete(existingBinds, key) - return n.writeOverrideBinds(existingBinds) -} - -type overrideBind struct { - Key string - Action string - Description string - Options map[string]any -} - -func (n *NiriProvider) loadOverrideBinds() (map[string]*overrideBind, error) { - overridePath := n.GetOverridePath() - binds := make(map[string]*overrideBind) - - data, err := os.ReadFile(overridePath) - if os.IsNotExist(err) { - return binds, nil - } - if err != nil { - return nil, err - } - - parser := NewNiriParser(filepath.Dir(overridePath)) - parser.currentSource = overridePath - - doc, err := kdl.Parse(strings.NewReader(string(data))) - if err != nil { - return nil, err - } - - for _, node := range doc.Nodes { - if node.Name.String() != "binds" || node.Children == nil { - continue - } - for _, child := range node.Children { - kb := parser.parseKeybindNode(child, "") - if kb == nil { - continue - } - keyStr := parser.formatBindKey(kb) - - action := n.buildActionFromNode(child) - if action == "" { - action = n.formatRawAction(kb.Action, kb.Args) - } - - binds[keyStr] = &overrideBind{ - Key: keyStr, - Action: action, - Description: kb.Description, - Options: n.extractOptions(child), - } - } - } - - return binds, nil -} - -func (n *NiriProvider) buildActionFromNode(bindNode *document.Node) string { - if len(bindNode.Children) == 0 { - return "" - } - - actionNode := bindNode.Children[0] - actionName := actionNode.Name.String() - if actionName == "" { - return "" - } - - parts := []string{actionName} - for _, arg := range actionNode.Arguments { - val := arg.ValueString() - if val == "" { - parts = append(parts, `""`) - } else if strings.ContainsAny(val, " \t") { - parts = append(parts, `"`+strings.ReplaceAll(val, `"`, `\"`)+`"`) - } else { - parts = append(parts, val) - } - } - - if actionNode.Properties != nil { - for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} { - if val, ok := actionNode.Properties.Get(propName); ok { - parts = append(parts, propName+"="+val.String()) - } - } - } - - return strings.Join(parts, " ") -} - -func (n *NiriProvider) extractOptions(node *document.Node) map[string]any { - if node.Properties == nil { - return make(map[string]any) - } - - opts := make(map[string]any) - if val, ok := node.Properties.Get("repeat"); ok { - opts["repeat"] = val.String() == "true" - } - if val, ok := node.Properties.Get("cooldown-ms"); ok { - if ms, err := strconv.Atoi(val.String()); err == nil { - opts["cooldown-ms"] = ms - } - } - if val, ok := node.Properties.Get("allow-when-locked"); ok { - opts["allow-when-locked"] = val.String() == "true" - } - if val, ok := node.Properties.Get("allow-inhibiting"); ok { - opts["allow-inhibiting"] = val.String() == "true" - } - return opts -} - -func (n *NiriProvider) isRecentWindowsAction(action string) bool { - switch action { - case "next-window", "previous-window": - return true - default: - return false - } -} - -func (n *NiriProvider) buildBindNode(bind *overrideBind) *document.Node { - node := document.NewNode() - node.SetName(bind.Key) - - if bind.Options != nil { - if v, ok := bind.Options["repeat"]; ok && v == false { - node.AddProperty("repeat", false, "") - } - if v, ok := bind.Options["cooldown-ms"]; ok { - switch val := v.(type) { - case int: - node.AddProperty("cooldown-ms", val, "") - case string: - if ms, err := strconv.Atoi(val); err == nil { - node.AddProperty("cooldown-ms", ms, "") - } - } - } - if v, ok := bind.Options["allow-when-locked"]; ok && v == true { - node.AddProperty("allow-when-locked", true, "") - } - if v, ok := bind.Options["allow-inhibiting"]; ok && v == false { - node.AddProperty("allow-inhibiting", false, "") - } - } - - if bind.Description != "" { - node.AddProperty("hotkey-overlay-title", bind.Description, "") - } - - actionNode := n.buildActionNode(bind.Action) - node.AddNode(actionNode) - - return node -} - -func (n *NiriProvider) buildActionNode(action string) *document.Node { - action = strings.TrimSpace(action) - node := document.NewNode() - - parts := n.parseActionParts(action) - if len(parts) == 0 { - node.SetName(action) - return node - } - - node.SetName(parts[0]) - for _, arg := range parts[1:] { - if strings.Contains(arg, "=") { - kv := strings.SplitN(arg, "=", 2) - switch kv[1] { - case "true": - node.AddProperty(kv[0], true, "") - case "false": - node.AddProperty(kv[0], false, "") - default: - node.AddProperty(kv[0], kv[1], "") - } - continue - } - node.AddArgument(arg, "") - } - return node -} - -func (n *NiriProvider) parseActionParts(action string) []string { - var parts []string - var current strings.Builder - var inQuote, escaped, wasQuoted bool - - for _, r := range action { - switch { - case escaped: - current.WriteRune(r) - escaped = false - case r == '\\': - escaped = true - case r == '"': - wasQuoted = true - inQuote = !inQuote - case r == ' ' && !inQuote: - if current.Len() > 0 || wasQuoted { - parts = append(parts, current.String()) - current.Reset() - wasQuoted = false - } - default: - current.WriteRune(r) - } - } - if current.Len() > 0 || wasQuoted { - parts = append(parts, current.String()) - } - return parts -} - -func (n *NiriProvider) writeOverrideBinds(binds map[string]*overrideBind) error { - overridePath := n.GetOverridePath() - content := n.generateBindsContent(binds) - - if err := n.validateBindsContent(content); err != nil { - return err - } - - return os.WriteFile(overridePath, []byte(content), 0o644) -} - -func (n *NiriProvider) getBindSortPriority(action string) int { - switch { - case strings.HasPrefix(action, "spawn") && strings.Contains(action, "dms"): - return 0 - case strings.Contains(action, "workspace"): - return 1 - case strings.Contains(action, "window") || strings.Contains(action, "column") || - strings.Contains(action, "focus") || strings.Contains(action, "move") || - strings.Contains(action, "swap") || strings.Contains(action, "resize"): - return 2 - case strings.HasPrefix(action, "focus-monitor") || strings.Contains(action, "monitor"): - return 3 - case strings.Contains(action, "screenshot"): - return 4 - case action == "quit" || action == "power-off-monitors" || strings.Contains(action, "dpms"): - return 5 - case strings.HasPrefix(action, "spawn"): - return 6 - default: - return 7 - } -} - -func (n *NiriProvider) generateBindsContent(binds map[string]*overrideBind) string { - if len(binds) == 0 { - return "binds {}\n" - } - - var regularBinds, recentWindowsBinds []*overrideBind - for _, bind := range binds { - switch { - case n.isRecentWindowsAction(bind.Action): - recentWindowsBinds = append(recentWindowsBinds, bind) - default: - regularBinds = append(regularBinds, bind) - } - } - - sort.Slice(regularBinds, func(i, j int) bool { - pi, pj := n.getBindSortPriority(regularBinds[i].Action), n.getBindSortPriority(regularBinds[j].Action) - if pi != pj { - return pi < pj - } - return regularBinds[i].Key < regularBinds[j].Key - }) - - sort.Slice(recentWindowsBinds, func(i, j int) bool { - return recentWindowsBinds[i].Key < recentWindowsBinds[j].Key - }) - - var sb strings.Builder - - sb.WriteString("binds {\n") - for _, bind := range regularBinds { - n.writeBindNode(&sb, bind, " ") - } - sb.WriteString("}\n") - - if len(recentWindowsBinds) > 0 { - sb.WriteString("\nrecent-windows {\n") - sb.WriteString(" binds {\n") - for _, bind := range recentWindowsBinds { - n.writeBindNode(&sb, bind, " ") - } - sb.WriteString(" }\n") - sb.WriteString("}\n") - } - - return sb.String() -} - -func (n *NiriProvider) writeBindNode(sb *strings.Builder, bind *overrideBind, indent string) { - node := n.buildBindNode(bind) - - sb.WriteString(indent) - sb.WriteString(node.Name.String()) - - if node.Properties.Exist() { - sb.WriteString(" ") - sb.WriteString(strings.TrimLeft(node.Properties.String(), " ")) - } - - sb.WriteString(" { ") - if len(node.Children) > 0 { - child := node.Children[0] - actionName := child.Name.String() - sb.WriteString(actionName) - forceQuote := actionName == "spawn" - for _, arg := range child.Arguments { - sb.WriteString(" ") - n.writeArg(sb, arg.ValueString(), forceQuote) - } - if child.Properties.Exist() { - sb.WriteString(" ") - sb.WriteString(strings.TrimLeft(child.Properties.String(), " ")) - } - } - sb.WriteString("; }\n") -} - -func (n *NiriProvider) writeArg(sb *strings.Builder, val string, forceQuote bool) { - if !forceQuote && n.isNumericArg(val) { - sb.WriteString(val) - return - } - sb.WriteString("\"") - sb.WriteString(strings.ReplaceAll(val, "\"", "\\\"")) - sb.WriteString("\"") -} - -func (n *NiriProvider) isNumericArg(val string) bool { - if val == "" { - return false - } - start := 0 - if val[0] == '-' || val[0] == '+' { - if len(val) == 1 { - return false - } - start = 1 - } - for i := start; i < len(val); i++ { - if val[i] < '0' || val[i] > '9' { - return false - } - } - return true -} - -func (n *NiriProvider) validateBindsContent(content string) error { - tmpFile, err := os.CreateTemp("", "dms-binds-*.kdl") - if err != nil { - return fmt.Errorf("failed to create temp file: %w", err) - } - defer os.Remove(tmpFile.Name()) - - if _, err := tmpFile.WriteString(content); err != nil { - tmpFile.Close() - return fmt.Errorf("failed to write temp file: %w", err) - } - tmpFile.Close() - - cmd := exec.Command("niri", "validate", "-c", tmpFile.Name()) - output, err := cmd.CombinedOutput() - if err != nil { - return fmt.Errorf("invalid config: %s", strings.TrimSpace(string(output))) - } - - return nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_parser.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_parser.go deleted file mode 100644 index 46520ff..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_parser.go +++ /dev/null @@ -1,400 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/sblinch/kdl-go" - "github.com/sblinch/kdl-go/document" -) - -type NiriKeyBinding struct { - Mods []string - Key string - Action string - Args []string - Description string - HideOnOverlay bool - CooldownMs int - AllowWhenLocked bool - AllowInhibiting *bool - Repeat *bool - Source string -} - -type NiriSection struct { - Name string - Keybinds []NiriKeyBinding - Children []NiriSection -} - -type NiriParser struct { - configDir string - processedFiles map[string]bool - bindMap map[string]*NiriKeyBinding - bindOrder []string - currentSource string - dmsBindsIncluded bool - dmsBindsExists bool - includeCount int - dmsIncludePos int - bindsBeforeDMS int - bindsAfterDMS int - dmsBindKeys map[string]bool - configBindKeys map[string]bool - dmsProcessed bool - dmsBindMap map[string]*NiriKeyBinding - conflictingConfigs map[string]*NiriKeyBinding -} - -func NewNiriParser(configDir string) *NiriParser { - return &NiriParser{ - configDir: configDir, - processedFiles: make(map[string]bool), - bindMap: make(map[string]*NiriKeyBinding), - bindOrder: []string{}, - currentSource: "", - dmsIncludePos: -1, - dmsBindKeys: make(map[string]bool), - configBindKeys: make(map[string]bool), - dmsBindMap: make(map[string]*NiriKeyBinding), - conflictingConfigs: make(map[string]*NiriKeyBinding), - } -} - -func (p *NiriParser) Parse() (*NiriSection, error) { - dmsBindsPath := filepath.Join(p.configDir, "dms", "binds.kdl") - if _, err := os.Stat(dmsBindsPath); err == nil { - p.dmsBindsExists = true - } - - configPath := filepath.Join(p.configDir, "config.kdl") - section, err := p.parseFile(configPath, "") - if err != nil { - return nil, err - } - - if p.dmsBindsExists && !p.dmsProcessed { - p.parseDMSBindsDirectly(dmsBindsPath, section) - } - - section.Keybinds = p.finalizeBinds() - return section, nil -} - -func (p *NiriParser) parseDMSBindsDirectly(dmsBindsPath string, section *NiriSection) { - data, err := os.ReadFile(dmsBindsPath) - if err != nil { - return - } - - doc, err := kdl.Parse(strings.NewReader(string(data))) - if err != nil { - return - } - - prevSource := p.currentSource - p.currentSource = dmsBindsPath - baseDir := filepath.Dir(dmsBindsPath) - p.processNodes(doc.Nodes, section, baseDir) - p.currentSource = prevSource - p.dmsProcessed = true -} - -func (p *NiriParser) finalizeBinds() []NiriKeyBinding { - binds := make([]NiriKeyBinding, 0, len(p.bindOrder)) - for _, key := range p.bindOrder { - if kb, ok := p.bindMap[key]; ok { - binds = append(binds, *kb) - } - } - return binds -} - -func (p *NiriParser) addBind(kb *NiriKeyBinding) { - key := p.formatBindKey(kb) - isDMSBind := strings.Contains(kb.Source, "dms/binds.kdl") - - if isDMSBind { - p.dmsBindKeys[key] = true - p.dmsBindMap[key] = kb - } else if p.dmsBindKeys[key] { - p.bindsAfterDMS++ - p.conflictingConfigs[key] = kb - p.configBindKeys[key] = true - return - } else { - p.configBindKeys[key] = true - } - - if _, exists := p.bindMap[key]; !exists { - p.bindOrder = append(p.bindOrder, key) - } - p.bindMap[key] = kb -} - -func (p *NiriParser) formatBindKey(kb *NiriKeyBinding) string { - parts := make([]string, 0, len(kb.Mods)+1) - parts = append(parts, kb.Mods...) - parts = append(parts, kb.Key) - return strings.Join(parts, "+") -} - -func (p *NiriParser) parseFile(filePath, sectionName string) (*NiriSection, error) { - absPath, err := filepath.Abs(filePath) - if err != nil { - return nil, fmt.Errorf("failed to resolve path %s: %w", filePath, err) - } - - if p.processedFiles[absPath] { - return &NiriSection{Name: sectionName}, nil - } - p.processedFiles[absPath] = true - - data, err := os.ReadFile(absPath) - if err != nil { - return nil, fmt.Errorf("failed to read %s: %w", absPath, err) - } - - doc, err := kdl.Parse(strings.NewReader(string(data))) - if err != nil { - return nil, fmt.Errorf("failed to parse KDL in %s: %w", absPath, err) - } - - section := &NiriSection{ - Name: sectionName, - } - - prevSource := p.currentSource - p.currentSource = absPath - baseDir := filepath.Dir(absPath) - p.processNodes(doc.Nodes, section, baseDir) - p.currentSource = prevSource - - return section, nil -} - -func (p *NiriParser) processNodes(nodes []*document.Node, section *NiriSection, baseDir string) { - for _, node := range nodes { - name := node.Name.String() - - switch name { - case "include": - p.handleInclude(node, section, baseDir) - case "binds": - p.extractBinds(node, section, "") - case "recent-windows": - p.handleRecentWindows(node, section) - } - } -} - -func (p *NiriParser) handleInclude(node *document.Node, section *NiriSection, baseDir string) { - if len(node.Arguments) == 0 { - return - } - - includePath := strings.Trim(node.Arguments[0].String(), "\"") - isDMSInclude := includePath == "dms/binds.kdl" || strings.HasSuffix(includePath, "/dms/binds.kdl") - - p.includeCount++ - if isDMSInclude { - p.dmsBindsIncluded = true - p.dmsIncludePos = p.includeCount - p.bindsBeforeDMS = len(p.bindMap) - } - - fullPath := filepath.Join(baseDir, includePath) - if filepath.IsAbs(includePath) { - fullPath = includePath - } - - if isDMSInclude { - p.dmsProcessed = true - } - - includedSection, err := p.parseFile(fullPath, "") - if err != nil { - return - } - - section.Children = append(section.Children, includedSection.Children...) -} - -func (p *NiriParser) HasDMSBindsIncluded() bool { - return p.dmsBindsIncluded -} - -func (p *NiriParser) handleRecentWindows(node *document.Node, section *NiriSection) { - if node.Children == nil { - return - } - - for _, child := range node.Children { - if child.Name.String() != "binds" { - continue - } - p.extractBinds(child, section, "Alt-Tab") - } -} - -func (p *NiriParser) extractBinds(node *document.Node, section *NiriSection, subcategory string) { - if node.Children == nil { - return - } - - for _, child := range node.Children { - kb := p.parseKeybindNode(child, subcategory) - if kb == nil { - continue - } - p.addBind(kb) - } -} - -func (p *NiriParser) parseKeybindNode(node *document.Node, _ string) *NiriKeyBinding { - keyCombo := node.Name.String() - if keyCombo == "" { - return nil - } - - mods, key := p.parseKeyCombo(keyCombo) - - var action string - var args []string - if len(node.Children) > 0 { - actionNode := node.Children[0] - action = actionNode.Name.String() - for _, arg := range actionNode.Arguments { - args = append(args, arg.ValueString()) - } - if actionNode.Properties != nil { - for _, propName := range []string{"focus", "show-pointer", "write-to-disk", "skip-confirmation", "delay-ms"} { - if val, ok := actionNode.Properties.Get(propName); ok { - args = append(args, propName+"="+val.String()) - } - } - } - } - - var description string - var hideOnOverlay bool - var cooldownMs int - var allowWhenLocked bool - var allowInhibiting *bool - var repeat *bool - if node.Properties != nil { - if val, ok := node.Properties.Get("hotkey-overlay-title"); ok { - switch val.ValueString() { - case "null", "": - hideOnOverlay = true - default: - description = val.ValueString() - } - } - if val, ok := node.Properties.Get("cooldown-ms"); ok { - cooldownMs, _ = strconv.Atoi(val.String()) - } - if val, ok := node.Properties.Get("allow-when-locked"); ok { - allowWhenLocked = val.String() == "true" - } - if val, ok := node.Properties.Get("allow-inhibiting"); ok { - v := val.String() == "true" - allowInhibiting = &v - } - if val, ok := node.Properties.Get("repeat"); ok { - v := val.String() == "true" - repeat = &v - } - } - - return &NiriKeyBinding{ - Mods: mods, - Key: key, - Action: action, - Args: args, - Description: description, - HideOnOverlay: hideOnOverlay, - CooldownMs: cooldownMs, - AllowWhenLocked: allowWhenLocked, - AllowInhibiting: allowInhibiting, - Repeat: repeat, - Source: p.currentSource, - } -} - -func (p *NiriParser) parseKeyCombo(combo string) ([]string, string) { - parts := strings.Split(combo, "+") - - switch len(parts) { - case 0: - return nil, combo - case 1: - return nil, parts[0] - default: - return parts[:len(parts)-1], parts[len(parts)-1] - } -} - -type NiriParseResult struct { - Section *NiriSection - DMSBindsIncluded bool - DMSStatus *DMSBindsStatusInfo - ConflictingConfigs map[string]*NiriKeyBinding -} - -type DMSBindsStatusInfo struct { - Exists bool - Included bool - IncludePosition int - TotalIncludes int - BindsAfterDMS int - Effective bool - OverriddenBy int - StatusMessage string -} - -func (p *NiriParser) buildDMSStatus() *DMSBindsStatusInfo { - status := &DMSBindsStatusInfo{ - Exists: p.dmsBindsExists, - Included: p.dmsBindsIncluded, - IncludePosition: p.dmsIncludePos, - TotalIncludes: p.includeCount, - BindsAfterDMS: p.bindsAfterDMS, - } - - switch { - case !p.dmsBindsExists: - status.Effective = false - status.StatusMessage = "dms/binds.kdl does not exist" - case !p.dmsBindsIncluded: - status.Effective = false - status.StatusMessage = "dms/binds.kdl is not included in config.kdl" - case p.bindsAfterDMS > 0: - status.Effective = true - status.OverriddenBy = p.bindsAfterDMS - status.StatusMessage = "Some DMS binds may be overridden by config binds" - default: - status.Effective = true - status.StatusMessage = "DMS binds are active" - } - - return status -} - -func ParseNiriKeys(configDir string) (*NiriParseResult, error) { - parser := NewNiriParser(configDir) - section, err := parser.Parse() - if err != nil { - return nil, err - } - return &NiriParseResult{ - Section: section, - DMSBindsIncluded: parser.HasDMSBindsIncluded(), - DMSStatus: parser.buildDMSStatus(), - ConflictingConfigs: parser.conflictingConfigs, - }, nil -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_parser_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_parser_test.go deleted file mode 100644 index e21fde5..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_parser_test.go +++ /dev/null @@ -1,630 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestNiriParseKeyCombo(t *testing.T) { - tests := []struct { - combo string - expectedMods []string - expectedKey string - }{ - {"Mod+Q", []string{"Mod"}, "Q"}, - {"Mod+Shift+F", []string{"Mod", "Shift"}, "F"}, - {"Ctrl+Alt+Delete", []string{"Ctrl", "Alt"}, "Delete"}, - {"Print", nil, "Print"}, - {"XF86AudioMute", nil, "XF86AudioMute"}, - {"Super+Tab", []string{"Super"}, "Tab"}, - {"Mod+Shift+Ctrl+H", []string{"Mod", "Shift", "Ctrl"}, "H"}, - } - - parser := NewNiriParser("") - for _, tt := range tests { - t.Run(tt.combo, func(t *testing.T) { - mods, key := parser.parseKeyCombo(tt.combo) - - if len(mods) != len(tt.expectedMods) { - t.Errorf("Mods length = %d, want %d", len(mods), len(tt.expectedMods)) - } else { - for i := range mods { - if mods[i] != tt.expectedMods[i] { - t.Errorf("Mods[%d] = %q, want %q", i, mods[i], tt.expectedMods[i]) - } - } - } - - if key != tt.expectedKey { - t.Errorf("Key = %q, want %q", key, tt.expectedKey) - } - }) - } -} - -func TestNiriParseBasicBinds(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+Q { close-window; } - Mod+F { fullscreen-window; } - Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 3 { - t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds)) - } - - foundClose := false - foundFullscreen := false - foundTerminal := false - - for _, kb := range result.Section.Keybinds { - switch kb.Action { - case "close-window": - foundClose = true - if kb.Key != "Q" || len(kb.Mods) != 1 || kb.Mods[0] != "Mod" { - t.Errorf("close-window keybind mismatch: %+v", kb) - } - case "fullscreen-window": - foundFullscreen = true - case "spawn": - foundTerminal = true - if kb.Description != "Open Terminal" { - t.Errorf("spawn description = %q, want %q", kb.Description, "Open Terminal") - } - if len(kb.Args) != 1 || kb.Args[0] != "kitty" { - t.Errorf("spawn args = %v, want [kitty]", kb.Args) - } - } - } - - if !foundClose { - t.Error("close-window keybind not found") - } - if !foundFullscreen { - t.Error("fullscreen-window keybind not found") - } - if !foundTerminal { - t.Error("spawn keybind not found") - } -} - -func TestNiriParseRecentWindows(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `recent-windows { - binds { - Alt+Tab { next-window scope="output"; } - Alt+Shift+Tab { previous-window scope="output"; } - } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 2 { - t.Errorf("Expected 2 keybinds from recent-windows, got %d", len(result.Section.Keybinds)) - } - - foundNext := false - foundPrev := false - - for _, kb := range result.Section.Keybinds { - switch kb.Action { - case "next-window": - foundNext = true - case "previous-window": - foundPrev = true - } - } - - if !foundNext { - t.Error("next-window keybind not found") - } - if !foundPrev { - t.Error("previous-window keybind not found") - } -} - -func TestNiriParseInclude(t *testing.T) { - tmpDir := t.TempDir() - subDir := filepath.Join(tmpDir, "dms") - if err := os.MkdirAll(subDir, 0o755); err != nil { - t.Fatalf("Failed to create subdir: %v", err) - } - - mainConfig := filepath.Join(tmpDir, "config.kdl") - includeConfig := filepath.Join(subDir, "binds.kdl") - - mainContent := `binds { - Mod+Q { close-window; } -} -include "dms/binds.kdl" -` - includeContent := `binds { - Mod+T hotkey-overlay-title="Terminal" { spawn "kitty"; } -} -` - - if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil { - t.Fatalf("Failed to write main config: %v", err) - } - if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil { - t.Fatalf("Failed to write include config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 2 { - t.Errorf("Expected 2 keybinds (1 main + 1 include), got %d", len(result.Section.Keybinds)) - } -} - -func TestNiriParseIncludeOverride(t *testing.T) { - tmpDir := t.TempDir() - subDir := filepath.Join(tmpDir, "dms") - if err := os.MkdirAll(subDir, 0o755); err != nil { - t.Fatalf("Failed to create subdir: %v", err) - } - - mainConfig := filepath.Join(tmpDir, "config.kdl") - includeConfig := filepath.Join(subDir, "binds.kdl") - - mainContent := `binds { - Mod+T hotkey-overlay-title="Main Terminal" { spawn "alacritty"; } -} -include "dms/binds.kdl" -` - includeContent := `binds { - Mod+T hotkey-overlay-title="Override Terminal" { spawn "kitty"; } -} -` - - if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil { - t.Fatalf("Failed to write main config: %v", err) - } - if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil { - t.Fatalf("Failed to write include config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 1 { - t.Errorf("Expected 1 keybind (later overrides earlier), got %d", len(result.Section.Keybinds)) - } - - if len(result.Section.Keybinds) > 0 { - kb := result.Section.Keybinds[0] - if kb.Description != "Override Terminal" { - t.Errorf("Expected description 'Override Terminal' (from include), got %q", kb.Description) - } - if len(kb.Args) != 1 || kb.Args[0] != "kitty" { - t.Errorf("Expected args [kitty] (from include), got %v", kb.Args) - } - } -} - -func TestNiriParseCircularInclude(t *testing.T) { - tmpDir := t.TempDir() - - mainConfig := filepath.Join(tmpDir, "config.kdl") - otherConfig := filepath.Join(tmpDir, "other.kdl") - - mainContent := `binds { - Mod+Q { close-window; } -} -include "other.kdl" -` - otherContent := `binds { - Mod+T { spawn "kitty"; } -} -include "config.kdl" -` - - if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil { - t.Fatalf("Failed to write main config: %v", err) - } - if err := os.WriteFile(otherConfig, []byte(otherContent), 0o644); err != nil { - t.Fatalf("Failed to write other config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed (should handle circular includes): %v", err) - } - - if len(result.Section.Keybinds) != 2 { - t.Errorf("Expected 2 keybinds (circular include handled), got %d", len(result.Section.Keybinds)) - } -} - -func TestNiriParseMissingInclude(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+Q { close-window; } -} -include "nonexistent/file.kdl" -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed (should skip missing include): %v", err) - } - - if len(result.Section.Keybinds) != 1 { - t.Errorf("Expected 1 keybind (missing include skipped), got %d", len(result.Section.Keybinds)) - } -} - -func TestNiriParseNoBinds(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `cursor { - xcursor-theme "Bibata" - xcursor-size 24 -} - -input { - keyboard { - numlock - } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 0 { - t.Errorf("Expected 0 keybinds, got %d", len(result.Section.Keybinds)) - } -} - -func TestNiriParseErrors(t *testing.T) { - tests := []struct { - name string - path string - }{ - { - name: "nonexistent_directory", - path: "/nonexistent/path/that/does/not/exist", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, err := ParseNiriKeys(tt.path) - if err == nil { - t.Error("Expected error, got nil") - } - }) - } -} - -func TestNiriBindOverrideBehavior(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+T hotkey-overlay-title="First" { spawn "first"; } - Mod+Q { close-window; } - Mod+T hotkey-overlay-title="Second" { spawn "second"; } - Mod+F { fullscreen-window; } - Mod+T hotkey-overlay-title="Third" { spawn "third"; } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 3 { - t.Fatalf("Expected 3 unique keybinds, got %d", len(result.Section.Keybinds)) - } - - var modT *NiriKeyBinding - for i := range result.Section.Keybinds { - kb := &result.Section.Keybinds[i] - if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" && kb.Key == "T" { - modT = kb - break - } - } - - if modT == nil { - t.Fatal("Mod+T keybind not found") - } - - if modT.Description != "Third" { - t.Errorf("Mod+T description = %q, want 'Third' (last definition wins)", modT.Description) - } - - if len(modT.Args) != 1 || modT.Args[0] != "third" { - t.Errorf("Mod+T args = %v, want [third] (last definition wins)", modT.Args) - } -} - -func TestNiriBindOverrideWithIncludes(t *testing.T) { - tmpDir := t.TempDir() - subDir := filepath.Join(tmpDir, "custom") - if err := os.MkdirAll(subDir, 0o755); err != nil { - t.Fatalf("Failed to create subdir: %v", err) - } - - mainConfig := filepath.Join(tmpDir, "config.kdl") - includeConfig := filepath.Join(subDir, "overrides.kdl") - - mainContent := `binds { - Mod+1 { focus-workspace 1; } - Mod+2 { focus-workspace 2; } - Mod+T hotkey-overlay-title="Default Terminal" { spawn "xterm"; } -} -include "custom/overrides.kdl" -binds { - Mod+3 { focus-workspace 3; } -} -` - includeContent := `binds { - Mod+T hotkey-overlay-title="Custom Terminal" { spawn "kitty"; } - Mod+2 { focus-workspace 22; } -} -` - - if err := os.WriteFile(mainConfig, []byte(mainContent), 0o644); err != nil { - t.Fatalf("Failed to write main config: %v", err) - } - if err := os.WriteFile(includeConfig, []byte(includeContent), 0o644); err != nil { - t.Fatalf("Failed to write include config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 4 { - t.Errorf("Expected 4 unique keybinds, got %d", len(result.Section.Keybinds)) - } - - bindMap := make(map[string]*NiriKeyBinding) - for i := range result.Section.Keybinds { - kb := &result.Section.Keybinds[i] - key := "" - for _, m := range kb.Mods { - key += m + "+" - } - key += kb.Key - bindMap[key] = kb - } - - if kb, ok := bindMap["Mod+T"]; ok { - if kb.Description != "Custom Terminal" { - t.Errorf("Mod+T should be overridden by include, got description %q", kb.Description) - } - } else { - t.Error("Mod+T not found") - } - - if kb, ok := bindMap["Mod+2"]; ok { - if len(kb.Args) != 1 || kb.Args[0] != "22" { - t.Errorf("Mod+2 should be overridden by include with workspace 22, got args %v", kb.Args) - } - } else { - t.Error("Mod+2 not found") - } - - if _, ok := bindMap["Mod+1"]; !ok { - t.Error("Mod+1 should exist (not overridden)") - } - - if _, ok := bindMap["Mod+3"]; !ok { - t.Error("Mod+3 should exist (added after include)") - } -} - -func TestNiriParseMultipleArgs(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+Space hotkey-overlay-title="Application Launcher" { - spawn "dms" "ipc" "call" "spotlight" "toggle"; - } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 1 { - t.Fatalf("Expected 1 keybind, got %d", len(result.Section.Keybinds)) - } - - kb := result.Section.Keybinds[0] - if len(kb.Args) != 5 { - t.Errorf("Expected 5 args, got %d: %v", len(kb.Args), kb.Args) - } - - expectedArgs := []string{"dms", "ipc", "call", "spotlight", "toggle"} - for i, arg := range expectedArgs { - if i < len(kb.Args) && kb.Args[i] != arg { - t.Errorf("Args[%d] = %q, want %q", i, kb.Args[i], arg) - } - } -} - -func TestNiriParseNumericWorkspaceBinds(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; } - Mod+2 hotkey-overlay-title="Focus Workspace 2" { focus-workspace 2; } - Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; } - Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1; } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 4 { - t.Errorf("Expected 4 keybinds, got %d", len(result.Section.Keybinds)) - } - - for _, kb := range result.Section.Keybinds { - switch kb.Key { - case "1": - if len(kb.Mods) == 1 && kb.Mods[0] == "Mod" { - if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "1" { - t.Errorf("Mod+1 action/args mismatch: %+v", kb) - } - if kb.Description != "Focus Workspace 1" { - t.Errorf("Mod+1 description = %q, want 'Focus Workspace 1'", kb.Description) - } - } - case "0": - if kb.Action != "focus-workspace" || len(kb.Args) != 1 || kb.Args[0] != "10" { - t.Errorf("Mod+0 action/args mismatch: %+v", kb) - } - } - } -} - -func TestNiriParseQuotedStringArgs(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; } - Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; } - Super+Shift+Minus hotkey-overlay-title="Adjust Window Height -10%" { set-window-height "-10%"; } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 3 { - t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds)) - } - - for _, kb := range result.Section.Keybinds { - if kb.Action == "set-column-width" { - if len(kb.Args) != 1 { - t.Errorf("set-column-width should have 1 arg, got %d", len(kb.Args)) - continue - } - if kb.Args[0] != "-10%" && kb.Args[0] != "+10%" { - t.Errorf("set-column-width arg = %q, want -10%% or +10%%", kb.Args[0]) - } - } - } -} - -func TestNiriParseActionWithProperties(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+Shift+1 hotkey-overlay-title="Move to Workspace 1" { move-column-to-workspace 1 focus=false; } - Mod+Shift+2 hotkey-overlay-title="Move to Workspace 2" { move-column-to-workspace 2 focus=false; } - Alt+Tab { next-window scope="output"; } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("ParseNiriKeys failed: %v", err) - } - - if len(result.Section.Keybinds) != 3 { - t.Errorf("Expected 3 keybinds, got %d", len(result.Section.Keybinds)) - } - - for _, kb := range result.Section.Keybinds { - switch kb.Action { - case "move-column-to-workspace": - if len(kb.Args) != 2 { - t.Errorf("move-column-to-workspace should have 2 args (index + focus), got %d", len(kb.Args)) - } - hasIndex := false - hasFocus := false - for _, arg := range kb.Args { - if arg == "1" || arg == "2" { - hasIndex = true - } - if arg == "focus=false" { - hasFocus = true - } - } - if !hasIndex { - t.Errorf("move-column-to-workspace missing index arg") - } - if !hasFocus { - t.Errorf("move-column-to-workspace missing focus=false arg") - } - case "next-window": - if kb.Key != "Tab" { - t.Errorf("next-window key = %q, want 'Tab'", kb.Key) - } - } - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_test.go deleted file mode 100644 index b04c5b2..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/niri_test.go +++ /dev/null @@ -1,661 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestNiriProviderName(t *testing.T) { - provider := NewNiriProvider("") - if provider.Name() != "niri" { - t.Errorf("Name() = %q, want %q", provider.Name(), "niri") - } -} - -func TestNiriProviderGetCheatSheet(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+Q { close-window; } - Mod+F { fullscreen-window; } - Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } - Mod+1 { focus-workspace 1; } - Mod+Shift+1 { move-column-to-workspace 1; } - Print { screenshot; } - Mod+Shift+E { quit; } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - provider := NewNiriProvider(tmpDir) - cheatSheet, err := provider.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - if cheatSheet.Title != "Niri Keybinds" { - t.Errorf("Title = %q, want %q", cheatSheet.Title, "Niri Keybinds") - } - - if cheatSheet.Provider != "niri" { - t.Errorf("Provider = %q, want %q", cheatSheet.Provider, "niri") - } - - windowBinds := cheatSheet.Binds["Window"] - if len(windowBinds) < 2 { - t.Errorf("Expected at least 2 Window binds, got %d", len(windowBinds)) - } - - execBinds := cheatSheet.Binds["Execute"] - if len(execBinds) < 1 { - t.Errorf("Expected at least 1 Execute bind, got %d", len(execBinds)) - } - - workspaceBinds := cheatSheet.Binds["Workspace"] - if len(workspaceBinds) < 2 { - t.Errorf("Expected at least 2 Workspace binds, got %d", len(workspaceBinds)) - } - - screenshotBinds := cheatSheet.Binds["Screenshot"] - if len(screenshotBinds) < 1 { - t.Errorf("Expected at least 1 Screenshot bind, got %d", len(screenshotBinds)) - } - - systemBinds := cheatSheet.Binds["System"] - if len(systemBinds) < 1 { - t.Errorf("Expected at least 1 System bind, got %d", len(systemBinds)) - } -} - -func TestNiriCategorizeByAction(t *testing.T) { - provider := NewNiriProvider("") - - tests := []struct { - action string - expected string - }{ - {"focus-workspace", "Workspace"}, - {"focus-workspace-up", "Workspace"}, - {"move-column-to-workspace", "Workspace"}, - {"focus-monitor-left", "Monitor"}, - {"move-column-to-monitor-right", "Monitor"}, - {"close-window", "Window"}, - {"fullscreen-window", "Window"}, - {"maximize-column", "Window"}, - {"toggle-window-floating", "Window"}, - {"focus-column-left", "Window"}, - {"move-column-right", "Window"}, - {"spawn", "Execute"}, - {"quit", "System"}, - {"power-off-monitors", "System"}, - {"screenshot", "Screenshot"}, - {"screenshot-window", "Screenshot"}, - {"toggle-overview", "Overview"}, - {"show-hotkey-overlay", "Overview"}, - {"next-window", "Alt-Tab"}, - {"previous-window", "Alt-Tab"}, - {"unknown-action", "Other"}, - } - - for _, tt := range tests { - t.Run(tt.action, func(t *testing.T) { - result := provider.categorizeByAction(tt.action) - if result != tt.expected { - t.Errorf("categorizeByAction(%q) = %q, want %q", tt.action, result, tt.expected) - } - }) - } -} - -func TestNiriFormatRawAction(t *testing.T) { - provider := NewNiriProvider("") - - tests := []struct { - action string - args []string - expected string - }{ - {"spawn", []string{"kitty"}, "spawn kitty"}, - {"spawn", []string{"dms", "ipc", "call"}, "spawn dms ipc call"}, - {"spawn", []string{"dms", "ipc", "call", "brightness", "increment", "5", ""}, `spawn dms ipc call brightness increment 5 ""`}, - {"spawn", []string{"dms", "ipc", "call", "dash", "toggle", ""}, `spawn dms ipc call dash toggle ""`}, - {"close-window", nil, "close-window"}, - {"fullscreen-window", nil, "fullscreen-window"}, - {"focus-workspace", []string{"1"}, "focus-workspace 1"}, - {"move-column-to-workspace", []string{"5"}, "move-column-to-workspace 5"}, - {"set-column-width", []string{"+10%"}, "set-column-width +10%"}, - } - - for _, tt := range tests { - t.Run(tt.action, func(t *testing.T) { - result := provider.formatRawAction(tt.action, tt.args) - if result != tt.expected { - t.Errorf("formatRawAction(%q, %v) = %q, want %q", tt.action, tt.args, result, tt.expected) - } - }) - } -} - -func TestNiriFormatKey(t *testing.T) { - provider := NewNiriProvider("") - - tests := []struct { - mods []string - key string - expected string - }{ - {[]string{"Mod"}, "Q", "Mod+Q"}, - {[]string{"Mod", "Shift"}, "F", "Mod+Shift+F"}, - {[]string{"Ctrl", "Alt"}, "Delete", "Ctrl+Alt+Delete"}, - {nil, "Print", "Print"}, - {[]string{}, "XF86AudioMute", "XF86AudioMute"}, - } - - for _, tt := range tests { - t.Run(tt.expected, func(t *testing.T) { - kb := &NiriKeyBinding{ - Mods: tt.mods, - Key: tt.key, - } - result := provider.formatKey(kb) - if result != tt.expected { - t.Errorf("formatKey(%v) = %q, want %q", kb, result, tt.expected) - } - }) - } -} - -func TestNiriDefaultConfigDir(t *testing.T) { - originalXDG := os.Getenv("XDG_CONFIG_HOME") - defer os.Setenv("XDG_CONFIG_HOME", originalXDG) - - os.Setenv("XDG_CONFIG_HOME", "/custom/config") - dir := defaultNiriConfigDir() - if dir != "/custom/config/niri" { - t.Errorf("With XDG_CONFIG_HOME set, got %q, want %q", dir, "/custom/config/niri") - } - - os.Unsetenv("XDG_CONFIG_HOME") - dir = defaultNiriConfigDir() - home, _ := os.UserHomeDir() - expected := filepath.Join(home, ".config", "niri") - if dir != expected { - t.Errorf("Without XDG_CONFIG_HOME, got %q, want %q", dir, expected) - } -} - -func TestNiriGenerateBindsContent(t *testing.T) { - provider := NewNiriProvider("") - - tests := []struct { - name string - binds map[string]*overrideBind - expected string - }{ - { - name: "empty binds", - binds: map[string]*overrideBind{}, - expected: "binds {}\n", - }, - { - name: "simple spawn bind", - binds: map[string]*overrideBind{ - "Mod+T": { - Key: "Mod+T", - Action: "spawn kitty", - Description: "Open Terminal", - }, - }, - expected: `binds { - Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } -} -`, - }, - { - name: "spawn with multiple args", - binds: map[string]*overrideBind{ - "Mod+Space": { - Key: "Mod+Space", - Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`, - Description: "Application Launcher", - }, - }, - expected: `binds { - Mod+Space hotkey-overlay-title="Application Launcher" { spawn "dms" "ipc" "call" "spotlight" "toggle"; } -} -`, - }, - { - name: "bind with allow-when-locked", - binds: map[string]*overrideBind{ - "XF86AudioMute": { - Key: "XF86AudioMute", - Action: `spawn "dms" "ipc" "call" "audio" "mute"`, - Options: map[string]any{"allow-when-locked": true}, - }, - }, - expected: `binds { - XF86AudioMute allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "mute"; } -} -`, - }, - { - name: "simple action without args", - binds: map[string]*overrideBind{ - "Mod+Q": { - Key: "Mod+Q", - Action: "close-window", - Description: "Close Window", - }, - }, - expected: `binds { - Mod+Q hotkey-overlay-title="Close Window" { close-window; } -} -`, - }, - { - name: "recent-windows action", - binds: map[string]*overrideBind{ - "Alt+Tab": { - Key: "Alt+Tab", - Action: "next-window", - }, - }, - expected: `binds { -} - -recent-windows { - binds { - Alt+Tab { next-window; } - } -} -`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := provider.generateBindsContent(tt.binds) - if result != tt.expected { - t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected) - } - }) - } -} - -func TestNiriGenerateBindsContentRoundTrip(t *testing.T) { - provider := NewNiriProvider("") - - binds := map[string]*overrideBind{ - "Mod+Space": { - Key: "Mod+Space", - Action: `spawn "dms" "ipc" "call" "spotlight" "toggle"`, - Description: "Application Launcher", - }, - "XF86AudioMute": { - Key: "XF86AudioMute", - Action: `spawn "dms" "ipc" "call" "audio" "mute"`, - Options: map[string]any{"allow-when-locked": true}, - }, - "Mod+Q": { - Key: "Mod+Q", - Action: "close-window", - Description: "Close Window", - }, - } - - content := provider.generateBindsContent(binds) - - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write temp file: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content) - } - - if len(result.Section.Keybinds) != 3 { - t.Errorf("Expected 3 keybinds after round-trip, got %d", len(result.Section.Keybinds)) - } -} - -func TestNiriEmptyArgsPreservation(t *testing.T) { - provider := NewNiriProvider("") - - binds := map[string]*overrideBind{ - "XF86MonBrightnessUp": { - Key: "XF86MonBrightnessUp", - Action: `spawn dms ipc call brightness increment 5 ""`, - Description: "Brightness Up", - }, - "XF86MonBrightnessDown": { - Key: "XF86MonBrightnessDown", - Action: `spawn dms ipc call brightness decrement 5 ""`, - Description: "Brightness Down", - }, - "Super+Alt+Page_Up": { - Key: "Super+Alt+Page_Up", - Action: `spawn dms ipc call dash toggle ""`, - Description: "Dashboard Toggle", - }, - } - - content := provider.generateBindsContent(binds) - - tmpDir := t.TempDir() - dmsDir := filepath.Join(tmpDir, "dms") - if err := os.MkdirAll(dmsDir, 0o755); err != nil { - t.Fatalf("Failed to create dms directory: %v", err) - } - - bindsFile := filepath.Join(dmsDir, "binds.kdl") - if err := os.WriteFile(bindsFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write binds file: %v", err) - } - - testProvider := NewNiriProvider(tmpDir) - loadedBinds, err := testProvider.loadOverrideBinds() - if err != nil { - t.Fatalf("Failed to load binds: %v\nContent was:\n%s", err, content) - } - - for key, expected := range binds { - loaded, ok := loadedBinds[key] - if !ok { - t.Errorf("Missing bind for key %s", key) - continue - } - if loaded.Action != expected.Action { - t.Errorf("Action mismatch for %s:\n got: %q\n want: %q", key, loaded.Action, expected.Action) - } - } -} - -func TestNiriProviderWithRealWorldConfig(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - - content := `binds { - Mod+Shift+Ctrl+D { debug-toggle-damage; } - Super+D { spawn "niri" "msg" "action" "toggle-overview"; } - Super+Tab repeat=false { toggle-overview; } - Mod+Shift+Slash { show-hotkey-overlay; } - - Mod+T hotkey-overlay-title="Open Terminal" { spawn "kitty"; } - Mod+Space hotkey-overlay-title="Application Launcher" { - spawn "dms" "ipc" "call" "spotlight" "toggle"; - } - - XF86AudioRaiseVolume allow-when-locked=true { - spawn "dms" "ipc" "call" "audio" "increment" "3"; - } - XF86AudioLowerVolume allow-when-locked=true { - spawn "dms" "ipc" "call" "audio" "decrement" "3"; - } - - Mod+Q repeat=false { close-window; } - Mod+F { maximize-column; } - Mod+Shift+F { fullscreen-window; } - - Mod+Left { focus-column-left; } - Mod+Down { focus-window-down; } - Mod+Up { focus-window-up; } - Mod+Right { focus-column-right; } - - Mod+1 { focus-workspace 1; } - Mod+2 { focus-workspace 2; } - Mod+Shift+1 { move-column-to-workspace 1; } - Mod+Shift+2 { move-column-to-workspace 2; } - - Print { screenshot; } - Ctrl+Print { screenshot-screen; } - Alt+Print { screenshot-window; } - - Mod+Shift+E { quit; } -} - -recent-windows { - binds { - Alt+Tab { next-window scope="output"; } - Alt+Shift+Tab { previous-window scope="output"; } - } -} -` - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - provider := NewNiriProvider(tmpDir) - cheatSheet, err := provider.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - totalBinds := 0 - for _, binds := range cheatSheet.Binds { - totalBinds += len(binds) - } - - if totalBinds < 20 { - t.Errorf("Expected at least 20 keybinds, got %d", totalBinds) - } - - if len(cheatSheet.Binds["Alt-Tab"]) < 2 { - t.Errorf("Expected at least 2 Alt-Tab binds, got %d", len(cheatSheet.Binds["Alt-Tab"])) - } -} - -func TestNiriGenerateBindsContentNumericArgs(t *testing.T) { - provider := NewNiriProvider("") - - tests := []struct { - name string - binds map[string]*overrideBind - expected string - }{ - { - name: "workspace with numeric arg", - binds: map[string]*overrideBind{ - "Mod+1": { - Key: "Mod+1", - Action: "focus-workspace 1", - Description: "Focus Workspace 1", - }, - }, - expected: `binds { - Mod+1 hotkey-overlay-title="Focus Workspace 1" { focus-workspace 1; } -} -`, - }, - { - name: "workspace with large numeric arg", - binds: map[string]*overrideBind{ - "Mod+0": { - Key: "Mod+0", - Action: "focus-workspace 10", - Description: "Focus Workspace 10", - }, - }, - expected: `binds { - Mod+0 hotkey-overlay-title="Focus Workspace 10" { focus-workspace 10; } -} -`, - }, - { - name: "percentage string arg (should be quoted)", - binds: map[string]*overrideBind{ - "Super+Minus": { - Key: "Super+Minus", - Action: `set-column-width "-10%"`, - Description: "Adjust Column Width -10%", - }, - }, - expected: `binds { - Super+Minus hotkey-overlay-title="Adjust Column Width -10%" { set-column-width "-10%"; } -} -`, - }, - { - name: "positive percentage string arg", - binds: map[string]*overrideBind{ - "Super+Equal": { - Key: "Super+Equal", - Action: `set-column-width "+10%"`, - Description: "Adjust Column Width +10%", - }, - }, - expected: `binds { - Super+Equal hotkey-overlay-title="Adjust Column Width +10%" { set-column-width "+10%"; } -} -`, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := provider.generateBindsContent(tt.binds) - if result != tt.expected { - t.Errorf("generateBindsContent() =\n%q\nwant:\n%q", result, tt.expected) - } - }) - } -} - -func TestNiriGenerateActionWithUnquotedPercentArg(t *testing.T) { - provider := NewNiriProvider("") - - binds := map[string]*overrideBind{ - "Super+Equal": { - Key: "Super+Equal", - Action: "set-window-height +10%", - Description: "Adjust Window Height +10%", - }, - } - - content := provider.generateBindsContent(binds) - expected := `binds { - Super+Equal hotkey-overlay-title="Adjust Window Height +10%" { set-window-height "+10%"; } -} -` - if content != expected { - t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected) - } -} - -func TestNiriGenerateSpawnWithNumericArgs(t *testing.T) { - provider := NewNiriProvider("") - - binds := map[string]*overrideBind{ - "XF86AudioLowerVolume": { - Key: "XF86AudioLowerVolume", - Action: `spawn "dms" "ipc" "call" "audio" "decrement" "3"`, - Options: map[string]any{"allow-when-locked": true}, - }, - } - - content := provider.generateBindsContent(binds) - expected := `binds { - XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; } -} -` - if content != expected { - t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected) - } -} - -func TestNiriGenerateSpawnNumericArgFromCLI(t *testing.T) { - provider := NewNiriProvider("") - - binds := map[string]*overrideBind{ - "XF86AudioLowerVolume": { - Key: "XF86AudioLowerVolume", - Action: "spawn dms ipc call audio decrement 3", - Options: map[string]any{"allow-when-locked": true}, - }, - } - - content := provider.generateBindsContent(binds) - expected := `binds { - XF86AudioLowerVolume allow-when-locked=true { spawn "dms" "ipc" "call" "audio" "decrement" "3"; } -} -` - if content != expected { - t.Errorf("Content mismatch.\nGot:\n%s\nWant:\n%s", content, expected) - } -} - -func TestNiriGenerateWorkspaceBindsRoundTrip(t *testing.T) { - provider := NewNiriProvider("") - - binds := map[string]*overrideBind{ - "Mod+1": { - Key: "Mod+1", - Action: "focus-workspace 1", - Description: "Focus Workspace 1", - }, - "Mod+2": { - Key: "Mod+2", - Action: "focus-workspace 2", - Description: "Focus Workspace 2", - }, - "Mod+Shift+1": { - Key: "Mod+Shift+1", - Action: "move-column-to-workspace 1", - Description: "Move to Workspace 1", - }, - "Super+Minus": { - Key: "Super+Minus", - Action: "set-column-width -10%", - Description: "Adjust Column Width -10%", - }, - } - - content := provider.generateBindsContent(binds) - - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config.kdl") - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write temp file: %v", err) - } - - result, err := ParseNiriKeys(tmpDir) - if err != nil { - t.Fatalf("Failed to parse generated content: %v\nContent was:\n%s", err, content) - } - - if len(result.Section.Keybinds) != 4 { - t.Errorf("Expected 4 keybinds after round-trip, got %d", len(result.Section.Keybinds)) - } - - foundFocusWS1 := false - foundMoveWS1 := false - foundSetWidth := false - - for _, kb := range result.Section.Keybinds { - switch { - case kb.Action == "focus-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1": - foundFocusWS1 = true - case kb.Action == "move-column-to-workspace" && len(kb.Args) > 0 && kb.Args[0] == "1": - foundMoveWS1 = true - case kb.Action == "set-column-width" && len(kb.Args) > 0 && kb.Args[0] == "-10%": - foundSetWidth = true - } - } - - if !foundFocusWS1 { - t.Error("focus-workspace 1 not found after round-trip") - } - if !foundMoveWS1 { - t.Error("move-column-to-workspace 1 not found after round-trip") - } - if !foundSetWidth { - t.Error("set-column-width -10% not found after round-trip") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway.go deleted file mode 100644 index 93ceb73..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway.go +++ /dev/null @@ -1,150 +0,0 @@ -package providers - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" -) - -type SwayProvider struct { - configPath string - isScroll bool -} - -func NewSwayProvider(configPath string) *SwayProvider { - isScroll := false - _, scrollEnvSet := os.LookupEnv("SCROLLSOCK") - - if configPath == "" { - configDir, err := os.UserConfigDir() - if err != nil { - configDir = "" - } - if scrollEnvSet { - if configDir != "" { - configPath = filepath.Join(configDir, "scroll") - } - isScroll = true - } else { - if configDir != "" { - configPath = filepath.Join(configDir, "sway") - } - } - } else { - isScroll = strings.Contains(configPath, "scroll") - } - - return &SwayProvider{ - configPath: configPath, - isScroll: isScroll, - } -} - -func (s *SwayProvider) Name() string { - if s == nil { - if os.Getenv("SCROLLSOCK") != "" { - return "scroll" - } - return "sway" - } - - if s.isScroll { - return "scroll" - } - return "sway" -} - -func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) { - section, err := ParseSwayKeys(s.configPath) - if err != nil { - return nil, fmt.Errorf("failed to parse sway config: %w", err) - } - - categorizedBinds := make(map[string][]keybinds.Keybind) - s.convertSection(section, "", categorizedBinds) - - cheatSheetTitle := "Sway Keybinds" - if s != nil && s.isScroll { - cheatSheetTitle = "Scroll Keybinds" - } - - return &keybinds.CheatSheet{ - Title: cheatSheetTitle, - Provider: s.Name(), - Binds: categorizedBinds, - }, nil -} - -func (s *SwayProvider) convertSection(section *SwaySection, subcategory string, categorizedBinds map[string][]keybinds.Keybind) { - currentSubcat := subcategory - if section.Name != "" { - currentSubcat = section.Name - } - - for _, kb := range section.Keybinds { - category := s.categorizeByCommand(kb.Command) - bind := s.convertKeybind(&kb, currentSubcat) - categorizedBinds[category] = append(categorizedBinds[category], bind) - } - - for _, child := range section.Children { - s.convertSection(&child, currentSubcat, categorizedBinds) - } -} - -func (s *SwayProvider) categorizeByCommand(command string) string { - command = strings.ToLower(command) - - switch { - case strings.Contains(command, "scratchpad"): - return "Scratchpad" - case strings.Contains(command, "workspace") && strings.Contains(command, "output"): - return "Monitor" - case strings.Contains(command, "workspace"): - return "Workspace" - case strings.Contains(command, "output"): - return "Monitor" - case strings.Contains(command, "layout"): - return "Layout" - case command == "kill" || - command == "fullscreen" || strings.Contains(command, "fullscreen") || - command == "floating toggle" || strings.Contains(command, "floating") || - strings.Contains(command, "focus") || - strings.Contains(command, "move") || - strings.Contains(command, "resize") || - strings.Contains(command, "split"): - return "Window" - case strings.HasPrefix(command, "exec"): - return "Execute" - case command == "exit" || command == "reload": - return "System" - default: - return "Other" - } -} - -func (s *SwayProvider) convertKeybind(kb *SwayKeyBinding, subcategory string) keybinds.Keybind { - key := s.formatKey(kb) - desc := kb.Comment - - if desc == "" { - desc = kb.Command - } - - return keybinds.Keybind{ - Key: key, - Description: desc, - Action: kb.Command, - Subcategory: subcategory, - } -} - -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) - return strings.Join(parts, "+") -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_parser.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_parser.go deleted file mode 100644 index 647c9f7..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_parser.go +++ /dev/null @@ -1,364 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "regexp" - "strings" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/utils" -) - -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, err := utils.ExpandPath(path) - if err != nil { - return err - } - - 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 -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_parser_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_parser_test.go deleted file mode 100644 index 6bb5ecf..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_parser_test.go +++ /dev/null @@ -1,471 +0,0 @@ -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), 0o644); 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), 0o644); 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, 0o755); 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"), 0o644); 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), 0o644); 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), 0o644); 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) - } - }) - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_test.go deleted file mode 100644 index fd476d8..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/providers/sway_test.go +++ /dev/null @@ -1,293 +0,0 @@ -package providers - -import ( - "os" - "path/filepath" - "testing" -) - -func TestSwayProviderName(t *testing.T) { - provider := NewSwayProvider("") - if provider.Name() != "sway" { - t.Errorf("Name() = %q, want %q", provider.Name(), "sway") - } -} - -func TestSwayProviderDefaultPath(t *testing.T) { - provider := NewSwayProvider("") - configDir, err := os.UserConfigDir() - if err != nil { - t.Skip("UserConfigDir not available") - } - expected := filepath.Join(configDir, "sway") - if provider.configPath != expected { - t.Errorf("configPath = %q, want %q", provider.configPath, expected) - } -} - -func TestSwayProviderCustomPath(t *testing.T) { - customPath := "/custom/path" - provider := NewSwayProvider(customPath) - if provider.configPath != customPath { - t.Errorf("configPath = %q, want %q", provider.configPath, customPath) - } -} - -func TestSwayCategorizeByCommand(t *testing.T) { - tests := []struct { - command string - expected string - }{ - {"workspace number 1", "Workspace"}, - {"workspace prev", "Workspace"}, - {"workspace next", "Workspace"}, - {"move container to workspace number 1", "Workspace"}, - {"focus output left", "Monitor"}, - {"move workspace to output right", "Monitor"}, - {"kill", "Window"}, - {"fullscreen toggle", "Window"}, - {"floating toggle", "Window"}, - {"focus left", "Window"}, - {"focus right", "Window"}, - {"move left", "Window"}, - {"move right", "Window"}, - {"resize grow width 10px", "Window"}, - {"splith", "Window"}, - {"splitv", "Window"}, - {"layout tabbed", "Layout"}, - {"layout stacking", "Layout"}, - {"move scratchpad", "Scratchpad"}, - {"scratchpad show", "Scratchpad"}, - {"exec kitty", "Execute"}, - {"exec --no-startup-id firefox", "Execute"}, - {"exit", "System"}, - {"reload", "System"}, - {"unknown command", "Other"}, - } - - provider := NewSwayProvider("") - for _, tt := range tests { - t.Run(tt.command, func(t *testing.T) { - result := provider.categorizeByCommand(tt.command) - if result != tt.expected { - t.Errorf("categorizeByCommand(%q) = %q, want %q", tt.command, result, tt.expected) - } - }) - } -} - -func TestSwayFormatKey(t *testing.T) { - tests := []struct { - name string - keybind *SwayKeyBinding - expected string - }{ - { - name: "single_mod", - keybind: &SwayKeyBinding{ - Mods: []string{"Mod4"}, - Key: "q", - }, - expected: "Mod4+q", - }, - { - name: "multiple_mods", - keybind: &SwayKeyBinding{ - Mods: []string{"Mod4", "Shift"}, - Key: "e", - }, - expected: "Mod4+Shift+e", - }, - { - name: "no_mods", - keybind: &SwayKeyBinding{ - Mods: []string{}, - Key: "Print", - }, - expected: "Print", - }, - } - - provider := NewSwayProvider("") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := provider.formatKey(tt.keybind) - if result != tt.expected { - t.Errorf("formatKey() = %q, want %q", result, tt.expected) - } - }) - } -} - -func TestSwayConvertKeybind(t *testing.T) { - tests := []struct { - name string - keybind *SwayKeyBinding - wantKey string - wantDesc string - }{ - { - name: "with_comment", - keybind: &SwayKeyBinding{ - Mods: []string{"Mod4"}, - Key: "t", - Command: "exec kitty", - Comment: "Open terminal", - }, - wantKey: "Mod4+t", - wantDesc: "Open terminal", - }, - { - name: "without_comment", - keybind: &SwayKeyBinding{ - Mods: []string{"Mod4"}, - Key: "r", - Command: "reload", - Comment: "", - }, - wantKey: "Mod4+r", - wantDesc: "reload", - }, - } - - provider := NewSwayProvider("") - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := provider.convertKeybind(tt.keybind, "") - if result.Key != tt.wantKey { - t.Errorf("convertKeybind().Key = %q, want %q", result.Key, tt.wantKey) - } - if result.Description != tt.wantDesc { - t.Errorf("convertKeybind().Description = %q, want %q", result.Description, tt.wantDesc) - } - }) - } -} - -func TestSwayGetCheatSheet(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config") - - content := `set $mod Mod4 -set $term kitty - -# System -bindsym $mod+Shift+c reload -bindsym $mod+Shift+e exit - -# Applications -bindsym $mod+t exec $term -bindsym $mod+Space exec rofi - -# Window Management -bindsym $mod+q kill -bindsym $mod+f fullscreen toggle -bindsym $mod+Left focus left -bindsym $mod+Right focus right - -# Workspace -bindsym $mod+1 workspace number 1 -bindsym $mod+2 workspace number 2 -bindsym $mod+Shift+1 move container to workspace number 1 - -# Layout -bindsym $mod+s layout stacking -bindsym $mod+w layout tabbed -` - - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - provider := NewSwayProvider(tmpDir) - sheet, err := provider.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - if sheet == nil { - t.Fatal("Expected non-nil CheatSheet") - } - - if sheet.Title != "Sway Keybinds" { - t.Errorf("Title = %q, want %q", sheet.Title, "Sway Keybinds") - } - - if sheet.Provider != "sway" { - t.Errorf("Provider = %q, want %q", sheet.Provider, "sway") - } - - categories := []string{"System", "Execute", "Window", "Workspace", "Layout"} - for _, category := range categories { - if _, exists := sheet.Binds[category]; !exists { - t.Errorf("Expected category %q to exist", category) - } - } - - if len(sheet.Binds["System"]) < 2 { - t.Error("Expected at least 2 System keybinds") - } - if len(sheet.Binds["Execute"]) < 2 { - t.Error("Expected at least 2 Execute keybinds") - } - if len(sheet.Binds["Window"]) < 4 { - t.Error("Expected at least 4 Window keybinds") - } - if len(sheet.Binds["Workspace"]) < 3 { - t.Error("Expected at least 3 Workspace keybinds") - } -} - -func TestSwayGetCheatSheetError(t *testing.T) { - provider := NewSwayProvider("/nonexistent/path") - _, err := provider.GetCheatSheet() - if err == nil { - t.Error("Expected error for nonexistent path, got nil") - } -} - -func TestSwayIntegration(t *testing.T) { - tmpDir := t.TempDir() - configFile := filepath.Join(tmpDir, "config") - - content := `set $mod Mod4 - -bindsym $mod+t exec kitty # Terminal -bindsym $mod+q kill -bindsym $mod+f fullscreen toggle -bindsym $mod+1 workspace number 1 -` - - if err := os.WriteFile(configFile, []byte(content), 0o644); err != nil { - t.Fatalf("Failed to write test config: %v", err) - } - - provider := NewSwayProvider(tmpDir) - sheet, err := provider.GetCheatSheet() - if err != nil { - t.Fatalf("GetCheatSheet failed: %v", err) - } - - totalBinds := 0 - for _, binds := range sheet.Binds { - totalBinds += len(binds) - } - - expectedBinds := 4 - if totalBinds != expectedBinds { - t.Errorf("Expected %d total keybinds, got %d", expectedBinds, totalBinds) - } - - foundTerminal := false - for _, binds := range sheet.Binds { - for _, bind := range binds { - if bind.Description == "Terminal" && bind.Key == "Mod4+t" { - foundTerminal = true - } - } - } - - if !foundTerminal { - t.Error("Did not find terminal keybind with correct key and description") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/registry.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/registry.go deleted file mode 100644 index d726838..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/registry.go +++ /dev/null @@ -1,79 +0,0 @@ -package keybinds - -import ( - "fmt" - "sync" -) - -type Registry struct { - mu sync.RWMutex - providers map[string]Provider -} - -func NewRegistry() *Registry { - return &Registry{ - providers: make(map[string]Provider), - } -} - -func (r *Registry) Register(provider Provider) error { - if provider == nil { - return fmt.Errorf("cannot register nil provider") - } - - name := provider.Name() - if name == "" { - return fmt.Errorf("provider name cannot be empty") - } - - r.mu.Lock() - defer r.mu.Unlock() - - if _, exists := r.providers[name]; exists { - return fmt.Errorf("provider %q already registered", name) - } - - r.providers[name] = provider - return nil -} - -func (r *Registry) Get(name string) (Provider, error) { - r.mu.RLock() - defer r.mu.RUnlock() - - provider, exists := r.providers[name] - if !exists { - return nil, fmt.Errorf("provider %q not found", name) - } - - return provider, nil -} - -func (r *Registry) List() []string { - r.mu.RLock() - defer r.mu.RUnlock() - - names := make([]string, 0, len(r.providers)) - for name := range r.providers { - names = append(names, name) - } - return names -} - -var defaultRegistry = NewRegistry() - -func GetDefaultRegistry() *Registry { - return defaultRegistry -} - -func Register(provider Provider) error { - return defaultRegistry.Register(provider) -} - -func Get(name string) (Provider, error) { - return defaultRegistry.Get(name) -} - -func List() []string { - return defaultRegistry.List() -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/registry_test.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/registry_test.go deleted file mode 100644 index c3ddfee..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/registry_test.go +++ /dev/null @@ -1,183 +0,0 @@ -package keybinds - -import ( - "testing" -) - -type mockProvider struct { - name string - err error -} - -func (m *mockProvider) Name() string { - return m.name -} - -func (m *mockProvider) GetCheatSheet() (*CheatSheet, error) { - if m.err != nil { - return nil, m.err - } - return &CheatSheet{ - Title: "Test", - Provider: m.name, - Binds: make(map[string][]Keybind), - }, nil -} - -func TestNewRegistry(t *testing.T) { - r := NewRegistry() - if r == nil { - t.Fatal("NewRegistry returned nil") - } - - if r.providers == nil { - t.Error("providers map is nil") - } -} - -func TestRegisterProvider(t *testing.T) { - tests := []struct { - name string - provider Provider - expectError bool - errorMsg string - }{ - { - name: "valid provider", - provider: &mockProvider{name: "test"}, - expectError: false, - }, - { - name: "nil provider", - provider: nil, - expectError: true, - errorMsg: "cannot register nil provider", - }, - { - name: "empty name", - provider: &mockProvider{name: ""}, - expectError: true, - errorMsg: "provider name cannot be empty", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := NewRegistry() - err := r.Register(tt.provider) - - if tt.expectError { - if err == nil { - t.Error("expected error, got nil") - } - return - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - } - }) - } -} - -func TestRegisterDuplicate(t *testing.T) { - r := NewRegistry() - p := &mockProvider{name: "test"} - - if err := r.Register(p); err != nil { - t.Fatalf("first registration failed: %v", err) - } - - err := r.Register(p) - if err == nil { - t.Error("expected error when registering duplicate, got nil") - } -} - -func TestGetProvider(t *testing.T) { - r := NewRegistry() - p := &mockProvider{name: "test"} - - if err := r.Register(p); err != nil { - t.Fatalf("registration failed: %v", err) - } - - retrieved, err := r.Get("test") - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - if retrieved.Name() != "test" { - t.Errorf("Got provider name %q, want %q", retrieved.Name(), "test") - } -} - -func TestGetNonexistent(t *testing.T) { - r := NewRegistry() - - _, err := r.Get("nonexistent") - if err == nil { - t.Error("expected error for nonexistent provider, got nil") - } -} - -func TestListProviders(t *testing.T) { - r := NewRegistry() - - p1 := &mockProvider{name: "test1"} - p2 := &mockProvider{name: "test2"} - p3 := &mockProvider{name: "test3"} - - r.Register(p1) - r.Register(p2) - r.Register(p3) - - list := r.List() - - if len(list) != 3 { - t.Errorf("expected 3 providers, got %d", len(list)) - } - - found := make(map[string]bool) - for _, name := range list { - found[name] = true - } - - expected := []string{"test1", "test2", "test3"} - for _, name := range expected { - if !found[name] { - t.Errorf("expected provider %q not found in list", name) - } - } -} - -func TestDefaultRegistry(t *testing.T) { - p := &mockProvider{name: "default-test"} - - err := Register(p) - if err != nil { - t.Fatalf("Register failed: %v", err) - } - - retrieved, err := Get("default-test") - if err != nil { - t.Fatalf("Get failed: %v", err) - } - - if retrieved.Name() != "default-test" { - t.Errorf("Got provider name %q, want %q", retrieved.Name(), "default-test") - } - - list := List() - found := false - for _, name := range list { - if name == "default-test" { - found = true - break - } - } - - if !found { - t.Error("provider not found in default registry list") - } -} diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/types.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/types.go deleted file mode 100644 index 109b53b..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/keybinds/types.go +++ /dev/null @@ -1,47 +0,0 @@ -package keybinds - -type Keybind struct { - Key string `json:"key"` - Description string `json:"desc"` - Action string `json:"action,omitempty"` - Subcategory string `json:"subcat,omitempty"` - Source string `json:"source,omitempty"` - HideOnOverlay bool `json:"hideOnOverlay,omitempty"` - CooldownMs int `json:"cooldownMs,omitempty"` - Flags string `json:"flags,omitempty"` // Hyprland bind flags: e=repeat, l=locked, r=release, o=long-press - AllowWhenLocked bool `json:"allowWhenLocked,omitempty"` - AllowInhibiting *bool `json:"allowInhibiting,omitempty"` // nil=default(true), false=explicitly disabled - Repeat *bool `json:"repeat,omitempty"` // nil=default(true), false=explicitly disabled - Conflict *Keybind `json:"conflict,omitempty"` -} - -type DMSBindsStatus struct { - Exists bool `json:"exists"` - Included bool `json:"included"` - IncludePosition int `json:"includePosition"` - TotalIncludes int `json:"totalIncludes"` - BindsAfterDMS int `json:"bindsAfterDms"` - Effective bool `json:"effective"` - OverriddenBy int `json:"overriddenBy"` - StatusMessage string `json:"statusMessage"` -} - -type CheatSheet struct { - Title string `json:"title"` - Provider string `json:"provider"` - Binds map[string][]Keybind `json:"binds"` - DMSBindsIncluded bool `json:"dmsBindsIncluded"` - DMSStatus *DMSBindsStatus `json:"dmsStatus,omitempty"` -} - -type Provider interface { - Name() string - GetCheatSheet() (*CheatSheet, error) -} - -type WritableProvider interface { - Provider - SetBind(key, action, description string, options map[string]any) error - RemoveBind(key string) error - GetOverridePath() string -} |