initial commit
This commit is contained in:
116
internal/keybinds/providers/hyprland.go
Normal file
116
internal/keybinds/providers/hyprland.go
Normal 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, "+")
|
||||
}
|
||||
225
internal/keybinds/providers/hyprland_test.go
Normal file
225
internal/keybinds/providers/hyprland_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
130
internal/keybinds/providers/jsonfile.go
Normal file
130
internal/keybinds/providers/jsonfile.go
Normal 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
|
||||
}
|
||||
279
internal/keybinds/providers/jsonfile_test.go
Normal file
279
internal/keybinds/providers/jsonfile_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
112
internal/keybinds/providers/mangowc.go
Normal file
112
internal/keybinds/providers/mangowc.go
Normal 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, "+")
|
||||
}
|
||||
313
internal/keybinds/providers/mangowc_test.go
Normal file
313
internal/keybinds/providers/mangowc_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
112
internal/keybinds/providers/sway.go
Normal file
112
internal/keybinds/providers/sway.go
Normal 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, "+")
|
||||
}
|
||||
290
internal/keybinds/providers/sway_test.go
Normal file
290
internal/keybinds/providers/sway_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user