1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/core/internal/keybinds/providers/sway_parser.go

368 lines
8.5 KiB
Go

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