added required partitioning package installations
Some checks failed
Build / build (push) Failing after 4m53s

This commit is contained in:
tumillanino
2025-11-12 16:01:17 +11:00
parent f05bd8b929
commit 6fd2f42ec2
574 changed files with 160914 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
package keybinds
import (
"fmt"
"os"
"path/filepath"
"strings"
)
type DiscoveryConfig struct {
SearchPaths []string
}
func DefaultDiscoveryConfig() *DiscoveryConfig {
var searchPaths []string
configHome := os.Getenv("XDG_CONFIG_HOME")
if configHome == "" {
if homeDir, err := os.UserHomeDir(); err == nil {
configHome = filepath.Join(homeDir, ".config")
}
}
if configHome != "" {
searchPaths = append(searchPaths, filepath.Join(configHome, "DankMaterialShell", "cheatsheets"))
}
configDirs := os.Getenv("XDG_CONFIG_DIRS")
if configDirs != "" {
for _, dir := range strings.Split(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 := 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
}
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), 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
}

View File

@@ -0,0 +1,285 @@
package keybinds
import (
"os"
"path/filepath"
"testing"
)
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("{}"), 0644); err != nil {
t.Fatalf("Failed to create file1: %v", err)
}
if err := os.WriteFile(file2, []byte("{}"), 0644); err != nil {
t.Fatalf("Failed to create file2: %v", err)
}
if err := os.WriteFile(txtFile, []byte("text"), 0644); err != nil {
t.Fatalf("Failed to create txt file: %v", err)
}
if err := os.MkdirAll(subdir, 0755); 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("{}"), 0644); err != nil {
t.Fatalf("Failed to create file1: %v", err)
}
if err := os.WriteFile(file2, []byte("{}"), 0644); 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), 0644); 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("{}"), 0644); 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 := expandPath(tt.input)
if err != nil {
t.Fatalf("expandPath failed: %v", err)
}
if result != tt.expected {
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

View File

@@ -0,0 +1,116 @@
package providers
import (
"fmt"
"strings"
"github.com/AvengeMedia/danklinux/internal/hyprland"
"github.com/AvengeMedia/danklinux/internal/keybinds"
)
type HyprlandProvider struct {
configPath string
}
func NewHyprlandProvider(configPath string) *HyprlandProvider {
if configPath == "" {
configPath = "$HOME/.config/hypr"
}
return &HyprlandProvider{
configPath: configPath,
}
}
func (h *HyprlandProvider) Name() string {
return "hyprland"
}
func (h *HyprlandProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := hyprland.ParseKeys(h.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse hyprland config: %w", err)
}
categorizedBinds := make(map[string][]keybinds.Keybind)
h.convertSection(section, "", categorizedBinds)
return &keybinds.CheatSheet{
Title: "Hyprland Keybinds",
Provider: h.Name(),
Binds: categorizedBinds,
}, nil
}
func (h *HyprlandProvider) convertSection(section *hyprland.Section, subcategory string, categorizedBinds map[string][]keybinds.Keybind) {
currentSubcat := subcategory
if section.Name != "" {
currentSubcat = section.Name
}
for _, kb := range section.Keybinds {
category := h.categorizeByDispatcher(kb.Dispatcher)
bind := h.convertKeybind(&kb, currentSubcat)
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
for _, child := range section.Children {
h.convertSection(&child, currentSubcat, categorizedBinds)
}
}
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 *hyprland.KeyBinding, subcategory string) keybinds.Keybind {
key := h.formatKey(kb)
desc := kb.Comment
if desc == "" {
desc = h.generateDescription(kb.Dispatcher, kb.Params)
}
return keybinds.Keybind{
Key: key,
Description: desc,
Subcategory: subcategory,
}
}
func (h *HyprlandProvider) generateDescription(dispatcher, params string) string {
if params != "" {
return dispatcher + " " + params
}
return dispatcher
}
func (h *HyprlandProvider) formatKey(kb *hyprland.KeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}

View File

@@ -0,0 +1,225 @@
package providers
import (
"os"
"path/filepath"
"testing"
)
func TestNewHyprlandProvider(t *testing.T) {
tests := []struct {
name string
configPath string
wantPath string
}{
{
name: "custom path",
configPath: "/custom/path",
wantPath: "/custom/path",
},
{
name: "empty path defaults",
configPath: "",
wantPath: "$HOME/.config/hypr",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewHyprlandProvider(tt.configPath)
if p == nil {
t.Fatal("NewHyprlandProvider returned nil")
}
if p.configPath != tt.wantPath {
t.Errorf("configPath = %q, want %q", p.configPath, tt.wantPath)
}
})
}
}
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), 0644); 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, "test.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), 0644); 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, "test.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), 0644); 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)
}
})
}
}

