mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-08 12:13:31 -04:00
feat(mangowm): add live config reloads & misc QOL updates
- Hide workspace tags during Mango overview - Add HJKL focus/move defaults - Add Mango natural touchpad scrolling & cursor configs - Fix Mango startup
This commit is contained in:
@@ -520,6 +520,18 @@ func TestHyprlandConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, HyprlandLuaConfig, "input =")
|
||||
}
|
||||
|
||||
func TestMangoConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, MangoConfig, "exec-once=dms run")
|
||||
assert.NotContains(t, MangoConfig, "exec_once=dms run")
|
||||
assert.Contains(t, MangoConfig, "source=./dms/binds.conf")
|
||||
assert.Contains(t, MangoBindsConfig, "bind=SUPER,H,focusdir,left")
|
||||
assert.Contains(t, MangoBindsConfig, "bind=SUPER,J,focusdir,down")
|
||||
assert.Contains(t, MangoBindsConfig, "bind=SUPER,K,focusdir,up")
|
||||
assert.Contains(t, MangoBindsConfig, "bind=SUPER,L,focusdir,right")
|
||||
assert.Contains(t, MangoBindsConfig, "gesturebind=none,right,3,viewtoleft_have_client")
|
||||
assert.Contains(t, MangoBindsConfig, "gesturebind=none,left,3,viewtoright_have_client")
|
||||
}
|
||||
|
||||
func TestGhosttyConfigStructure(t *testing.T) {
|
||||
assert.Contains(t, GhosttyConfig, "window-decoration = false")
|
||||
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
|
||||
# Format: bind=MODS,key,action[,args]
|
||||
# Descriptions go on the line ABOVE each bind (mango does not strip inline
|
||||
# comments — a trailing `# ...` would be passed to spawn as extra arguments).
|
||||
# Put bind descriptions above bind lines; inline # comments break Mango spawn args.
|
||||
|
||||
# === Application Launchers ===
|
||||
# Open Terminal
|
||||
@@ -52,131 +51,90 @@ bind=CTRL,Print,spawn,dms screenshot full
|
||||
bind=ALT,Print,spawn,dms screenshot window
|
||||
|
||||
# === Audio Controls ===
|
||||
# Volume Up
|
||||
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
|
||||
# Volume Down
|
||||
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
|
||||
# Mute Output
|
||||
bind=none,XF86AudioMute,spawn,dms ipc call audio mute
|
||||
# Mute Microphone
|
||||
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
|
||||
# Play/Pause
|
||||
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
|
||||
# Play/Pause
|
||||
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
|
||||
# Previous Track
|
||||
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
|
||||
# Next Track
|
||||
bind=none,XF86AudioNext,spawn,dms ipc call mpris next
|
||||
|
||||
# === Brightness Controls ===
|
||||
# Brightness Up
|
||||
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
|
||||
# Brightness Down
|
||||
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
|
||||
|
||||
# === Window Management ===
|
||||
# Close Window
|
||||
bind=SUPER,q,killclient,
|
||||
# Toggle Fullscreen
|
||||
bind=SUPER,f,togglefullscreen,
|
||||
# Toggle Maximize
|
||||
bind=SUPER,a,togglemaximizescreen,
|
||||
# Toggle Floating
|
||||
bind=SUPER+SHIFT,space,togglefloating,
|
||||
# Toggle Overview
|
||||
bind=SUPER,o,toggleoverview
|
||||
bind=ALT,Tab,toggleoverview
|
||||
# Exit Compositor
|
||||
bind=SUPER+SHIFT,e,quit,
|
||||
|
||||
# === Focus Navigation ===
|
||||
# Focus Next Window
|
||||
bind=SUPER,Tab,focusstack,next
|
||||
# Focus Previous Window
|
||||
bind=SUPER+SHIFT,Tab,focusstack,prev
|
||||
# Focus Left
|
||||
bind=SUPER,Left,focusdir,left
|
||||
# Focus Right
|
||||
bind=SUPER,H,focusdir,left
|
||||
bind=SUPER,Right,focusdir,right
|
||||
# Focus Up
|
||||
bind=SUPER,L,focusdir,right
|
||||
bind=SUPER,Up,focusdir,up
|
||||
# Focus Down
|
||||
bind=SUPER,K,focusdir,up
|
||||
bind=SUPER,Down,focusdir,down
|
||||
bind=SUPER,J,focusdir,down
|
||||
|
||||
# === Window Movement ===
|
||||
# Move Window Left
|
||||
bind=SUPER+SHIFT,Left,exchange_client,left
|
||||
# Move Window Right
|
||||
bind=SUPER+SHIFT,Right,exchange_client,right
|
||||
# Move Window Up
|
||||
bind=SUPER+SHIFT,Up,exchange_client,up
|
||||
# Move Window Down
|
||||
bind=SUPER+SHIFT,Down,exchange_client,down
|
||||
bind=SUPER+SHIFT,H,exchange_client,left
|
||||
bind=SUPER+SHIFT,L,exchange_client,right
|
||||
bind=SUPER+SHIFT,K,exchange_client,up
|
||||
bind=SUPER+SHIFT,J,exchange_client,down
|
||||
|
||||
# === Monitor Navigation ===
|
||||
# Focus Monitor Left
|
||||
bind=SUPER+ALT,Left,focusmon,left
|
||||
# Focus Monitor Right
|
||||
bind=SUPER+ALT,Right,focusmon,right
|
||||
# Move to Monitor Left
|
||||
bind=SUPER+ALT+SHIFT,Left,tagmon,left
|
||||
# Move to Monitor Right
|
||||
bind=SUPER+ALT+SHIFT,Right,tagmon,right
|
||||
|
||||
# === Layout ===
|
||||
# Cycle Layout
|
||||
bind=SUPER,j,switch_layout
|
||||
# Increase Gaps
|
||||
# Cycle Layout - Gaps, Floating, Tiling
|
||||
bind=SUPER+ALT,j,switch_layout
|
||||
bind=SUPER+SHIFT,equal,incgaps,1
|
||||
# Decrease Gaps
|
||||
bind=SUPER+SHIFT,minus,incgaps,-1
|
||||
|
||||
# === Tags (1-9): view tag ===
|
||||
# View Tag 1
|
||||
bind=SUPER,1,view,1
|
||||
# View Tag 2
|
||||
bind=SUPER,2,view,2
|
||||
# View Tag 3
|
||||
bind=SUPER,3,view,3
|
||||
# View Tag 4
|
||||
bind=SUPER,4,view,4
|
||||
# View Tag 5
|
||||
bind=SUPER,5,view,5
|
||||
# View Tag 6
|
||||
bind=SUPER,6,view,6
|
||||
# View Tag 7
|
||||
bind=SUPER,7,view,7
|
||||
# View Tag 8
|
||||
bind=SUPER,8,view,8
|
||||
# View Tag 9
|
||||
bind=SUPER,9,view,9
|
||||
|
||||
# === Tags (1-9): move focused window to tag ===
|
||||
# Move to Tag 1
|
||||
bind=SUPER+SHIFT,1,tag,1
|
||||
# Move to Tag 2
|
||||
bind=SUPER+SHIFT,2,tag,2
|
||||
# Move to Tag 3
|
||||
bind=SUPER+SHIFT,3,tag,3
|
||||
# Move to Tag 4
|
||||
bind=SUPER+SHIFT,4,tag,4
|
||||
# Move to Tag 5
|
||||
bind=SUPER+SHIFT,5,tag,5
|
||||
# Move to Tag 6
|
||||
bind=SUPER+SHIFT,6,tag,6
|
||||
# Move to Tag 7
|
||||
bind=SUPER+SHIFT,7,tag,7
|
||||
# Move to Tag 8
|
||||
bind=SUPER+SHIFT,8,tag,8
|
||||
# Move to Tag 9
|
||||
bind=SUPER+SHIFT,9,tag,9
|
||||
|
||||
# === Touchpad Gestures ===
|
||||
# Syntax: gesturebind=MODIFIERS,DIRECTION,FINGERS,COMMAND,PARAMETERS
|
||||
# 3-finger horizontal swipe: switch between occupied workspaces
|
||||
gesturebind=none,left,3,viewtoleft_have_client
|
||||
gesturebind=none,right,3,viewtoright_have_client
|
||||
gesturebind=none,right,3,viewtoleft_have_client
|
||||
gesturebind=none,left,3,viewtoright_have_client
|
||||
# 4-finger vertical swipe: toggle the overview
|
||||
gesturebind=none,up,4,toggleoverview
|
||||
gesturebind=none,down,4,toggleoverview
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
env=XDG_CURRENT_DESKTOP,mango
|
||||
env=XDG_SESSION_TYPE,wayland
|
||||
|
||||
# exec_once runs only at startup. Do NOT use exec= for the shell: mango re-runs
|
||||
# exec-once runs only at startup. Do NOT use exec= for the shell: mango re-runs
|
||||
# every exec= on each config reload, and DMS reloads the config, which would
|
||||
# spawn a new shell on every reload.
|
||||
exec_once=dms run
|
||||
exec-once=dms run
|
||||
|
||||
source=./dms/colors.conf
|
||||
source=./dms/layout.conf
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
|
||||
)
|
||||
@@ -228,11 +229,20 @@ func (m *MangoWCProvider) SetBind(key, action, description string, options map[s
|
||||
}
|
||||
|
||||
normalizedKey := strings.ToLower(key)
|
||||
prefix := "bind"
|
||||
if existing, ok := existingBinds[normalizedKey]; ok && existing.Prefix != "" {
|
||||
prefix = existing.Prefix
|
||||
}
|
||||
if optionPrefix := m.bindPrefixFromOptions(options); optionPrefix != "" {
|
||||
prefix = optionPrefix
|
||||
}
|
||||
|
||||
existingBinds[normalizedKey] = &mangowcOverrideBind{
|
||||
Key: key,
|
||||
Action: action,
|
||||
Description: description,
|
||||
Options: options,
|
||||
Prefix: prefix,
|
||||
}
|
||||
|
||||
return m.writeOverrideBinds(existingBinds)
|
||||
@@ -246,7 +256,7 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
|
||||
|
||||
normalizedKey := strings.ToLower(key)
|
||||
delete(existingBinds, normalizedKey)
|
||||
return m.writeOverrideBinds(existingBinds)
|
||||
return m.writeOverrideBindsWithRemoved(existingBinds, map[string]bool{normalizedKey: true})
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) ResetBind(key string) error {
|
||||
@@ -258,6 +268,7 @@ type mangowcOverrideBind struct {
|
||||
Action string
|
||||
Description string
|
||||
Options map[string]any
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
|
||||
@@ -272,62 +283,99 @@ func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
var pendingComment string
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
pendingComment = ""
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
if isMangoWCSectionComment(pendingComment) {
|
||||
pendingComment = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(line, "bind") {
|
||||
bind, ok := m.parseOverrideBindLine(line, pendingComment)
|
||||
pendingComment = ""
|
||||
if !ok || bind == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(parts[1])
|
||||
commentParts := strings.SplitN(content, "#", 2)
|
||||
bindContent := strings.TrimSpace(commentParts[0])
|
||||
|
||||
var comment string
|
||||
if len(commentParts) > 1 {
|
||||
comment = strings.TrimSpace(commentParts[1])
|
||||
}
|
||||
|
||||
fields := strings.SplitN(bindContent, ",", 4)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(fields[0])
|
||||
keyName := strings.TrimSpace(fields[1])
|
||||
command := strings.TrimSpace(fields[2])
|
||||
|
||||
var params string
|
||||
if len(fields) > 3 {
|
||||
params = strings.TrimSpace(fields[3])
|
||||
}
|
||||
|
||||
keyStr := m.buildKeyString(mods, keyName)
|
||||
normalizedKey := strings.ToLower(keyStr)
|
||||
action := command
|
||||
if params != "" {
|
||||
action = command + " " + params
|
||||
}
|
||||
|
||||
binds[normalizedKey] = &mangowcOverrideBind{
|
||||
Key: keyStr,
|
||||
Action: action,
|
||||
Description: comment,
|
||||
}
|
||||
binds[strings.ToLower(bind.Key)] = bind
|
||||
}
|
||||
|
||||
return binds, nil
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) parseOverrideBindLine(line, precedingComment string) (*mangowcOverrideBind, bool) {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) < 2 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
prefix := strings.TrimSpace(parts[0])
|
||||
if !m.isBindPrefix(prefix) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
content := strings.TrimSpace(parts[1])
|
||||
commentParts := strings.SplitN(content, "#", 2)
|
||||
bindContent := strings.TrimSpace(commentParts[0])
|
||||
|
||||
description := strings.TrimSpace(precedingComment)
|
||||
if isMangoWCSectionComment(description) {
|
||||
description = ""
|
||||
}
|
||||
if len(commentParts) > 1 {
|
||||
description = strings.TrimSpace(commentParts[1])
|
||||
}
|
||||
if strings.HasPrefix(description, MangoWCHideComment) {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
fields := strings.SplitN(bindContent, ",", 4)
|
||||
if len(fields) < 3 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(fields[0])
|
||||
keyName := strings.TrimSpace(fields[1])
|
||||
command := strings.TrimSpace(fields[2])
|
||||
|
||||
var params string
|
||||
if len(fields) > 3 {
|
||||
params = strings.TrimSpace(fields[3])
|
||||
}
|
||||
|
||||
action := command
|
||||
if params != "" {
|
||||
action = command + " " + params
|
||||
}
|
||||
|
||||
return &mangowcOverrideBind{
|
||||
Key: m.buildKeyString(mods, keyName),
|
||||
Action: action,
|
||||
Description: description,
|
||||
Prefix: prefix,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) isBindPrefix(prefix string) bool {
|
||||
if !strings.HasPrefix(prefix, "bind") {
|
||||
return false
|
||||
}
|
||||
for _, ch := range strings.TrimPrefix(prefix, "bind") {
|
||||
if !strings.ContainsRune("lsrp", ch) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) buildKeyString(mods, key string) string {
|
||||
if mods == "" || strings.EqualFold(mods, "none") {
|
||||
return key
|
||||
@@ -362,21 +410,113 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error {
|
||||
return m.writeOverrideBindsWithRemoved(binds, nil)
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) writeOverrideBindsWithRemoved(binds map[string]*mangowcOverrideBind, removed map[string]bool) error {
|
||||
overridePath := m.GetOverridePath()
|
||||
content := m.generateBindsContent(binds)
|
||||
existingContent := ""
|
||||
if data, err := os.ReadFile(overridePath); err == nil {
|
||||
existingContent = string(data)
|
||||
}
|
||||
|
||||
content := m.generatePreservedBindsContent(existingContent, binds, removed)
|
||||
return os.WriteFile(overridePath, []byte(content), 0o644)
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string {
|
||||
if len(binds) == 0 {
|
||||
return ""
|
||||
func (m *MangoWCProvider) generatePreservedBindsContent(existingContent string, binds map[string]*mangowcOverrideBind, removed map[string]bool) string {
|
||||
useStockScaffold := m.shouldUseStockScaffold(existingContent)
|
||||
source := existingContent
|
||||
if useStockScaffold {
|
||||
source = m.stockBindsScaffold(binds)
|
||||
}
|
||||
|
||||
remaining := make(map[string]*mangowcOverrideBind, len(binds))
|
||||
for key, bind := range binds {
|
||||
remaining[key] = bind
|
||||
}
|
||||
if useStockScaffold {
|
||||
m.dropReplacedStockBinds(remaining)
|
||||
}
|
||||
|
||||
var lines []string
|
||||
for _, line := range strings.Split(source, "\n") {
|
||||
templateBind, ok := m.parseOverrideBindLine(line, m.previousComment(lines))
|
||||
if !ok || templateBind == nil {
|
||||
lines = append(lines, line)
|
||||
continue
|
||||
}
|
||||
|
||||
normalizedKey := strings.ToLower(templateBind.Key)
|
||||
m.dropPreviousDescriptionComment(&lines)
|
||||
|
||||
if bind, exists := remaining[normalizedKey]; exists {
|
||||
if useStockScaffold && bind.Description == "" {
|
||||
bind = m.copyBindWithDescription(bind, templateBind.Description)
|
||||
}
|
||||
m.writeBindLineToLines(&lines, bind)
|
||||
delete(remaining, normalizedKey)
|
||||
continue
|
||||
}
|
||||
|
||||
if useStockScaffold && !removed[normalizedKey] {
|
||||
m.writeBindLineToLines(&lines, templateBind)
|
||||
}
|
||||
}
|
||||
|
||||
if len(remaining) > 0 {
|
||||
m.trimTrailingEmptyLines(&lines)
|
||||
if len(lines) > 0 {
|
||||
lines = append(lines, "")
|
||||
}
|
||||
lines = append(lines, "# === Custom Keybinds ===")
|
||||
for _, bind := range m.sortedBinds(remaining) {
|
||||
m.writeBindLineToLines(&lines, bind)
|
||||
}
|
||||
}
|
||||
|
||||
m.trimTrailingEmptyLines(&lines)
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.Join(lines, "\n") + "\n"
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) shouldUseStockScaffold(content string) bool {
|
||||
if strings.TrimSpace(content) == "" {
|
||||
return true
|
||||
}
|
||||
if strings.Contains(content, "gesturebind=") && strings.Contains(content, "# ===") {
|
||||
return false
|
||||
}
|
||||
return !strings.Contains(content, "gesturebind=") && (strings.Count(content, "\nbind=")+strings.Count(content, "\nbindl=")+strings.Count(content, "\nbinds=")+strings.Count(content, "\nbindr=")+strings.Count(content, "\nbindp=") >= 10 || strings.Contains(content, "dms ipc call"))
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) stockBindsScaffold(binds map[string]*mangowcOverrideBind) string {
|
||||
terminalCommand := "ghostty"
|
||||
for _, key := range []string{"super+t", "super+return"} {
|
||||
if bind, ok := binds[key]; ok {
|
||||
command, params := m.parseAction(bind.Action)
|
||||
if command == "spawn" && strings.TrimSpace(params) != "" && !strings.Contains(params, "dms ") {
|
||||
terminalCommand = params
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) dropReplacedStockBinds(binds map[string]*mangowcOverrideBind) {
|
||||
if bind, ok := binds["super+j"]; ok && bind.Action == "switch_layout" {
|
||||
delete(binds, "super+j")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) sortedBinds(binds map[string]*mangowcOverrideBind) []*mangowcOverrideBind {
|
||||
bindList := make([]*mangowcOverrideBind, 0, len(binds))
|
||||
for _, bind := range binds {
|
||||
bindList = append(bindList, bind)
|
||||
}
|
||||
|
||||
sort.Slice(bindList, func(i, j int) bool {
|
||||
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
|
||||
if pi != pj {
|
||||
@@ -384,13 +524,55 @@ func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverride
|
||||
}
|
||||
return bindList[i].Key < bindList[j].Key
|
||||
})
|
||||
return bindList
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) writeBindLineToLines(lines *[]string, bind *mangowcOverrideBind) {
|
||||
var sb strings.Builder
|
||||
for _, bind := range bindList {
|
||||
m.writeBindLine(&sb, bind)
|
||||
m.writeBindLine(&sb, bind)
|
||||
text := strings.TrimSuffix(sb.String(), "\n")
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
*lines = append(*lines, strings.Split(text, "\n")...)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
func (m *MangoWCProvider) previousComment(lines []string) string {
|
||||
if len(lines) == 0 {
|
||||
return ""
|
||||
}
|
||||
trimmed := strings.TrimSpace(lines[len(lines)-1])
|
||||
if !strings.HasPrefix(trimmed, "#") {
|
||||
return ""
|
||||
}
|
||||
comment := strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
if isMangoWCSectionComment(comment) {
|
||||
return ""
|
||||
}
|
||||
return comment
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) dropPreviousDescriptionComment(lines *[]string) {
|
||||
if len(*lines) == 0 {
|
||||
return
|
||||
}
|
||||
trimmed := strings.TrimSpace((*lines)[len(*lines)-1])
|
||||
if !strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "# ===") {
|
||||
return
|
||||
}
|
||||
*lines = (*lines)[:len(*lines)-1]
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) trimTrailingEmptyLines(lines *[]string) {
|
||||
for len(*lines) > 0 && strings.TrimSpace((*lines)[len(*lines)-1]) == "" {
|
||||
*lines = (*lines)[:len(*lines)-1]
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) copyBindWithDescription(bind *mangowcOverrideBind, description string) *mangowcOverrideBind {
|
||||
copy := *bind
|
||||
copy.Description = description
|
||||
return ©
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) {
|
||||
@@ -405,7 +587,12 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("bind=")
|
||||
prefix := bind.Prefix
|
||||
if prefix == "" {
|
||||
prefix = "bind"
|
||||
}
|
||||
sb.WriteString(prefix)
|
||||
sb.WriteString("=")
|
||||
if mods == "" {
|
||||
sb.WriteString("none")
|
||||
} else {
|
||||
@@ -424,6 +611,36 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) bindPrefixFromOptions(options map[string]any) string {
|
||||
if options == nil {
|
||||
return ""
|
||||
}
|
||||
value, ok := options["flags"]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
flags := ""
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
flags = v
|
||||
case fmt.Stringer:
|
||||
flags = v.String()
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
flags = strings.TrimSpace(flags)
|
||||
if flags == "" {
|
||||
return "bind"
|
||||
}
|
||||
var clean strings.Builder
|
||||
for _, ch := range flags {
|
||||
if strings.ContainsRune("lsrp", ch) && !strings.ContainsRune(clean.String(), ch) {
|
||||
clean.WriteRune(ch)
|
||||
}
|
||||
}
|
||||
return "bind" + clean.String()
|
||||
}
|
||||
|
||||
func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
|
||||
parts := strings.Split(keyStr, "+")
|
||||
switch len(parts) {
|
||||
|
||||
@@ -15,6 +15,10 @@ const (
|
||||
|
||||
var MangoWCModSeparators = []rune{'+', ' '}
|
||||
|
||||
func isMangoWCSectionComment(comment string) bool {
|
||||
return strings.HasPrefix(strings.TrimSpace(comment), "===")
|
||||
}
|
||||
|
||||
type MangoWCKeyBinding struct {
|
||||
Mods []string `json:"mods"`
|
||||
Key string `json:"key"`
|
||||
@@ -235,6 +239,9 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
if isMangoWCSectionComment(pendingComment) {
|
||||
pendingComment = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(trimmed, "bind") {
|
||||
@@ -414,6 +421,9 @@ func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBindin
|
||||
|
||||
if strings.HasPrefix(trimmed, "#") {
|
||||
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
|
||||
if isMangoWCSectionComment(pendingComment) {
|
||||
pendingComment = ""
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -483,7 +493,7 @@ func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyB
|
||||
// line directly above) is the description: mango feeds inline comments to spawn
|
||||
// as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
|
||||
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding {
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsr]*)\s*=\s*(.+)$`)
|
||||
bindMatch := regexp.MustCompile(`^(bind[lsrp]*)\s*=\s*(.+)$`)
|
||||
matches := bindMatch.FindStringSubmatch(line)
|
||||
if len(matches) < 3 {
|
||||
return nil
|
||||
@@ -499,6 +509,9 @@ func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment st
|
||||
}
|
||||
if comment == "" {
|
||||
comment = strings.TrimSpace(precedingComment)
|
||||
if isMangoWCSectionComment(comment) {
|
||||
comment = ""
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(comment, MangoWCHideComment) {
|
||||
|
||||
@@ -71,9 +71,10 @@ func TestMangoWCAutogenerateComment(t *testing.T) {
|
||||
|
||||
func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
expected *MangoWCKeyBinding
|
||||
name string
|
||||
line string
|
||||
precedingComment string
|
||||
expected *MangoWCKeyBinding
|
||||
}{
|
||||
{
|
||||
name: "basic_keybind",
|
||||
@@ -157,6 +158,41 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
||||
Comment: "dms ipc call lock lock",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bindp_flag",
|
||||
line: "bindp=SUPER,p,spawn,pass-through",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER"},
|
||||
Key: "p",
|
||||
Command: "spawn",
|
||||
Params: "pass-through",
|
||||
Comment: "pass-through",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "preceding_comment",
|
||||
line: "bind=SUPER+SHIFT,S,spawn,dms screenshot",
|
||||
precedingComment: "Screenshot: Interactive",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{"SUPER", "SHIFT"},
|
||||
Key: "S",
|
||||
Command: "spawn",
|
||||
Params: "dms screenshot",
|
||||
Comment: "Screenshot: Interactive",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "section_header_not_description",
|
||||
line: "bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3",
|
||||
precedingComment: "=== Audio Controls ===",
|
||||
expected: &MangoWCKeyBinding{
|
||||
Mods: []string{},
|
||||
Key: "XF86AudioRaiseVolume",
|
||||
Command: "spawn",
|
||||
Params: "dms ipc call audio increment 3",
|
||||
Comment: "dms ipc call audio increment 3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "keybind_with_spaces",
|
||||
line: "bind = SUPER, r, reload_config",
|
||||
@@ -174,7 +210,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := NewMangoWCParser("")
|
||||
parser.contentLines = []string{tt.line}
|
||||
result := parser.getKeybindAtLine(0, "")
|
||||
result := parser.getKeybindAtLine(0, tt.precedingComment)
|
||||
|
||||
if tt.expected == nil {
|
||||
if result != nil {
|
||||
|
||||
@@ -3,7 +3,10 @@ package providers
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
|
||||
)
|
||||
|
||||
func TestMangoWCProviderName(t *testing.T) {
|
||||
@@ -318,3 +321,138 @@ bind=Ctrl,1,view,1,0
|
||||
t.Error("Did not find terminal keybind with correct key and description")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCSetBindPreservesStockCommentsAndGestures(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create dms dir: %v", err)
|
||||
}
|
||||
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||
stock := strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", "ghostty")
|
||||
if err := os.WriteFile(bindsPath, []byte(stock), 0o644); err != nil {
|
||||
t.Fatalf("failed to write stock binds: %v", err)
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider(tmpDir)
|
||||
if err := provider.SetBind("SUPER+SHIFT+S", "spawn dms screenshot", "Screenshot: Interactive", nil); err != nil {
|
||||
t.Fatalf("SetBind failed: %v", err)
|
||||
}
|
||||
|
||||
contentBytes, err := os.ReadFile(bindsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read binds: %v", err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
for _, want := range []string{
|
||||
"# === Application Launchers ===",
|
||||
"# === Touchpad Gestures ===",
|
||||
"gesturebind=none,right,3,viewtoleft_have_client",
|
||||
"gesturebind=none,left,3,viewtoright_have_client",
|
||||
"# Screenshot: Interactive\nbind=SUPER+SHIFT,S,spawn,dms screenshot",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("expected saved binds to contain %q\ncontent:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
if strings.Contains(content, "# === Audio Controls ===\n# === Audio Controls ===") {
|
||||
t.Fatalf("section header should not be duplicated as a bind description\ncontent:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCSetBindRestoresScaffoldForStrippedFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create dms dir: %v", err)
|
||||
}
|
||||
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||
stripped := `bind=SUPER,t,spawn,ghostty
|
||||
bind=SUPER,Return,spawn,ghostty
|
||||
bind=SUPER,space,spawn,dms ipc call spotlight toggle
|
||||
bind=SUPER,v,spawn,dms ipc call clipboard toggle
|
||||
bind=SUPER,q,killclient
|
||||
bind=SUPER,Left,focusdir,left
|
||||
bind=SUPER,Right,focusdir,right
|
||||
bind=SUPER,Up,focusdir,up
|
||||
bind=SUPER,Down,focusdir,down
|
||||
bind=SUPER,1,view,1
|
||||
bind=SUPER,2,view,2
|
||||
bind=SUPER,3,view,3
|
||||
`
|
||||
if err := os.WriteFile(bindsPath, []byte(stripped), 0o644); err != nil {
|
||||
t.Fatalf("failed to write stripped binds: %v", err)
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider(tmpDir)
|
||||
if err := provider.SetBind("SUPER+SHIFT+S", "spawn dms screenshot", "Screenshot: Interactive", nil); err != nil {
|
||||
t.Fatalf("SetBind failed: %v", err)
|
||||
}
|
||||
|
||||
contentBytes, err := os.ReadFile(bindsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read binds: %v", err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
for _, want := range []string{
|
||||
"# DMS default keybinds (MangoWM)",
|
||||
"# === Touchpad Gestures ===",
|
||||
"gesturebind=none,right,3,viewtoleft_have_client",
|
||||
"bind=SUPER,H,focusdir,left",
|
||||
"bind=SUPER,J,focusdir,down",
|
||||
"bind=SUPER,K,focusdir,up",
|
||||
"bind=SUPER,L,focusdir,right",
|
||||
"# === Custom Keybinds ===",
|
||||
"# Screenshot: Interactive\nbind=SUPER+SHIFT,S,spawn,dms screenshot",
|
||||
"bind=SUPER,t,spawn,ghostty",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("expected restored binds to contain %q\ncontent:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
if strings.Contains(content, "{{TERMINAL_COMMAND}}") {
|
||||
t.Fatalf("terminal placeholder should have been resolved\ncontent:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMangoWCRemoveBindPreservesNonBindLines(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dmsDir := filepath.Join(tmpDir, "dms")
|
||||
if err := os.MkdirAll(dmsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create dms dir: %v", err)
|
||||
}
|
||||
bindsPath := filepath.Join(dmsDir, "binds.conf")
|
||||
stock := strings.ReplaceAll(config.MangoBindsConfig, "{{TERMINAL_COMMAND}}", "ghostty")
|
||||
if err := os.WriteFile(bindsPath, []byte(stock), 0o644); err != nil {
|
||||
t.Fatalf("failed to write stock binds: %v", err)
|
||||
}
|
||||
|
||||
provider := NewMangoWCProvider(tmpDir)
|
||||
if err := provider.RemoveBind("SUPER+Tab"); err != nil {
|
||||
t.Fatalf("RemoveBind failed: %v", err)
|
||||
}
|
||||
|
||||
contentBytes, err := os.ReadFile(bindsPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read binds: %v", err)
|
||||
}
|
||||
content := string(contentBytes)
|
||||
|
||||
if strings.Contains(content, "bind=SUPER,Tab,focusstack,next") {
|
||||
t.Fatalf("removed bind should be absent\ncontent:\n%s", content)
|
||||
}
|
||||
if strings.Contains(content, "# Focus Next Window") {
|
||||
t.Fatalf("removed bind description should be absent\ncontent:\n%s", content)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"# === Focus Navigation ===",
|
||||
"# === Touchpad Gestures ===",
|
||||
"gesturebind=none,down,4,toggleoverview",
|
||||
} {
|
||||
if !strings.Contains(content, want) {
|
||||
t.Fatalf("expected non-bind line %q to be preserved\ncontent:\n%s", want, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ func (m Model) viewInstallComplete() string {
|
||||
|
||||
wm := m.selectedWindowManager()
|
||||
|
||||
// mango launches DMS via `exec_once=dms run` (not a systemd session target)
|
||||
// mango launches DMS via `exec-once=dms run` (not a systemd session target)
|
||||
loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
|
||||
switch wm {
|
||||
case deps.WindowManagerNiri:
|
||||
@@ -223,7 +223,7 @@ func (m Model) viewInstallComplete() string {
|
||||
|
||||
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
|
||||
if wm == deps.WindowManagerMango {
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec_once=dms run' from ~/.config/mango/config.conf") + "\n")
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("remove 'exec-once=dms run' from ~/.config/mango/config.conf") + "\n")
|
||||
b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
|
||||
} else {
|
||||
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
|
||||
|
||||
Reference in New Issue
Block a user