mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-08 06:25:37 -05:00
core: ensure all NM tests use mock backend + re-orgs + dep updates
This commit is contained in:
367
core/internal/keybinds/providers/sway_parser.go
Normal file
367
core/internal/keybinds/providers/sway_parser.go
Normal file
@@ -0,0 +1,367 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user