View File

@@ -0,0 +1,130 @@
package providers
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/AvengeMedia/danklinux/internal/keybinds"
)
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 := 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]interface{}
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]interface{}:
for category, categoryBindsRaw := range binds {
categoryBindsList, ok := categoryBindsRaw.([]interface{})
if !ok {
continue
}
var keybindsList []keybinds.Keybind
categoryBindsJSON, _ := json.Marshal(categoryBindsList)
if err := json.Unmarshal(categoryBindsJSON, &keybindsList); err != nil {
continue
}
categorizedBinds[category] = keybindsList
}
case []interface{}:
flatBindsJSON, _ := json.Marshal(binds)
var flatBinds []struct {
Key string `json:"key"`
Description string `json:"desc"`
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,
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
}
func expandPath(path string) (string, error) {
expandedPath := os.ExpandEnv(path)
if filepath.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return "", err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
return filepath.Clean(expandedPath), nil
}

View File

@@ -0,0 +1,279 @@
package providers
import (
"os"
"path/filepath"
"testing"
)
func TestNewJSONFileProvider(t *testing.T) {
tmpDir := t.TempDir()
testFile := filepath.Join(tmpDir, "test.json")
if err := os.WriteFile(testFile, []byte("{}"), 0644); 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), 0644); 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), 0644); 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), 0644); 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"), 0644); 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 := expandPath(tt.input)
if err != nil {
t.Fatalf("expandPath failed: %v", err)
}
if result != tt.expected {
t.Errorf("expandPath(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}

View File

@@ -0,0 +1,112 @@
package providers
import (
"fmt"
"strings"
"github.com/AvengeMedia/danklinux/internal/keybinds"
"github.com/AvengeMedia/danklinux/internal/mangowc"
)
type MangoWCProvider struct {
configPath string
}
func NewMangoWCProvider(configPath string) *MangoWCProvider {
if configPath == "" {
configPath = "$HOME/.config/mango"
}
return &MangoWCProvider{
configPath: configPath,
}
}
func (m *MangoWCProvider) Name() string {
return "mangowc"
}
func (m *MangoWCProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
keybinds_list, err := mangowc.ParseKeys(m.configPath)
if err != nil {
return nil, fmt.Errorf("failed to parse mangowc config: %w", err)
}
categorizedBinds := make(map[string][]keybinds.Keybind)
for _, kb := range keybinds_list {
category := m.categorizeByCommand(kb.Command)
bind := m.convertKeybind(&kb)
categorizedBinds[category] = append(categorizedBinds[category], bind)
}
return &keybinds.CheatSheet{
Title: "MangoWC Keybinds",
Provider: m.Name(),
Binds: categorizedBinds,
}, nil
}
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 *mangowc.KeyBinding) keybinds.Keybind {
key := m.formatKey(kb)
desc := kb.Comment
if desc == "" {
desc = m.generateDescription(kb.Command, kb.Params)
}
return keybinds.Keybind{
Key: key,
Description: desc,
}
}
func (m *MangoWCProvider) generateDescription(command, params string) string {
if params != "" {
return command + " " + params
}
return command
}
func (m *MangoWCProvider) formatKey(kb *mangowc.KeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}

View File

@@ -0,0 +1,313 @@
package providers
import (
"os"
"path/filepath"
"testing"
"github.com/AvengeMedia/danklinux/internal/mangowc"
)
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("")
if provider.configPath != "$HOME/.config/mango" {
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/mango")
}
}
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 *mangowc.KeyBinding
expected string
}{
{
name: "single_mod",
keybind: &mangowc.KeyBinding{
Mods: []string{"ALT"},
Key: "q",
},
expected: "ALT+q",
},
{
name: "multiple_mods",
keybind: &mangowc.KeyBinding{
Mods: []string{"SUPER", "SHIFT"},
Key: "Up",
},
expected: "SUPER+SHIFT+Up",
},
{
name: "no_mods",
keybind: &mangowc.KeyBinding{
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 *mangowc.KeyBinding
wantKey string
wantDesc string
}{
{
name: "with_comment",
keybind: &mangowc.KeyBinding{
Mods: []string{"ALT"},
Key: "t",
Command: "spawn",
Params: "kitty",
Comment: "Open terminal",
},
wantKey: "ALT+t",
wantDesc: "Open terminal",
},
{
name: "without_comment",
keybind: &mangowc.KeyBinding{
Mods: []string{"SUPER"},
Key: "r",
Command: "reload_config",
Params: "",
Comment: "",
},
wantKey: "SUPER+r",
wantDesc: "reload_config",
},
{
name: "with_params_no_comment",
keybind: &mangowc.KeyBinding{
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)
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), 0644); 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), 0644); 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")
}
}

