1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-07 19:59:14 -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:
purian23
2026-06-05 10:53:26 -04:00
parent bcb5617194
commit e50ac208e3
15 changed files with 618 additions and 138 deletions
+8 -1
View File
@@ -294,7 +294,14 @@ func runSetup() error {
wm, wmSelected := promptCompositor() wm, wmSelected := promptCompositor()
terminal, terminalSelected := promptTerminal() terminal, terminalSelected := promptTerminal()
useSystemd := promptSystemd() useSystemd := true
if wmSelected {
if wm == deps.WindowManagerMango {
useSystemd = false
} else {
useSystemd = promptSystemd()
}
}
if !wmSelected && !terminalSelected { if !wmSelected && !terminalSelected {
fmt.Println("No configurations selected. Exiting.") fmt.Println("No configurations selected. Exiting.")
+12
View File
@@ -520,6 +520,18 @@ func TestHyprlandConfigStructure(t *testing.T) {
assert.Contains(t, HyprlandLuaConfig, "input =") 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) { func TestGhosttyConfigStructure(t *testing.T) {
assert.Contains(t, GhosttyConfig, "window-decoration = false") assert.Contains(t, GhosttyConfig, "window-decoration = false")
assert.Contains(t, GhosttyConfig, "background-opacity = 1.0") assert.Contains(t, GhosttyConfig, "background-opacity = 1.0")
+13 -55
View File
@@ -1,7 +1,6 @@
# DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`. # DMS default keybinds (MangoWM) — managed by DMS, regenerated by `dms setup`.
# Format: bind=MODS,key,action[,args] # Format: bind=MODS,key,action[,args]
# Descriptions go on the line ABOVE each bind (mango does not strip inline # Put bind descriptions above bind lines; inline # comments break Mango spawn args.
# comments — a trailing `# ...` would be passed to spawn as extra arguments).
# === Application Launchers === # === Application Launchers ===
# Open Terminal # Open Terminal
@@ -52,131 +51,90 @@ bind=CTRL,Print,spawn,dms screenshot full
bind=ALT,Print,spawn,dms screenshot window bind=ALT,Print,spawn,dms screenshot window
# === Audio Controls === # === Audio Controls ===
# Volume Up
bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3 bind=none,XF86AudioRaiseVolume,spawn,dms ipc call audio increment 3
# Volume Down
bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3 bind=none,XF86AudioLowerVolume,spawn,dms ipc call audio decrement 3
# Mute Output
bind=none,XF86AudioMute,spawn,dms ipc call audio mute bind=none,XF86AudioMute,spawn,dms ipc call audio mute
# Mute Microphone
bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute bind=none,XF86AudioMicMute,spawn,dms ipc call audio micmute
# Play/Pause
bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause bind=none,XF86AudioPlay,spawn,dms ipc call mpris playPause
# Play/Pause
bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause bind=none,XF86AudioPause,spawn,dms ipc call mpris playPause
# Previous Track
bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous bind=none,XF86AudioPrev,spawn,dms ipc call mpris previous
# Next Track
bind=none,XF86AudioNext,spawn,dms ipc call mpris next bind=none,XF86AudioNext,spawn,dms ipc call mpris next
# === Brightness Controls === # === Brightness Controls ===
# Brightness Up
bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5 bind=none,XF86MonBrightnessUp,spawn,dms ipc call brightness increment 5
# Brightness Down
bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5 bind=none,XF86MonBrightnessDown,spawn,dms ipc call brightness decrement 5
# === Window Management === # === Window Management ===
# Close Window # Close Window
bind=SUPER,q,killclient, bind=SUPER,q,killclient,
# Toggle Fullscreen
bind=SUPER,f,togglefullscreen, bind=SUPER,f,togglefullscreen,
# Toggle Maximize
bind=SUPER,a,togglemaximizescreen, bind=SUPER,a,togglemaximizescreen,
# Toggle Floating
bind=SUPER+SHIFT,space,togglefloating, bind=SUPER+SHIFT,space,togglefloating,
# Toggle Overview
bind=SUPER,o,toggleoverview bind=SUPER,o,toggleoverview
bind=ALT,Tab,toggleoverview bind=ALT,Tab,toggleoverview
# Exit Compositor # Exit Compositor
bind=SUPER+SHIFT,e,quit, bind=SUPER+SHIFT,e,quit,
# === Focus Navigation === # === Focus Navigation ===
# Focus Next Window
bind=SUPER,Tab,focusstack,next bind=SUPER,Tab,focusstack,next
# Focus Previous Window
bind=SUPER+SHIFT,Tab,focusstack,prev bind=SUPER+SHIFT,Tab,focusstack,prev
# Focus Left
bind=SUPER,Left,focusdir,left bind=SUPER,Left,focusdir,left
# Focus Right bind=SUPER,H,focusdir,left
bind=SUPER,Right,focusdir,right bind=SUPER,Right,focusdir,right
# Focus Up bind=SUPER,L,focusdir,right
bind=SUPER,Up,focusdir,up bind=SUPER,Up,focusdir,up
# Focus Down bind=SUPER,K,focusdir,up
bind=SUPER,Down,focusdir,down bind=SUPER,Down,focusdir,down
bind=SUPER,J,focusdir,down
# === Window Movement === # === Window Movement ===
# Move Window Left
bind=SUPER+SHIFT,Left,exchange_client,left bind=SUPER+SHIFT,Left,exchange_client,left
# Move Window Right
bind=SUPER+SHIFT,Right,exchange_client,right bind=SUPER+SHIFT,Right,exchange_client,right
# Move Window Up
bind=SUPER+SHIFT,Up,exchange_client,up bind=SUPER+SHIFT,Up,exchange_client,up
# Move Window Down
bind=SUPER+SHIFT,Down,exchange_client,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 === # === Monitor Navigation ===
# Focus Monitor Left
bind=SUPER+ALT,Left,focusmon,left bind=SUPER+ALT,Left,focusmon,left
# Focus Monitor Right
bind=SUPER+ALT,Right,focusmon,right bind=SUPER+ALT,Right,focusmon,right
# Move to Monitor Left
bind=SUPER+ALT+SHIFT,Left,tagmon,left bind=SUPER+ALT+SHIFT,Left,tagmon,left
# Move to Monitor Right
bind=SUPER+ALT+SHIFT,Right,tagmon,right bind=SUPER+ALT+SHIFT,Right,tagmon,right
# === Layout === # === Layout ===
# Cycle Layout # Cycle Layout - Gaps, Floating, Tiling
bind=SUPER,j,switch_layout bind=SUPER+ALT,j,switch_layout
# Increase Gaps
bind=SUPER+SHIFT,equal,incgaps,1 bind=SUPER+SHIFT,equal,incgaps,1
# Decrease Gaps
bind=SUPER+SHIFT,minus,incgaps,-1 bind=SUPER+SHIFT,minus,incgaps,-1
# === Tags (1-9): view tag === # === Tags (1-9): view tag ===
# View Tag 1
bind=SUPER,1,view,1 bind=SUPER,1,view,1
# View Tag 2
bind=SUPER,2,view,2 bind=SUPER,2,view,2
# View Tag 3
bind=SUPER,3,view,3 bind=SUPER,3,view,3
# View Tag 4
bind=SUPER,4,view,4 bind=SUPER,4,view,4
# View Tag 5
bind=SUPER,5,view,5 bind=SUPER,5,view,5
# View Tag 6
bind=SUPER,6,view,6 bind=SUPER,6,view,6
# View Tag 7
bind=SUPER,7,view,7 bind=SUPER,7,view,7
# View Tag 8
bind=SUPER,8,view,8 bind=SUPER,8,view,8
# View Tag 9
bind=SUPER,9,view,9 bind=SUPER,9,view,9
# === Tags (1-9): move focused window to tag === # === Tags (1-9): move focused window to tag ===
# Move to Tag 1
bind=SUPER+SHIFT,1,tag,1 bind=SUPER+SHIFT,1,tag,1
# Move to Tag 2
bind=SUPER+SHIFT,2,tag,2 bind=SUPER+SHIFT,2,tag,2
# Move to Tag 3
bind=SUPER+SHIFT,3,tag,3 bind=SUPER+SHIFT,3,tag,3
# Move to Tag 4
bind=SUPER+SHIFT,4,tag,4 bind=SUPER+SHIFT,4,tag,4
# Move to Tag 5
bind=SUPER+SHIFT,5,tag,5 bind=SUPER+SHIFT,5,tag,5
# Move to Tag 6
bind=SUPER+SHIFT,6,tag,6 bind=SUPER+SHIFT,6,tag,6
# Move to Tag 7
bind=SUPER+SHIFT,7,tag,7 bind=SUPER+SHIFT,7,tag,7
# Move to Tag 8
bind=SUPER+SHIFT,8,tag,8 bind=SUPER+SHIFT,8,tag,8
# Move to Tag 9
bind=SUPER+SHIFT,9,tag,9 bind=SUPER+SHIFT,9,tag,9
# === Touchpad Gestures === # === Touchpad Gestures ===
# Syntax: gesturebind=MODIFIERS,DIRECTION,FINGERS,COMMAND,PARAMETERS
# 3-finger horizontal swipe: switch between occupied workspaces # 3-finger horizontal swipe: switch between occupied workspaces
gesturebind=none,left,3,viewtoleft_have_client gesturebind=none,right,3,viewtoleft_have_client
gesturebind=none,right,3,viewtoright_have_client gesturebind=none,left,3,viewtoright_have_client
# 4-finger vertical swipe: toggle the overview # 4-finger vertical swipe: toggle the overview
gesturebind=none,up,4,toggleoverview gesturebind=none,up,4,toggleoverview
gesturebind=none,down,4,toggleoverview gesturebind=none,down,4,toggleoverview
+2 -2
View File
@@ -5,10 +5,10 @@
env=XDG_CURRENT_DESKTOP,mango env=XDG_CURRENT_DESKTOP,mango
env=XDG_SESSION_TYPE,wayland 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 # every exec= on each config reload, and DMS reloads the config, which would
# spawn a new shell on every reload. # spawn a new shell on every reload.
exec_once=dms run exec-once=dms run
source=./dms/colors.conf source=./dms/colors.conf
source=./dms/layout.conf source=./dms/layout.conf
+272 -55
View File
@@ -7,6 +7,7 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
"github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds" "github.com/AvengeMedia/DankMaterialShell/core/internal/keybinds"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils" "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) 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{ existingBinds[normalizedKey] = &mangowcOverrideBind{
Key: key, Key: key,
Action: action, Action: action,
Description: description, Description: description,
Options: options, Options: options,
Prefix: prefix,
} }
return m.writeOverrideBinds(existingBinds) return m.writeOverrideBinds(existingBinds)
@@ -246,7 +256,7 @@ func (m *MangoWCProvider) RemoveBind(key string) error {
normalizedKey := strings.ToLower(key) normalizedKey := strings.ToLower(key)
delete(existingBinds, normalizedKey) delete(existingBinds, normalizedKey)
return m.writeOverrideBinds(existingBinds) return m.writeOverrideBindsWithRemoved(existingBinds, map[string]bool{normalizedKey: true})
} }
func (m *MangoWCProvider) ResetBind(key string) error { func (m *MangoWCProvider) ResetBind(key string) error {
@@ -258,6 +268,7 @@ type mangowcOverrideBind struct {
Action string Action string
Description string Description string
Options map[string]any Options map[string]any
Prefix string
} }
func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) { func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind, error) {
@@ -272,62 +283,99 @@ func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind,
return nil, err return nil, err
} }
lines := strings.Split(string(data), "\n") var pendingComment string
for _, line := range lines { for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") { if trimmed == "" {
pendingComment = ""
continue
}
if strings.HasPrefix(trimmed, "#") {
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(pendingComment) {
pendingComment = ""
}
continue continue
} }
if !strings.HasPrefix(line, "bind") { bind, ok := m.parseOverrideBindLine(line, pendingComment)
pendingComment = ""
if !ok || bind == nil {
continue continue
} }
parts := strings.SplitN(line, "=", 2) binds[strings.ToLower(bind.Key)] = bind
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,
}
} }
return binds, nil 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 { func (m *MangoWCProvider) buildKeyString(mods, key string) string {
if mods == "" || strings.EqualFold(mods, "none") { if mods == "" || strings.EqualFold(mods, "none") {
return key return key
@@ -362,21 +410,113 @@ func (m *MangoWCProvider) getBindSortPriority(action string) int {
} }
func (m *MangoWCProvider) writeOverrideBinds(binds map[string]*mangowcOverrideBind) error { 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() 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) return os.WriteFile(overridePath, []byte(content), 0o644)
} }
func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverrideBind) string { func (m *MangoWCProvider) generatePreservedBindsContent(existingContent string, binds map[string]*mangowcOverrideBind, removed map[string]bool) string {
if len(binds) == 0 { useStockScaffold := m.shouldUseStockScaffold(existingContent)
return "" 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)) bindList := make([]*mangowcOverrideBind, 0, len(binds))
for _, bind := range binds { for _, bind := range binds {
bindList = append(bindList, bind) bindList = append(bindList, bind)
} }
sort.Slice(bindList, func(i, j int) bool { sort.Slice(bindList, func(i, j int) bool {
pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action) pi, pj := m.getBindSortPriority(bindList[i].Action), m.getBindSortPriority(bindList[j].Action)
if pi != pj { if pi != pj {
@@ -384,13 +524,55 @@ func (m *MangoWCProvider) generateBindsContent(binds map[string]*mangowcOverride
} }
return bindList[i].Key < bindList[j].Key return bindList[i].Key < bindList[j].Key
}) })
return bindList
}
func (m *MangoWCProvider) writeBindLineToLines(lines *[]string, bind *mangowcOverrideBind) {
var sb strings.Builder 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 &copy
} }
func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverrideBind) { 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("\n")
} }
sb.WriteString("bind=") prefix := bind.Prefix
if prefix == "" {
prefix = "bind"
}
sb.WriteString(prefix)
sb.WriteString("=")
if mods == "" { if mods == "" {
sb.WriteString("none") sb.WriteString("none")
} else { } else {
@@ -424,6 +611,36 @@ func (m *MangoWCProvider) writeBindLine(sb *strings.Builder, bind *mangowcOverri
sb.WriteString("\n") 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) { func (m *MangoWCProvider) parseKeyString(keyStr string) (mods, key string) {
parts := strings.Split(keyStr, "+") parts := strings.Split(keyStr, "+")
switch len(parts) { switch len(parts) {
@@ -15,6 +15,10 @@ const (
var MangoWCModSeparators = []rune{'+', ' '} var MangoWCModSeparators = []rune{'+', ' '}
func isMangoWCSectionComment(comment string) bool {
return strings.HasPrefix(strings.TrimSpace(comment), "===")
}
type MangoWCKeyBinding struct { type MangoWCKeyBinding struct {
Mods []string `json:"mods"` Mods []string `json:"mods"`
Key string `json:"key"` Key string `json:"key"`
@@ -235,6 +239,9 @@ func (p *MangoWCParser) ParseKeys() []MangoWCKeyBinding {
} }
if strings.HasPrefix(trimmed, "#") { if strings.HasPrefix(trimmed, "#") {
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#")) pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(pendingComment) {
pendingComment = ""
}
continue continue
} }
if !strings.HasPrefix(trimmed, "bind") { if !strings.HasPrefix(trimmed, "bind") {
@@ -414,6 +421,9 @@ func (p *MangoWCParser) parseFileWithSource(filePath string) ([]MangoWCKeyBindin
if strings.HasPrefix(trimmed, "#") { if strings.HasPrefix(trimmed, "#") {
pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#")) pendingComment = strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))
if isMangoWCSectionComment(pendingComment) {
pendingComment = ""
}
continue continue
} }
@@ -483,7 +493,7 @@ func (p *MangoWCParser) parseDMSBindsDirectly(dmsBindsPath string) []MangoWCKeyB
// line directly above) is the description: mango feeds inline comments to spawn // 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. // as argv, so DMS keeps descriptions on the line above; inline `#` is a fallback.
func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment string) *MangoWCKeyBinding { 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) matches := bindMatch.FindStringSubmatch(line)
if len(matches) < 3 { if len(matches) < 3 {
return nil return nil
@@ -499,6 +509,9 @@ func (p *MangoWCParser) getKeybindAtLineContent(line string, precedingComment st
} }
if comment == "" { if comment == "" {
comment = strings.TrimSpace(precedingComment) comment = strings.TrimSpace(precedingComment)
if isMangoWCSectionComment(comment) {
comment = ""
}
} }
if strings.HasPrefix(comment, MangoWCHideComment) { if strings.HasPrefix(comment, MangoWCHideComment) {
@@ -71,9 +71,10 @@ func TestMangoWCAutogenerateComment(t *testing.T) {
func TestMangoWCGetKeybindAtLine(t *testing.T) { func TestMangoWCGetKeybindAtLine(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
line string line string
expected *MangoWCKeyBinding precedingComment string
expected *MangoWCKeyBinding
}{ }{
{ {
name: "basic_keybind", name: "basic_keybind",
@@ -157,6 +158,41 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
Comment: "dms ipc call lock lock", 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", name: "keybind_with_spaces",
line: "bind = SUPER, r, reload_config", line: "bind = SUPER, r, reload_config",
@@ -174,7 +210,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
parser := NewMangoWCParser("") parser := NewMangoWCParser("")
parser.contentLines = []string{tt.line} parser.contentLines = []string{tt.line}
result := parser.getKeybindAtLine(0, "") result := parser.getKeybindAtLine(0, tt.precedingComment)
if tt.expected == nil { if tt.expected == nil {
if result != nil { if result != nil {
@@ -3,7 +3,10 @@ package providers
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/config"
) )
func TestMangoWCProviderName(t *testing.T) { 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") 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)
}
}
}
+2 -2
View File
@@ -201,7 +201,7 @@ func (m Model) viewInstallComplete() string {
wm := m.selectedWindowManager() 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\"" loginHint := "If you do not have a greeter, login with \"niri-session\" or \"Hyprland\""
switch wm { switch wm {
case deps.WindowManagerNiri: case deps.WindowManagerNiri:
@@ -223,7 +223,7 @@ func (m Model) viewInstallComplete() string {
b.WriteString(labelStyle.Render("Troubleshooting:") + "\n") b.WriteString(labelStyle.Render("Troubleshooting:") + "\n")
if wm == deps.WindowManagerMango { 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") b.WriteString(labelStyle.Render(" View logs: ") + cmdStyle.Render("qs -p ~/.config/quickshell/dms log") + "\n")
} else { } else {
b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n") b.WriteString(labelStyle.Render(" Disable autostart: ") + cmdStyle.Render("systemctl --user disable dms") + "\n")
+1
View File
@@ -177,6 +177,7 @@ Singleton {
property int mangoLayoutGapsOverride: -1 property int mangoLayoutGapsOverride: -1
property int mangoLayoutRadiusOverride: -1 property int mangoLayoutRadiusOverride: -1
property int mangoLayoutBorderSize: -1 property int mangoLayoutBorderSize: -1
property bool mangoTrackpadNaturalScrolling: true
property int firstDayOfWeek: -1 property int firstDayOfWeek: -1
property bool showWeekNumber: false property bool showWeekNumber: false
+2 -1
View File
@@ -33,6 +33,7 @@ var SPEC = {
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
firstDayOfWeek: { def: -1 }, firstDayOfWeek: { def: -1 },
showWeekNumber: { def: false }, showWeekNumber: { def: false },
@@ -237,7 +238,7 @@ var SPEC = {
qt6ctAvailable: { def: false, persist: false }, qt6ctAvailable: { def: false, persist: false },
gtkAvailable: { def: false, persist: false }, gtkAvailable: { def: false, persist: false },
cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" }, cursorSettings: { def: { theme: "System Default", size: 24, niri: { hideWhenTyping: false, hideAfterInactiveMs: 0 }, hyprland: { hideOnKeyPress: false, hideOnTouch: false, inactiveTimeout: 0 }, dwl: { cursorHideTimeout: 0 }, mango: { cursorHideTimeout: 0 } }, onChange: "updateCompositorCursor" },
availableCursorThemes: { def: ["System Default"], persist: false }, availableCursorThemes: { def: ["System Default"], persist: false },
systemDefaultCursorTheme: { def: "", persist: false }, systemDefaultCursorTheme: { def: "", persist: false },
@@ -92,6 +92,7 @@ Item {
return root.screenName; return root.screenName;
} }
} }
readonly property bool mangoOverviewActive: CompositorService.isMango && MangoService.isOutputInOverview(effectiveScreenName)
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
readonly property bool useExtWorkspace: { readonly property bool useExtWorkspace: {
@@ -160,7 +161,11 @@ Item {
baseList = getHyprlandWorkspaces(); baseList = getHyprlandWorkspaces();
break; break;
case "dwl": case "dwl":
baseList = getDwlTags();
break;
case "mango": case "mango":
if (root.mangoOverviewActive)
return [];
baseList = getDwlTags(); baseList = getDwlTags();
break; break;
case "sway": case "sway":
@@ -977,7 +982,7 @@ Item {
StyledText { StyledText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: !root.isVertical visible: !root.isVertical
text: I18n.tr("OVERVIEW") text: I18n.tr("Overview")
color: Theme.primary color: Theme.primary
font.pixelSize: overviewPill.labelSize font.pixelSize: overviewPill.labelSize
font.weight: Font.DemiBold font.weight: Font.DemiBold
@@ -1115,7 +1120,7 @@ Item {
targetWorkspaceId = modelData?.id; targetWorkspaceId = modelData?.id;
} else if (CompositorService.isHyprland) { } else if (CompositorService.isHyprland) {
targetWorkspaceId = modelData?.id; targetWorkspaceId = modelData?.id;
} else if (CompositorService.isDwl) { } else if (root.isDwlLike) {
targetWorkspaceId = modelData?.tag; targetWorkspaceId = modelData?.tag;
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) { } else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
targetWorkspaceId = modelData?.num; targetWorkspaceId = modelData?.num;
@@ -2432,6 +2432,17 @@ Item {
onSliderValueChanged: newValue => SettingsData.setCursorSize(newValue) onSliderValueChanged: newValue => SettingsData.setCursorSize(newValue)
} }
SettingsToggleRow {
tab: "theme"
tags: ["mango", "touchpad", "trackpad", "natural", "scrolling"]
settingKey: "mangoTrackpadNaturalScrolling"
text: I18n.tr("Natural Touchpad Scrolling")
description: I18n.tr("Invert touchpad scroll direction")
visible: CompositorService.isMango
checked: SettingsData.mangoTrackpadNaturalScrolling
onToggled: checked => SettingsData.set("mangoTrackpadNaturalScrolling", checked)
}
SettingsToggleRow { SettingsToggleRow {
tab: "theme" tab: "theme"
tags: ["cursor", "hide", "typing"] tags: ["cursor", "hide", "typing"]
@@ -189,7 +189,7 @@ Item {
settingKey: "dwlShowAllTags" settingKey: "dwlShowAllTags"
tags: ["dwl", "tags", "workspace"] tags: ["dwl", "tags", "workspace"]
text: I18n.tr("Show All Tags") text: I18n.tr("Show All Tags")
description: I18n.tr("Show all 9 tags instead of only occupied tags (DWL only)") description: I18n.tr("Show all 9 tags instead of only occupied tags")
checked: SettingsData.dwlShowAllTags checked: SettingsData.dwlShowAllTags
visible: CompositorService.isDwl || CompositorService.isMango visible: CompositorService.isDwl || CompositorService.isMango
onToggled: checked => SettingsData.set("dwlShowAllTags", checked) onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
+95 -14
View File
@@ -21,12 +21,18 @@ Singleton {
readonly property bool available: socketPath.length > 0 readonly property bool available: socketPath.length > 0
readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)) readonly property string configDir: Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation))
readonly property string configPath: configDir + "/mango/config.conf"
readonly property string mangoDmsDir: configDir + "/mango/dms" readonly property string mangoDmsDir: configDir + "/mango/dms"
readonly property string bindsPath: mangoDmsDir + "/binds.conf"
readonly property string colorsPath: mangoDmsDir + "/colors.conf"
readonly property string outputsPath: mangoDmsDir + "/outputs.conf" readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
readonly property string layoutPath: mangoDmsDir + "/layout.conf" readonly property string layoutPath: mangoDmsDir + "/layout.conf"
readonly property string cursorPath: mangoDmsDir + "/cursor.conf" readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
property int _lastGapValue: -1 property int _lastGapValue: -1
property real _ignoreWatchedReloadUntil: 0
property real _lastWatchedReloadAt: 0
// name -> { name, active, x, y, width, height, scale, layoutIndex, // name -> { name, active, x, y, width, height, scale, layoutIndex,
// layoutSymbol, lastOpenSurface, kbLayout, keymode, // layoutSymbol, lastOpenSurface, kbLayout, keymode,
@@ -47,6 +53,55 @@ Singleton {
// One connection per watch target; mango streams a fresh full snapshot on // One connection per watch target; mango streams a fresh full snapshot on
// every change, so each line is treated as the complete state. // every change, so each line is treated as the complete state.
FileView {
id: mangoConfigWatcher
path: CompositorService.isMango ? root.configPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
FileView {
id: mangoBindsWatcher
path: CompositorService.isMango ? root.bindsPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
FileView {
id: mangoColorsWatcher
path: CompositorService.isMango ? root.colorsPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
FileView {
id: mangoLayoutWatcher
path: CompositorService.isMango ? root.layoutPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
FileView {
id: mangoCursorWatcher
path: CompositorService.isMango ? root.cursorPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
FileView {
id: mangoOutputsWatcher
path: CompositorService.isMango ? root.outputsPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
FileView {
id: mangoWindowRulesWatcher
path: CompositorService.isMango ? root.windowRulesPath : ""
watchChanges: CompositorService.isMango
onFileChanged: root.handleWatchedConfigChanged()
}
DankSocket { DankSocket {
id: monitorsSocket id: monitorsSocket
path: root.socketPath path: root.socketPath
@@ -100,12 +155,14 @@ Singleton {
for (const m of monitors) { for (const m of monitors) {
if (!m.name) if (!m.name)
continue; continue;
const activeTags = m.active_tags || [];
const inOverview = activeTags.length === 0 || activeTags.every(t => t === 0);
const tags = (m.tags || []).map(t => ({ const tags = (m.tags || []).map(t => ({
// 0-based to match the legacy dwl tag model used by consumers // 0-based to match the legacy dwl tag model used by consumers
"tag": (t.index ?? 1) - 1, "tag": (t.index ?? 1) - 1,
"state": t.is_urgent ? 2 : (t.is_active ? 1 : 0), "state": t.is_urgent ? 2 : (!inOverview && t.is_active ? 1 : 0),
"clients": t.client_count ?? 0, "clients": t.client_count ?? 0,
"focused": !!t.is_active, "focused": !inOverview && !!t.is_active,
"urgent": !!t.is_urgent, "urgent": !!t.is_urgent,
"layout": t.layout ?? "" "layout": t.layout ?? ""
})); }));
@@ -119,7 +176,8 @@ Singleton {
"scale": m.scale ?? 1.0, "scale": m.scale ?? 1.0,
"layoutIndex": m.layout_index ?? 0, "layoutIndex": m.layout_index ?? 0,
"layout": m.layout_index ?? 0, "layout": m.layout_index ?? 0,
"activeTags": m.active_tags || [], "activeTags": activeTags,
"inOverview": inOverview,
"layoutSymbol": m.layout_symbol ?? "", "layoutSymbol": m.layout_symbol ?? "",
"lastOpenSurface": m.last_open_surface ?? "", "lastOpenSurface": m.last_open_surface ?? "",
"keymode": m.keymode ?? "", "keymode": m.keymode ?? "",
@@ -179,6 +237,8 @@ Singleton {
const output = getOutputState(outputName); const output = getOutputState(outputName);
if (!output) if (!output)
return false; return false;
if (output.inOverview !== undefined)
return output.inOverview;
const at = output.activeTags || []; const at = output.activeTags || [];
return at.length === 0 || at.every(t => t === 0); return at.length === 0 || at.every(t => t === 0);
} }
@@ -201,6 +261,8 @@ Singleton {
const output = getOutputState(outputName); const output = getOutputState(outputName);
if (!output || !output.tags) if (!output || !output.tags)
return []; return [];
if (isOutputInOverview(outputName))
return [];
const visibleTags = new Set(); const visibleTags = new Set();
output.tags.forEach(tag => { output.tags.forEach(tag => {
if (tag.state === 1 || tag.clients > 0) if (tag.state === 1 || tag.clients > 0)
@@ -336,10 +398,36 @@ Singleton {
// Commands (mango verb IPC: mmsg dispatch <func>,<args>) // Commands (mango verb IPC: mmsg dispatch <func>,<args>)
function reloadConfig() { function suppressWatchedConfigReloads(ms) {
root._ignoreWatchedReloadUntil = Math.max(root._ignoreWatchedReloadUntil, Date.now() + (ms || 1500));
}
function handleWatchedConfigChanged() {
if (!CompositorService.isMango || !root.available)
return;
const now = Date.now();
if (now < root._ignoreWatchedReloadUntil)
return;
if (now - root._lastWatchedReloadAt < 700)
return;
root._lastWatchedReloadAt = now;
root.reloadConfig(true, false);
}
function reloadConfig(showToast, suppressWatch) {
const shouldShowToast = showToast !== false;
const shouldSuppressWatch = suppressWatch !== false;
if (shouldSuppressWatch)
suppressWatchedConfigReloads(1500);
Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => { Proc.runCommand("mango-reload", ["mmsg", "dispatch", "reload_config"], (output, exitCode) => {
if (exitCode !== 0) if (exitCode !== 0) {
log.warn("mmsg reload_config failed:", output); log.warn("mmsg reload_config failed:", output);
if (shouldShowToast)
ToastService.showError(I18n.tr("mango: failed to reload config"), output || "", "", "mango-config");
return;
}
if (shouldShowToast)
ToastService.showInfo(I18n.tr("mango: config reloaded"), "", "", "mango-config");
}); });
} }
@@ -538,17 +626,10 @@ borderpx=${borderSize}
const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme; const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme;
const size = settings.size || 24; const size = settings.size || 24;
const hideTimeout = settings.mango?.cursorHideTimeout || 0; const hideTimeout = settings.mango?.cursorHideTimeout || 0;
const naturalScrolling = SettingsData.mangoTrackpadNaturalScrolling ? 1 : 0;
const isDefaultConfig = !themeName && size === 24 && hideTimeout === 0;
if (isDefaultConfig) {
Proc.runCommand("mango-write-cursor", ["sh", "-c", `mkdir -p "${mangoDmsDir}" && : > "${cursorPath}"`], (output, exitCode) => {
if (exitCode !== 0)
log.warn("Failed to write cursor config:", output);
});
return;
}
let content = `# Auto-generated by DMS - do not edit manually let content = `# Auto-generated by DMS - do not edit manually
trackpad_natural_scrolling=${naturalScrolling}
cursor_size=${size}`; cursor_size=${size}`;
if (themeName) if (themeName)