1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-10 07:25:37 -05:00

rename backend to core

This commit is contained in:
bbedward
2025-11-12 23:12:31 -05:00
parent 0fdc0748cf
commit db584b7897
280 changed files with 265 additions and 265 deletions

View File

@@ -0,0 +1,305 @@
package mangowc
import (
"os"
"path/filepath"
"regexp"
"strings"
)
const (
HideComment = "[hidden]"
)
var ModSeparators = []rune{'+', ' '}
type KeyBinding struct {
Mods []string `json:"mods"`
Key string `json:"key"`
Command string `json:"command"`
Params string `json:"params"`
Comment string `json:"comment"`
}
type Parser struct {
contentLines []string
readingLine int
}
func NewParser() *Parser {
return &Parser{
contentLines: []string{},
readingLine: 0,
}
}
func (p *Parser) ReadContent(path string) error {
expandedPath := os.ExpandEnv(path)
expandedPath = filepath.Clean(expandedPath)
if strings.HasPrefix(expandedPath, "~") {
home, err := os.UserHomeDir()
if err != nil {
return err
}
expandedPath = filepath.Join(home, expandedPath[1:])
}
info, err := os.Stat(expandedPath)
if err != nil {
return err
}
var files []string
if info.IsDir() {
confFiles, err := filepath.Glob(filepath.Join(expandedPath, "*.conf"))
if err != nil {
return err
}
if len(confFiles) == 0 {
return os.ErrNotExist
}
files = confFiles
} else {
files = []string{expandedPath}
}
var combinedContent []string
for _, file := range files {
if fileInfo, err := os.Stat(file); err == nil && fileInfo.Mode().IsRegular() {
data, err := os.ReadFile(file)
if err == nil {
combinedContent = append(combinedContent, string(data))
}
}
}
if len(combinedContent) == 0 {
return os.ErrNotExist
}
fullContent := strings.Join(combinedContent, "\n")
p.contentLines = strings.Split(fullContent, "\n")
return nil
}
func autogenerateComment(command, params string) string {
switch command {
case "spawn", "spawn_shell":
return params
case "killclient":
return "Close window"
case "quit":
return "Exit MangoWC"
case "reload_config":
return "Reload configuration"
case "focusstack":
if params == "next" {
return "Focus next window"
}
if params == "prev" {
return "Focus previous window"
}
return "Focus stack " + params
case "focusdir":
dirMap := map[string]string{
"left": "left",
"right": "right",
"up": "up",
"down": "down",
}
if dir, ok := dirMap[params]; ok {
return "Focus " + dir
}
return "Focus " + params
case "exchange_client":
dirMap := map[string]string{
"left": "left",
"right": "right",
"up": "up",
"down": "down",
}
if dir, ok := dirMap[params]; ok {
return "Swap window " + dir
}
return "Swap window " + params
case "togglefloating":
return "Float/unfloat window"
case "togglefullscreen":
return "Toggle fullscreen"
case "togglefakefullscreen":
return "Toggle fake fullscreen"
case "togglemaximizescreen":
return "Toggle maximize"
case "toggleglobal":
return "Toggle global"
case "toggleoverview":
return "Toggle overview"
case "toggleoverlay":
return "Toggle overlay"
case "minimized":
return "Minimize window"
case "restore_minimized":
return "Restore minimized"
case "toggle_scratchpad":
return "Toggle scratchpad"
case "setlayout":
return "Set layout " + params
case "switch_layout":
return "Switch layout"
case "view":
parts := strings.Split(params, ",")
if len(parts) > 0 {
return "View tag " + parts[0]
}
return "View tag"
case "tag":
parts := strings.Split(params, ",")
if len(parts) > 0 {
return "Move to tag " + parts[0]
}
return "Move to tag"
case "toggleview":
parts := strings.Split(params, ",")
if len(parts) > 0 {
return "Toggle tag " + parts[0]
}
return "Toggle tag"
case "viewtoleft", "viewtoleft_have_client":
return "View left tag"
case "viewtoright", "viewtoright_have_client":
return "View right tag"
case "tagtoleft":
return "Move to left tag"
case "tagtoright":
return "Move to right tag"
case "focusmon":
return "Focus monitor " + params
case "tagmon":
return "Move to monitor " + params
case "incgaps":
if strings.HasPrefix(params, "-") {
return "Decrease gaps"
}
return "Increase gaps"
case "togglegaps":
return "Toggle gaps"
case "movewin":
return "Move window by " + params
case "resizewin":
return "Resize window by " + params
case "set_proportion":
return "Set proportion " + params
case "switch_proportion_preset":
return "Switch proportion preset"
default:
return ""
}
}
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
if lineNumber >= len(p.contentLines) {
return nil
}
line := p.contentLines[lineNumber]
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
matches := bindMatch.FindStringSubmatch(line)
if len(matches) < 3 {
return nil
}
bindType := matches[1]
content := matches[2]
parts := strings.SplitN(content, "#", 2)
keys := parts[0]
var comment string
if len(parts) > 1 {
comment = strings.TrimSpace(parts[1])
}
if strings.HasPrefix(comment, HideComment) {
return nil
}
keyFields := strings.SplitN(keys, ",", 4)
if len(keyFields) < 3 {
return nil
}
mods := strings.TrimSpace(keyFields[0])
key := strings.TrimSpace(keyFields[1])
command := strings.TrimSpace(keyFields[2])
var params string
if len(keyFields) > 3 {
params = strings.TrimSpace(keyFields[3])
}
if comment == "" {
comment = autogenerateComment(command, params)
}
var modList []string
if mods != "" && !strings.EqualFold(mods, "none") {
modstring := mods + string(ModSeparators[0])
p := 0
for index, char := range modstring {
isModSep := false
for _, sep := range ModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-p > 1 {
modList = append(modList, modstring[p:index])
}
p = index + 1
}
}
}
_ = bindType
return &KeyBinding{
Mods: modList,
Key: key,
Command: command,
Params: params,
Comment: comment,
}
}
func (p *Parser) ParseKeys() []KeyBinding {
var keybinds []KeyBinding
for lineNumber := 0; lineNumber < len(p.contentLines); lineNumber++ {
line := p.contentLines[lineNumber]
if line == "" || strings.HasPrefix(strings.TrimSpace(line), "#") {
continue
}
if !strings.HasPrefix(strings.TrimSpace(line), "bind") {
continue
}
keybind := p.getKeybindAtLine(lineNumber)
if keybind != nil {
keybinds = append(keybinds, *keybind)
}
}
return keybinds
}
func ParseKeys(path string) ([]KeyBinding, error) {
parser := NewParser()
if err := parser.ReadContent(path); err != nil {
return nil, err
}
return parser.ParseKeys(), nil
}

