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:
@@ -294,7 +294,14 @@ func runSetup() error {
|
||||
|
||||
wm, wmSelected := promptCompositor()
|
||||
terminal, terminalSelected := promptTerminal()
|
||||
useSystemd := promptSystemd()
|
||||
useSystemd := true
|
||||
if wmSelected {
|
||||
if wm == deps.WindowManagerMango {
|
||||
useSystemd = false
|
||||
} else {
|
||||
useSystemd = promptSystemd()
|
||||
}
|
||||
}
|
||||
|
||||
if !wmSelected && !terminalSelected {
|
||||
fmt.Println("No configurations selected. Exiting.")
|
||||
|
||||
@@ -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,34 +283,63 @@ 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)
|
||||
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 {
|
||||
continue
|
||||
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])
|
||||
|
||||
var comment string
|
||||
description := strings.TrimSpace(precedingComment)
|
||||
if isMangoWCSectionComment(description) {
|
||||
description = ""
|
||||
}
|
||||
if len(commentParts) > 1 {
|
||||
comment = strings.TrimSpace(commentParts[1])
|
||||
description = strings.TrimSpace(commentParts[1])
|
||||
}
|
||||
if strings.HasPrefix(description, MangoWCHideComment) {
|
||||
return nil, true
|
||||
}
|
||||
|
||||
fields := strings.SplitN(bindContent, ",", 4)
|
||||
if len(fields) < 3 {
|
||||
continue
|
||||
return nil, false
|
||||
}
|
||||
|
||||
mods := strings.TrimSpace(fields[0])
|
||||
@@ -311,21 +351,29 @@ func (m *MangoWCProvider) loadOverrideBinds() (map[string]*mangowcOverrideBind,
|
||||
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,
|
||||
return &mangowcOverrideBind{
|
||||
Key: m.buildKeyString(mods, keyName),
|
||||
Action: action,
|
||||
Description: comment,
|
||||
}
|
||||
Description: description,
|
||||
Prefix: prefix,
|
||||
}, true
|
||||
}
|
||||
|
||||
return binds, nil
|
||||
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 {
|
||||
@@ -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
|
||||
})
|
||||
|
||||
var sb strings.Builder
|
||||
for _, bind := range bindList {
|
||||
m.writeBindLine(&sb, bind)
|
||||
return bindList
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
func (m *MangoWCProvider) writeBindLineToLines(lines *[]string, bind *mangowcOverrideBind) {
|
||||
var sb strings.Builder
|
||||
m.writeBindLine(&sb, bind)
|
||||
text := strings.TrimSuffix(sb.String(), "\n")
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
*lines = append(*lines, strings.Split(text, "\n")...)
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -73,6 +73,7 @@ func TestMangoWCGetKeybindAtLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line string
|
||||
precedingComment string
|
||||
expected *MangoWCKeyBinding
|
||||
}{
|
||||
{
|
||||
@@ -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")
|
||||
|
||||
@@ -177,6 +177,7 @@ Singleton {
|
||||
property int mangoLayoutGapsOverride: -1
|
||||
property int mangoLayoutRadiusOverride: -1
|
||||
property int mangoLayoutBorderSize: -1
|
||||
property bool mangoTrackpadNaturalScrolling: true
|
||||
|
||||
property int firstDayOfWeek: -1
|
||||
property bool showWeekNumber: false
|
||||
|
||||
@@ -33,6 +33,7 @@ var SPEC = {
|
||||
mangoLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" },
|
||||
|
||||
firstDayOfWeek: { def: -1 },
|
||||
showWeekNumber: { def: false },
|
||||
@@ -237,7 +238,7 @@ var SPEC = {
|
||||
qt6ctAvailable: { 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 },
|
||||
systemDefaultCursorTheme: { def: "", persist: false },
|
||||
|
||||
|
||||
@@ -92,6 +92,7 @@ Item {
|
||||
return root.screenName;
|
||||
}
|
||||
}
|
||||
readonly property bool mangoOverviewActive: CompositorService.isMango && MangoService.isOutputInOverview(effectiveScreenName)
|
||||
|
||||
readonly property var extProjection: (useExtWorkspace && parentScreen) ? WindowManager.screenProjection(parentScreen) : null
|
||||
readonly property bool useExtWorkspace: {
|
||||
@@ -160,7 +161,11 @@ Item {
|
||||
baseList = getHyprlandWorkspaces();
|
||||
break;
|
||||
case "dwl":
|
||||
baseList = getDwlTags();
|
||||
break;
|
||||
case "mango":
|
||||
if (root.mangoOverviewActive)
|
||||
return [];
|
||||
baseList = getDwlTags();
|
||||
break;
|
||||
case "sway":
|
||||
@@ -977,7 +982,7 @@ Item {
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !root.isVertical
|
||||
text: I18n.tr("OVERVIEW")
|
||||
text: I18n.tr("Overview")
|
||||
color: Theme.primary
|
||||
font.pixelSize: overviewPill.labelSize
|
||||
font.weight: Font.DemiBold
|
||||
@@ -1115,7 +1120,7 @@ Item {
|
||||
targetWorkspaceId = modelData?.id;
|
||||
} else if (CompositorService.isHyprland) {
|
||||
targetWorkspaceId = modelData?.id;
|
||||
} else if (CompositorService.isDwl) {
|
||||
} else if (root.isDwlLike) {
|
||||
targetWorkspaceId = modelData?.tag;
|
||||
} else if (CompositorService.isSway || CompositorService.isScroll || CompositorService.isMiracle) {
|
||||
targetWorkspaceId = modelData?.num;
|
||||
|
||||
@@ -2432,6 +2432,17 @@ Item {
|
||||
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 {
|
||||
tab: "theme"
|
||||
tags: ["cursor", "hide", "typing"]
|
||||
|
||||
@@ -189,7 +189,7 @@ Item {
|
||||
settingKey: "dwlShowAllTags"
|
||||
tags: ["dwl", "tags", "workspace"]
|
||||
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
|
||||
visible: CompositorService.isDwl || CompositorService.isMango
|
||||
onToggled: checked => SettingsData.set("dwlShowAllTags", checked)
|
||||
|
||||
@@ -21,12 +21,18 @@ Singleton {
|
||||
readonly property bool available: socketPath.length > 0
|
||||
|
||||
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 bindsPath: mangoDmsDir + "/binds.conf"
|
||||
readonly property string colorsPath: mangoDmsDir + "/colors.conf"
|
||||
readonly property string outputsPath: mangoDmsDir + "/outputs.conf"
|
||||
readonly property string layoutPath: mangoDmsDir + "/layout.conf"
|
||||
readonly property string cursorPath: mangoDmsDir + "/cursor.conf"
|
||||
readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf"
|
||||
|
||||
property int _lastGapValue: -1
|
||||
property real _ignoreWatchedReloadUntil: 0
|
||||
property real _lastWatchedReloadAt: 0
|
||||
|
||||
// name -> { name, active, x, y, width, height, scale, layoutIndex,
|
||||
// layoutSymbol, lastOpenSurface, kbLayout, keymode,
|
||||
@@ -47,6 +53,55 @@ Singleton {
|
||||
// One connection per watch target; mango streams a fresh full snapshot on
|
||||
// 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 {
|
||||
id: monitorsSocket
|
||||
path: root.socketPath
|
||||
@@ -100,12 +155,14 @@ Singleton {
|
||||
for (const m of monitors) {
|
||||
if (!m.name)
|
||||
continue;
|
||||
const activeTags = m.active_tags || [];
|
||||
const inOverview = activeTags.length === 0 || activeTags.every(t => t === 0);
|
||||
const tags = (m.tags || []).map(t => ({
|
||||
// 0-based to match the legacy dwl tag model used by consumers
|
||||
"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,
|
||||
"focused": !!t.is_active,
|
||||
"focused": !inOverview && !!t.is_active,
|
||||
"urgent": !!t.is_urgent,
|
||||
"layout": t.layout ?? ""
|
||||
}));
|
||||
@@ -119,7 +176,8 @@ Singleton {
|
||||
"scale": m.scale ?? 1.0,
|
||||
"layoutIndex": m.layout_index ?? 0,
|
||||
"layout": m.layout_index ?? 0,
|
||||
"activeTags": m.active_tags || [],
|
||||
"activeTags": activeTags,
|
||||
"inOverview": inOverview,
|
||||
"layoutSymbol": m.layout_symbol ?? "",
|
||||
"lastOpenSurface": m.last_open_surface ?? "",
|
||||
"keymode": m.keymode ?? "",
|
||||
@@ -179,6 +237,8 @@ Singleton {
|
||||
const output = getOutputState(outputName);
|
||||
if (!output)
|
||||
return false;
|
||||
if (output.inOverview !== undefined)
|
||||
return output.inOverview;
|
||||
const at = output.activeTags || [];
|
||||
return at.length === 0 || at.every(t => t === 0);
|
||||
}
|
||||
@@ -201,6 +261,8 @@ Singleton {
|
||||
const output = getOutputState(outputName);
|
||||
if (!output || !output.tags)
|
||||
return [];
|
||||
if (isOutputInOverview(outputName))
|
||||
return [];
|
||||
const visibleTags = new Set();
|
||||
output.tags.forEach(tag => {
|
||||
if (tag.state === 1 || tag.clients > 0)
|
||||
@@ -336,10 +398,36 @@ Singleton {
|
||||
|
||||
// ── 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) => {
|
||||
if (exitCode !== 0)
|
||||
if (exitCode !== 0) {
|
||||
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 size = settings.size || 24;
|
||||
const hideTimeout = settings.mango?.cursorHideTimeout || 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;
|
||||
}
|
||||
const naturalScrolling = SettingsData.mangoTrackpadNaturalScrolling ? 1 : 0;
|
||||
|
||||
let content = `# Auto-generated by DMS - do not edit manually
|
||||
trackpad_natural_scrolling=${naturalScrolling}
|
||||
cursor_size=${size}`;
|
||||
|
||||
if (themeName)
|
||||
|
||||
Reference in New Issue
Block a user