View File

@@ -0,0 +1,112 @@
package providers
import (
"fmt"
"strings"
"github.com/AvengeMedia/danklinux/internal/keybinds"
"github.com/AvengeMedia/danklinux/internal/sway"
)
type SwayProvider struct {
configPath string
}
func NewSwayProvider(configPath string) *SwayProvider {
if configPath == "" {
configPath = "$HOME/.config/sway"
}
return &SwayProvider{
configPath: configPath,
}
}
func (s *SwayProvider) Name() string {
return "sway"
}
func (s *SwayProvider) GetCheatSheet() (*keybinds.CheatSheet, error) {
section, err := sway.ParseKeys(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)
return &keybinds.CheatSheet{
Title: "Sway Keybinds",
Provider: s.Name(),
Binds: categorizedBinds,
}, nil
}
func (s *SwayProvider) convertSection(section *sway.Section, 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 *sway.KeyBinding, subcategory string) keybinds.Keybind {
key := s.formatKey(kb)
desc := kb.Comment
if desc == "" {
desc = kb.Command
}
return keybinds.Keybind{
Key: key,
Description: desc,
Subcategory: subcategory,
}
}
func (s *SwayProvider) formatKey(kb *sway.KeyBinding) string {
parts := make([]string, 0, len(kb.Mods)+1)
parts = append(parts, kb.Mods...)
parts = append(parts, kb.Key)
return strings.Join(parts, "+")
}

View File

@@ -0,0 +1,290 @@
package providers
import (
"os"
"path/filepath"
"testing"
"github.com/AvengeMedia/danklinux/internal/sway"
)
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("")
if provider.configPath != "$HOME/.config/sway" {
t.Errorf("configPath = %q, want %q", provider.configPath, "$HOME/.config/sway")
}
}
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 *sway.KeyBinding
expected string
}{
{
name: "single_mod",
keybind: &sway.KeyBinding{
Mods: []string{"Mod4"},
Key: "q",
},
expected: "Mod4+q",
},
{
name: "multiple_mods",
keybind: &sway.KeyBinding{
Mods: []string{"Mod4", "Shift"},
Key: "e",
},
expected: "Mod4+Shift+e",
},
{
name: "no_mods",
keybind: &sway.KeyBinding{
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 *sway.KeyBinding
wantKey string
wantDesc string
}{
{
name: "with_comment",
keybind: &sway.KeyBinding{
Mods: []string{"Mod4"},
Key: "t",
Command: "exec kitty",
Comment: "Open terminal",
},
wantKey: "Mod4+t",
wantDesc: "Open terminal",
},
{
name: "without_comment",
keybind: &sway.KeyBinding{
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), 0644); 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), 0644); 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")
}
}

View File

@@ -0,0 +1,79 @@
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()
}

View File

@@ -0,0 +1,183 @@
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")
}
}

View File

@@ -0,0 +1,18 @@
package keybinds
type Keybind struct {
Key string `json:"key"`
Description string `json:"desc"`
Subcategory string `json:"subcat,omitempty"`
}
type CheatSheet struct {
Title string `json:"title"`
Provider string `json:"provider"`
Binds map[string][]Keybind `json:"binds"`
}
type Provider interface {
Name() string
GetCheatSheet() (*CheatSheet, error)
}