View File

@@ -0,0 +1,499 @@
package mangowc
import (
"os"
"path/filepath"
"testing"
)
func TestAutogenerateComment(t *testing.T) {
tests := []struct {
command string
params string
expected string
}{
{"spawn", "kitty", "kitty"},
{"spawn_shell", "firefox", "firefox"},
{"killclient", "", "Close window"},
{"quit", "", "Exit MangoWC"},
{"reload_config", "", "Reload configuration"},
{"focusstack", "next", "Focus next window"},
{"focusstack", "prev", "Focus previous window"},
{"focusdir", "left", "Focus left"},
{"focusdir", "right", "Focus right"},
{"focusdir", "up", "Focus up"},
{"focusdir", "down", "Focus down"},
{"exchange_client", "left", "Swap window left"},
{"exchange_client", "right", "Swap window right"},
{"togglefloating", "", "Float/unfloat window"},
{"togglefullscreen", "", "Toggle fullscreen"},
{"togglefakefullscreen", "", "Toggle fake fullscreen"},
{"togglemaximizescreen", "", "Toggle maximize"},
{"toggleglobal", "", "Toggle global"},
{"toggleoverview", "", "Toggle overview"},
{"toggleoverlay", "", "Toggle overlay"},
{"minimized", "", "Minimize window"},
{"restore_minimized", "", "Restore minimized"},
{"toggle_scratchpad", "", "Toggle scratchpad"},
{"setlayout", "tile", "Set layout tile"},
{"switch_layout", "", "Switch layout"},
{"view", "1,0", "View tag 1"},
{"tag", "2,0", "Move to tag 2"},
{"toggleview", "3,0", "Toggle tag 3"},
{"viewtoleft", "", "View left tag"},
{"viewtoright", "", "View right tag"},
{"viewtoleft_have_client", "", "View left tag"},
{"viewtoright_have_client", "", "View right tag"},
{"tagtoleft", "", "Move to left tag"},
{"tagtoright", "", "Move to right tag"},
{"focusmon", "left", "Focus monitor left"},
{"tagmon", "right", "Move to monitor right"},
{"incgaps", "1", "Increase gaps"},
{"incgaps", "-1", "Decrease gaps"},
{"togglegaps", "", "Toggle gaps"},
{"movewin", "+0,-50", "Move window by +0,-50"},
{"resizewin", "+0,+50", "Resize window by +0,+50"},
{"set_proportion", "1.0", "Set proportion 1.0"},
{"switch_proportion_preset", "", "Switch proportion preset"},
{"unknown", "", ""},
}
for _, tt := range tests {
t.Run(tt.command+"_"+tt.params, func(t *testing.T) {
result := autogenerateComment(tt.command, tt.params)
if result != tt.expected {
t.Errorf("autogenerateComment(%q, %q) = %q, want %q",
tt.command, tt.params, result, tt.expected)
}
})
}
}
func TestGetKeybindAtLine(t *testing.T) {
tests := []struct {
name string
line string
expected *KeyBinding
}{
{
name: "basic_keybind",
line: "bind=ALT,q,killclient,",
expected: &KeyBinding{
Mods: []string{"ALT"},
Key: "q",
Command: "killclient",
Params: "",
Comment: "Close window",
},
},
{
name: "keybind_with_params",
line: "bind=ALT,Left,focusdir,left",
expected: &KeyBinding{
Mods: []string{"ALT"},
Key: "Left",
Command: "focusdir",
Params: "left",
Comment: "Focus left",
},
},
{
name: "keybind_with_comment",
line: "bind=Alt,t,spawn,kitty # Open terminal",
expected: &KeyBinding{
Mods: []string{"Alt"},
Key: "t",
Command: "spawn",
Params: "kitty",
Comment: "Open terminal",
},
},
{
name: "keybind_hidden",
line: "bind=SUPER,h,spawn,secret # [hidden]",
expected: nil,
},
{
name: "keybind_multiple_mods",
line: "bind=SUPER+SHIFT,Up,exchange_client,up",
expected: &KeyBinding{
Mods: []string{"SUPER", "SHIFT"},
Key: "Up",
Command: "exchange_client",
Params: "up",
Comment: "Swap window up",
},
},
{
name: "keybind_no_mods",
line: "bind=NONE,Print,spawn,screenshot",
expected: &KeyBinding{
Mods: []string{},
Key: "Print",
Command: "spawn",
Params: "screenshot",
Comment: "screenshot",
},
},
{
name: "keybind_multiple_params",
line: "bind=Ctrl,1,view,1,0",
expected: &KeyBinding{
Mods: []string{"Ctrl"},
Key: "1",
Command: "view",
Params: "1,0",
Comment: "View tag 1",
},
},
{
name: "bindl_flag",
line: "bindl=SUPER+ALT,l,spawn,dms ipc call lock lock",
expected: &KeyBinding{
Mods: []string{"SUPER", "ALT"},
Key: "l",
Command: "spawn",
Params: "dms ipc call lock lock",
Comment: "dms ipc call lock lock",
},
},
{
name: "keybind_with_spaces",
line: "bind = SUPER, r, reload_config",
expected: &KeyBinding{
Mods: []string{"SUPER"},
Key: "r",
Command: "reload_config",
Params: "",
Comment: "Reload configuration",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser()
parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0)
if tt.expected == nil {
if result != nil {
t.Errorf("expected nil, got %+v", result)
}
return
}
if result == nil {
t.Errorf("expected %+v, got nil", tt.expected)
return
}
if result.Key != tt.expected.Key {
t.Errorf("Key = %q, want %q", result.Key, tt.expected.Key)
}
if result.Command != tt.expected.Command {
t.Errorf("Command = %q, want %q", result.Command, tt.expected.Command)
}
if result.Params != tt.expected.Params {
t.Errorf("Params = %q, want %q", result.Params, tt.expected.Params)
}
if result.Comment != tt.expected.Comment {
t.Errorf("Comment = %q, want %q", result.Comment, tt.expected.Comment)
}
if len(result.Mods) != len(tt.expected.Mods) {
t.Errorf("Mods length = %d, want %d", len(result.Mods), len(tt.expected.Mods))
} else {
for i := range result.Mods {
if result.Mods[i] != tt.expected.Mods[i] {
t.Errorf("Mods[%d] = %q, want %q", i, result.Mods[i], tt.expected.Mods[i])
}
}
}
})
}
}
func TestParseKeys(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf")
content := `# MangoWC Configuration
blur=0
border_radius=12
# Key Bindings
bind=SUPER,r,reload_config
bind=Alt,t,spawn,kitty # Terminal
bind=ALT,q,killclient,
bind=ALT,Left,focusdir,left
# Hidden binding
bind=SUPER,h,spawn,secret # [hidden]
# Multiple modifiers
bind=SUPER+SHIFT,Up,exchange_client,up
# Workspace bindings
bind=Ctrl,1,view,1,0
bind=Ctrl,2,view,2,0
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
keybinds, err := ParseKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
expectedCount := 7
if len(keybinds) != expectedCount {
t.Errorf("Expected %d keybinds, got %d", expectedCount, len(keybinds))
}
if len(keybinds) > 0 && keybinds[0].Command != "reload_config" {
t.Errorf("First keybind command = %q, want %q", keybinds[0].Command, "reload_config")
}
foundHidden := false
for _, kb := range keybinds {
if kb.Command == "spawn" && kb.Params == "secret" {
foundHidden = true
}
}
if foundHidden {
t.Error("Hidden keybind should not be included in results")
}
}
func TestReadContentMultipleFiles(t *testing.T) {
tmpDir := t.TempDir()
file1 := filepath.Join(tmpDir, "a.conf")
file2 := filepath.Join(tmpDir, "b.conf")
content1 := "bind=ALT,q,killclient,\n"
content2 := "bind=Alt,t,spawn,kitty\n"
if err := os.WriteFile(file1, []byte(content1), 0644); err != nil {
t.Fatalf("Failed to write file1: %v", err)
}
if err := os.WriteFile(file2, []byte(content2), 0644); err != nil {
t.Fatalf("Failed to write file2: %v", err)
}
parser := NewParser()
if err := parser.ReadContent(tmpDir); err != nil {
t.Fatalf("ReadContent failed: %v", err)
}
keybinds := parser.ParseKeys()
if len(keybinds) != 2 {
t.Errorf("Expected 2 keybinds from multiple files, got %d", len(keybinds))
}
}
func TestReadContentSingleFile(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf")
content := "bind=ALT,q,killclient,\n"
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write config: %v", err)
}
parser := NewParser()
if err := parser.ReadContent(configFile); err != nil {
t.Fatalf("ReadContent failed: %v", err)
}
keybinds := parser.ParseKeys()
if len(keybinds) != 1 {
t.Errorf("Expected 1 keybind, got %d", len(keybinds))
}
}
func TestReadContentErrors(t *testing.T) {
tests := []struct {
name string
path string
}{
{
name: "nonexistent_directory",
path: "/nonexistent/path/that/does/not/exist",
},
{
name: "empty_directory",
path: t.TempDir(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseKeys(tt.path)
if err == nil {
t.Error("Expected error, got nil")
}
})
}
}
func TestReadContentWithTildeExpansion(t *testing.T) {
homeDir, err := os.UserHomeDir()
if err != nil {
t.Skip("Cannot get home directory")
}
tmpSubdir := filepath.Join(homeDir, ".config", "test-mango-"+t.Name())
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
t.Skip("Cannot create test directory in home")
}
defer os.RemoveAll(tmpSubdir)
configFile := filepath.Join(tmpSubdir, "config.conf")
if err := os.WriteFile(configFile, []byte("bind=ALT,q,killclient,\n"), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
relPath, err := filepath.Rel(homeDir, tmpSubdir)
if err != nil {
t.Skip("Cannot create relative path")
}
parser := NewParser()
tildePathMatch := "~/" + relPath
err = parser.ReadContent(tildePathMatch)
if err != nil {
t.Errorf("ReadContent with tilde path failed: %v", err)
}
}
func TestEmptyAndCommentLines(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf")
content := `
# This is a comment
bind=ALT,q,killclient,
# Another comment
bind=Alt,t,spawn,kitty
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
keybinds, err := ParseKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
if len(keybinds) != 2 {
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(keybinds))
}
}
func TestInvalidBindLines(t *testing.T) {
tests := []struct {
name string
line string
}{
{
name: "missing_parts",
line: "bind=SUPER,q",
},
{
name: "not_bind",
line: "blur=0",
},
{
name: "empty_line",
line: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := NewParser()
parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0)
if result != nil {
t.Errorf("expected nil for invalid line, got %+v", result)
}
})
}
}
func TestRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.conf")
content := `# Application Launchers
bind=Alt,t,spawn,kitty
bind=Alt,space,spawn,dms ipc call spotlight toggle
bind=Alt,v,spawn,dms ipc call clipboard toggle
# exit
bind=ALT+SHIFT,e,quit
bind=ALT,q,killclient,
# switch window focus
bind=SUPER,Tab,focusstack,next
bind=ALT,Left,focusdir,left
bind=ALT,Right,focusdir,right
# tag switch
bind=SUPER,Left,viewtoleft,0
bind=CTRL,Left,viewtoleft_have_client,0
bind=SUPER,Right,viewtoright,0
bind=Ctrl,1,view,1,0
bind=Ctrl,2,view,2,0
bind=Ctrl,3,view,3,0
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
keybinds, err := ParseKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
if len(keybinds) < 14 {
t.Errorf("Expected at least 14 keybinds, got %d", len(keybinds))
}
foundSpawn := false
foundQuit := false
foundView := false
for _, kb := range keybinds {
if kb.Command == "spawn" && kb.Params == "kitty" {
foundSpawn = true
}
if kb.Command == "quit" {
foundQuit = true
}
if kb.Command == "view" && kb.Params == "1,0" {
foundView = true
}
}
if !foundSpawn {
t.Error("Did not find spawn kitty keybind")
}
if !foundQuit {
t.Error("Did not find quit keybind")
}
if !foundView {
t.Error("Did not find view workspace 1 keybind")
}
}