1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-11 07:52:50 -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,367 @@
package sway
import (
"os"
"path/filepath"
"regexp"
"strings"
)
const (
TitleRegex = "#+!"
HideComment = "[hidden]"
)
var ModSeparators = []rune{'+', ' '}
type KeyBinding struct {
Mods []string `json:"mods"`
Key string `json:"key"`
Command string `json:"command"`
Comment string `json:"comment"`
}
type Section struct {
Children []Section `json:"children"`
Keybinds []KeyBinding `json:"keybinds"`
Name string `json:"name"`
}
type Parser struct {
contentLines []string
readingLine int
variables map[string]string
}
func NewParser() *Parser {
return &Parser{
contentLines: []string{},
readingLine: 0,
variables: make(map[string]string),
}
}
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() {
mainConfig := filepath.Join(expandedPath, "config")
if fileInfo, err := os.Stat(mainConfig); err == nil && fileInfo.Mode().IsRegular() {
files = []string{mainConfig}
} else {
return os.ErrNotExist
}
} else {
files = []string{expandedPath}
}
var combinedContent []string
for _, file := range files {
data, err := os.ReadFile(file)
if err != nil {
return err
}
combinedContent = append(combinedContent, string(data))
}
if len(combinedContent) == 0 {
return os.ErrNotExist
}
fullContent := strings.Join(combinedContent, "\n")
p.contentLines = strings.Split(fullContent, "\n")
p.parseVariables()
return nil
}
func (p *Parser) parseVariables() {
setRegex := regexp.MustCompile(`^\s*set\s+\$(\w+)\s+(.+)$`)
for _, line := range p.contentLines {
matches := setRegex.FindStringSubmatch(line)
if len(matches) == 3 {
varName := matches[1]
varValue := strings.TrimSpace(matches[2])
p.variables[varName] = varValue
}
}
}
func (p *Parser) expandVariables(text string) string {
result := text
for varName, varValue := range p.variables {
result = strings.ReplaceAll(result, "$"+varName, varValue)
}
return result
}
func autogenerateComment(command string) string {
command = strings.TrimSpace(command)
if strings.HasPrefix(command, "exec ") {
cmdPart := strings.TrimPrefix(command, "exec ")
cmdPart = strings.TrimPrefix(cmdPart, "--no-startup-id ")
return cmdPart
}
switch {
case command == "kill":
return "Close window"
case command == "exit":
return "Exit Sway"
case command == "reload":
return "Reload configuration"
case strings.HasPrefix(command, "fullscreen"):
return "Toggle fullscreen"
case strings.HasPrefix(command, "floating toggle"):
return "Float/unfloat window"
case strings.HasPrefix(command, "focus mode_toggle"):
return "Toggle focus mode"
case strings.HasPrefix(command, "focus parent"):
return "Focus parent container"
case strings.HasPrefix(command, "focus left"):
return "Focus left"
case strings.HasPrefix(command, "focus right"):
return "Focus right"
case strings.HasPrefix(command, "focus up"):
return "Focus up"
case strings.HasPrefix(command, "focus down"):
return "Focus down"
case strings.HasPrefix(command, "focus output"):
return "Focus monitor"
case strings.HasPrefix(command, "move left"):
return "Move window left"
case strings.HasPrefix(command, "move right"):
return "Move window right"
case strings.HasPrefix(command, "move up"):
return "Move window up"
case strings.HasPrefix(command, "move down"):
return "Move window down"
case strings.HasPrefix(command, "move container to workspace"):
if strings.Contains(command, "prev") {
return "Move to previous workspace"
}
if strings.Contains(command, "next") {
return "Move to next workspace"
}
parts := strings.Fields(command)
if len(parts) > 4 {
return "Move to workspace " + parts[len(parts)-1]
}
return "Move to workspace"
case strings.HasPrefix(command, "move workspace to output"):
return "Move workspace to monitor"
case strings.HasPrefix(command, "workspace"):
if strings.Contains(command, "prev") {
return "Previous workspace"
}
if strings.Contains(command, "next") {
return "Next workspace"
}
parts := strings.Fields(command)
if len(parts) > 1 {
wsNum := parts[len(parts)-1]
return "Workspace " + wsNum
}
return "Switch workspace"
case strings.HasPrefix(command, "layout"):
parts := strings.Fields(command)
if len(parts) > 1 {
return "Layout " + parts[1]
}
return "Change layout"
case strings.HasPrefix(command, "split"):
if strings.Contains(command, "h") {
return "Split horizontal"
}
if strings.Contains(command, "v") {
return "Split vertical"
}
return "Split container"
case strings.HasPrefix(command, "resize"):
return "Resize window"
case strings.Contains(command, "scratchpad"):
return "Toggle scratchpad"
default:
return command
}
}
func (p *Parser) getKeybindAtLine(lineNumber int) *KeyBinding {
if lineNumber >= len(p.contentLines) {
return nil
}
line := p.contentLines[lineNumber]
bindMatch := regexp.MustCompile(`^\s*(bindsym|bindcode)\s+(.+)$`)
matches := bindMatch.FindStringSubmatch(line)
if len(matches) < 3 {
return nil
}
content := matches[2]
parts := strings.SplitN(content, "#", 2)
keys := strings.TrimSpace(parts[0])
var comment string
if len(parts) > 1 {
comment = strings.TrimSpace(parts[1])
}
if strings.HasPrefix(comment, HideComment) {
return nil
}
flags := ""
if strings.HasPrefix(keys, "--") {
spaceIdx := strings.Index(keys, " ")
if spaceIdx > 0 {
flags = keys[:spaceIdx]
keys = strings.TrimSpace(keys[spaceIdx+1:])
}
}
keyParts := strings.Fields(keys)
if len(keyParts) < 2 {
return nil
}
keyCombo := keyParts[0]
keyCombo = p.expandVariables(keyCombo)
command := strings.Join(keyParts[1:], " ")
command = p.expandVariables(command)
var modList []string
var key string
modstring := keyCombo + string(ModSeparators[0])
pos := 0
for index, char := range modstring {
isModSep := false
for _, sep := range ModSeparators {
if char == sep {
isModSep = true
break
}
}
if isModSep {
if index-pos > 0 {
part := modstring[pos:index]
if isMod(part) {
modList = append(modList, part)
} else {
key = part
}
}
pos = index + 1
}
}
if comment == "" {
comment = autogenerateComment(command)
}
_ = flags
return &KeyBinding{
Mods: modList,
Key: key,
Command: command,
Comment: comment,
}
}
func isMod(s string) bool {
s = strings.ToLower(s)
if s == "mod1" || s == "mod2" || s == "mod3" || s == "mod4" || s == "mod5" ||
s == "shift" || s == "control" || s == "ctrl" || s == "alt" || s == "super" ||
strings.HasPrefix(s, "$") {
return true
}
isNumeric := true
for _, c := range s {
if c < '0' || c > '9' {
isNumeric = false
break
}
}
if isNumeric && len(s) >= 2 {
return true
}
return false
}
func (p *Parser) getBindsRecursive(currentContent *Section, scope int) *Section {
titleRegex := regexp.MustCompile(TitleRegex)
for p.readingLine < len(p.contentLines) {
line := p.contentLines[p.readingLine]
loc := titleRegex.FindStringIndex(line)
if loc != nil && loc[0] == 0 {
headingScope := strings.Index(line, "!")
if headingScope <= scope {
p.readingLine--
return currentContent
}
sectionName := strings.TrimSpace(line[headingScope+1:])
p.readingLine++
childSection := &Section{
Children: []Section{},
Keybinds: []KeyBinding{},
Name: sectionName,
}
result := p.getBindsRecursive(childSection, headingScope)
currentContent.Children = append(currentContent.Children, *result)
} else if line == "" || (!strings.Contains(line, "bindsym") && !strings.Contains(line, "bindcode")) {
} else {
keybind := p.getKeybindAtLine(p.readingLine)
if keybind != nil {
currentContent.Keybinds = append(currentContent.Keybinds, *keybind)
}
}
p.readingLine++
}
return currentContent
}
func (p *Parser) ParseKeys() *Section {
p.readingLine = 0
rootSection := &Section{
Children: []Section{},
Keybinds: []KeyBinding{},
Name: "",
}
return p.getBindsRecursive(rootSection, 0)
}
func ParseKeys(path string) (*Section, error) {
parser := NewParser()
if err := parser.ReadContent(path); err != nil {
return nil, err
}
return parser.ParseKeys(), nil
}

View File

@@ -0,0 +1,471 @@
package sway
import (
"os"
"path/filepath"
"testing"
)
func TestAutogenerateComment(t *testing.T) {
tests := []struct {
command string
expected string
}{
{"exec kitty", "kitty"},
{"exec --no-startup-id firefox", "firefox"},
{"kill", "Close window"},
{"exit", "Exit Sway"},
{"reload", "Reload configuration"},
{"fullscreen toggle", "Toggle fullscreen"},
{"floating toggle", "Float/unfloat window"},
{"focus mode_toggle", "Toggle focus mode"},
{"focus parent", "Focus parent container"},
{"focus left", "Focus left"},
{"focus right", "Focus right"},
{"focus up", "Focus up"},
{"focus down", "Focus down"},
{"focus output left", "Focus monitor"},
{"move left", "Move window left"},
{"move right", "Move window right"},
{"move up", "Move window up"},
{"move down", "Move window down"},
{"move container to workspace number 1", "Move to workspace 1"},
{"move container to workspace prev", "Move to previous workspace"},
{"move container to workspace next", "Move to next workspace"},
{"move workspace to output left", "Move workspace to monitor"},
{"workspace number 1", "Workspace 1"},
{"workspace prev", "Previous workspace"},
{"workspace next", "Next workspace"},
{"layout tabbed", "Layout tabbed"},
{"layout stacking", "Layout stacking"},
{"splith", "Split horizontal"},
{"splitv", "Split vertical"},
{"resize grow width 10 ppt", "Resize window"},
{"move scratchpad", "Toggle scratchpad"},
}
for _, tt := range tests {
t.Run(tt.command, func(t *testing.T) {
result := autogenerateComment(tt.command)
if result != tt.expected {
t.Errorf("autogenerateComment(%q) = %q, want %q",
tt.command, result, tt.expected)
}
})
}
}
func TestGetKeybindAtLine(t *testing.T) {
tests := []struct {
name string
line string
expected *KeyBinding
}{
{
name: "basic_keybind",
line: "bindsym Mod4+q kill",
expected: &KeyBinding{
Mods: []string{"Mod4"},
Key: "q",
Command: "kill",
Comment: "Close window",
},
},
{
name: "keybind_with_exec",
line: "bindsym Mod4+t exec kitty",
expected: &KeyBinding{
Mods: []string{"Mod4"},
Key: "t",
Command: "exec kitty",
Comment: "kitty",
},
},
{
name: "keybind_with_comment",
line: "bindsym Mod4+Space exec dms ipc call spotlight toggle # Open launcher",
expected: &KeyBinding{
Mods: []string{"Mod4"},
Key: "Space",
Command: "exec dms ipc call spotlight toggle",
Comment: "Open launcher",
},
},
{
name: "keybind_hidden",
line: "bindsym Mod4+h exec secret # [hidden]",
expected: nil,
},
{
name: "keybind_multiple_mods",
line: "bindsym Mod4+Shift+e exit",
expected: &KeyBinding{
Mods: []string{"Mod4", "Shift"},
Key: "e",
Command: "exit",
Comment: "Exit Sway",
},
},
{
name: "keybind_no_mods",
line: "bindsym Print exec grim screenshot.png",
expected: &KeyBinding{
Mods: []string{},
Key: "Print",
Command: "exec grim screenshot.png",
Comment: "grim screenshot.png",
},
},
{
name: "keybind_with_flags",
line: "bindsym --release Mod4+x exec notify-send released",
expected: &KeyBinding{
Mods: []string{"Mod4"},
Key: "x",
Command: "exec notify-send released",
Comment: "notify-send released",
},
},
{
name: "keybind_focus_direction",
line: "bindsym Mod4+Left focus left",
expected: &KeyBinding{
Mods: []string{"Mod4"},
Key: "Left",
Command: "focus left",
Comment: "Focus left",
},
},
{
name: "keybind_workspace",
line: "bindsym Mod4+1 workspace number 1",
expected: &KeyBinding{
Mods: []string{"Mod4"},
Key: "1",
Command: "workspace number 1",
Comment: "Workspace 1",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
parser := 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.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 TestVariableExpansion(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config")
content := `set $mod Mod4
set $term kitty
set $menu rofi
bindsym $mod+t exec $term
bindsym $mod+d exec $menu
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds, got %d", len(section.Keybinds))
}
if len(section.Keybinds) > 0 {
if section.Keybinds[0].Mods[0] != "Mod4" {
t.Errorf("Expected Mod4, got %q", section.Keybinds[0].Mods[0])
}
if section.Keybinds[0].Command != "exec kitty" {
t.Errorf("Expected 'exec kitty', got %q", section.Keybinds[0].Command)
}
}
if len(section.Keybinds) > 1 {
if section.Keybinds[1].Command != "exec rofi" {
t.Errorf("Expected 'exec rofi', got %q", section.Keybinds[1].Command)
}
}
}
func TestParseKeysWithSections(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config")
content := `set $mod Mod4
##! Window Management
bindsym $mod+q kill
bindsym $mod+f fullscreen toggle
###! Focus
bindsym $mod+Left focus left
bindsym $mod+Right focus right
##! Applications
bindsym $mod+t exec kitty # Terminal
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(tmpDir)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
if len(section.Children) != 2 {
t.Errorf("Expected 2 top-level sections, got %d", len(section.Children))
}
if len(section.Children) >= 1 {
windowMgmt := section.Children[0]
if windowMgmt.Name != "Window Management" {
t.Errorf("First section name = %q, want %q", windowMgmt.Name, "Window Management")
}
if len(windowMgmt.Keybinds) != 2 {
t.Errorf("Window Management keybinds = %d, want 2", len(windowMgmt.Keybinds))
}
if len(windowMgmt.Children) != 1 {
t.Errorf("Window Management children = %d, want 1", len(windowMgmt.Children))
} else {
focus := windowMgmt.Children[0]
if focus.Name != "Focus" {
t.Errorf("Focus section name = %q, want %q", focus.Name, "Focus")
}
if len(focus.Keybinds) != 2 {
t.Errorf("Focus keybinds = %d, want 2", len(focus.Keybinds))
}
}
}
if len(section.Children) >= 2 {
apps := section.Children[1]
if apps.Name != "Applications" {
t.Errorf("Second section name = %q, want %q", apps.Name, "Applications")
}
if len(apps.Keybinds) != 1 {
t.Errorf("Applications keybinds = %d, want 1", len(apps.Keybinds))
}
if len(apps.Keybinds) > 0 && apps.Keybinds[0].Comment != "Terminal" {
t.Errorf("Applications keybind comment = %q, want %q", apps.Keybinds[0].Comment, "Terminal")
}
}
}
func 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-sway-"+t.Name())
if err := os.MkdirAll(tmpSubdir, 0755); err != nil {
t.Skip("Cannot create test directory in home")
}
defer os.RemoveAll(tmpSubdir)
configFile := filepath.Join(tmpSubdir, "config")
if err := os.WriteFile(configFile, []byte("bindsym Mod4+q kill\n"), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
relPath, err := filepath.Rel(homeDir, tmpSubdir)
if err != nil {
t.Skip("Cannot create relative path")
}
parser := 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")
content := `
# This is a comment
bindsym Mod4+q kill
# Another comment
bindsym Mod4+t exec kitty
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
if len(section.Keybinds) != 2 {
t.Errorf("Expected 2 keybinds (comments ignored), got %d", len(section.Keybinds))
}
}
func TestRealWorldConfig(t *testing.T) {
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config")
content := `set $mod Mod4
set $term kitty
## Application Launchers
bindsym $mod+t exec $term
bindsym $mod+Space exec rofi
## Window Management
bindsym $mod+q kill
bindsym $mod+f fullscreen toggle
## Focus Navigation
bindsym $mod+Left focus left
bindsym $mod+Right focus right
## Workspace Navigation
bindsym $mod+1 workspace number 1
bindsym $mod+2 workspace number 2
bindsym $mod+Shift+1 move container to workspace number 1
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("Failed to write test config: %v", err)
}
section, err := ParseKeys(configFile)
if err != nil {
t.Fatalf("ParseKeys failed: %v", err)
}
if len(section.Keybinds) < 9 {
t.Errorf("Expected at least 9 keybinds, got %d", len(section.Keybinds))
}
foundExec := false
foundKill := false
foundWorkspace := false
for _, kb := range section.Keybinds {
if kb.Command == "exec kitty" {
foundExec = true
}
if kb.Command == "kill" {
foundKill = true
}
if kb.Command == "workspace number 1" {
foundWorkspace = true
}
}
if !foundExec {
t.Error("Did not find exec kitty keybind")
}
if !foundKill {
t.Error("Did not find kill keybind")
}
if !foundWorkspace {
t.Error("Did not find workspace 1 keybind")
}
}
func TestIsMod(t *testing.T) {
tests := []struct {
input string
expected bool
}{
{"Mod4", true},
{"Shift", true},
{"Control", true},
{"Alt", true},
{"Super", true},
{"$mod", true},
{"Left", false},
{"q", false},
{"1", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
result := isMod(tt.input)
if result != tt.expected {
t.Errorf("isMod(%q) = %v, want %v", tt.input, result, tt.expected)
}
})
}
}