diff --git a/core/cmd/dms/commands_config.go b/core/cmd/dms/commands_config.go index 37568a40e..094cf3da2 100644 --- a/core/cmd/dms/commands_config.go +++ b/core/cmd/dms/commands_config.go @@ -55,32 +55,8 @@ var resolveIncludeCmd = &cobra.Command{ Run: runResolveInclude, } -var mangoMigrateMain bool - -var mangoMigrateCmd = &cobra.Command{ - Use: "mango-migrate", - Short: "Respell Mango config keys for the installed Mango", - Long: "Detects whether the installed Mango uses legacy or snake_case config keys, rewrites the DMS fragments in mango/dms to match, and prints the dialect on the last line. --main also rewrites config.conf after a backup.", - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - dialect := mangoconf.Detect() - changed, err := dialect.Migrate(mangoconf.Dir(), mangoMigrateMain) - for _, path := range changed { - fmt.Fprintf(os.Stderr, "migrated %s\n", path) - } - // Printed before any error: the shell needs the dialect even when a fragment failed. - fmt.Println(dialect) - if err != nil { - fmt.Fprintf(os.Stderr, "Error: %v\n", err) - os.Exit(1) - } - }, -} - func init() { configCmd.AddCommand(resolveIncludeCmd) - mangoMigrateCmd.Flags().BoolVar(&mangoMigrateMain, "main", false, "also rewrite config.conf (a backup is kept)") - configCmd.AddCommand(mangoMigrateCmd) } type IncludeResult struct { diff --git a/core/cmd/dms/commands_doctor.go b/core/cmd/dms/commands_doctor.go index 1d207d7cc..99be2e6d4 100644 --- a/core/cmd/dms/commands_doctor.go +++ b/core/cmd/dms/commands_doctor.go @@ -858,6 +858,14 @@ func checkWindowManagers() []checkResult { if c.name == "Hyprland" { results = append(results, checkHyprlandConfigFormat(filepath.Join(utils.XDGConfigHome(), "hypr"))...) } + if c.name == "mangowc" && mangoLacksSnakeCase(ver) { + results = append(results, checkResult{ + catCompositor, "Mango config format", statusError, + fmt.Sprintf("Mango %s is older than 0.18", ver), + "DMS writes Mango config with the snake_case keys of Mango 0.18 and later, which this release rejects; upgrade Mango", + doctorDocsURL + "#compositor-checks", + }) + } } if !foundAny { @@ -878,7 +886,7 @@ func checkWindowManagers() []checkResult { } if os.Getenv("MANGO_INSTANCE_SIGNATURE") != "" { - results = append(results, checkMangoConfig(mangoconf.Dir(), mangoconf.Detect())...) + results = append(results, checkMangoConfig(mangoconf.Dir())...) } results = append(results, checkCompositorBlurSupport()) @@ -886,33 +894,70 @@ func checkWindowManagers() []checkResult { return results } -var mangoLegacyOverviewBinds = regexp.MustCompile(`(?im)^\s*mousebind\s*=\s*none\s*,\s*(btn_left\s*,\s*toggleoverview\s*,\s*1|btn_right\s*,\s*killclient\s*,\s*0)\s*$`) +var ( + mangoLegacyOverviewBinds = regexp.MustCompile(`(?im)^\s*mousebind\s*=\s*none\s*,\s*(btn_left\s*,\s*toggleoverview\s*,\s*1|btn_right\s*,\s*killclient\s*,\s*0)\s*$`) + mangoUnknownKeyword = regexp.MustCompile(`Unknown keyword:\s*(\S+)`) + ansiEscape = regexp.MustCompile(`\x1b\[[0-9;]*m`) +) -func checkMangoConfig(mangoDir string, dialect mangoconf.Dialect) []checkResult { +// Mango's parser follows source= and always exits 0; only its output tells. +var mangoParseConfig = func(path string) (string, error) { + if _, err := exec.LookPath("mango"); err != nil { + return "", err + } + out, _ := exec.Command("mango", "-p", "-c", path).CombinedOutput() + return string(out), nil +} + +// Each "Unknown keyword" line is followed by a location line naming the file. +func mangoUnknownKeywords(output string) []string { + var found []string + seen := map[string]bool{} + key := "" + for line := range strings.SplitSeq(ansiEscape.ReplaceAllString(output, ""), "\n") { + if m := mangoUnknownKeyword.FindStringSubmatch(line); m != nil { + key = m[1] + continue + } + slash := strings.LastIndex(line, "/") + if key == "" || slash < 0 { + continue + } + file, _, _ := strings.Cut(line[slash+1:], ":") + entry := key + " (" + file + ")" + if !seen[entry] { + seen[entry] = true + found = append(found, entry) + } + key = "" + } + return found +} + +func checkMangoConfig(mangoDir string) []checkResult { url := doctorDocsURL + "#compositor-checks" - paths, _ := filepath.Glob(filepath.Join(mangoDir, "dms", "*.conf")) - paths = append([]string{filepath.Join(mangoDir, "config.conf")}, paths...) + mainPath := filepath.Join(mangoDir, "config.conf") + var results []checkResult + if output, err := mangoParseConfig(mainPath); err == nil { + if unknown := mangoUnknownKeywords(output); len(unknown) > 0 { + results = append(results, checkResult{catCompositor, "Mango config keys", statusError, + "Unknown keys: " + strings.Join(unknown, ", "), + "Mango 0.18 renamed its config keys to snake_case. Run migrate-config-keys.sh from Mango's 0.18.0 release over ~/.config/mango, or rename the keys by hand.", url}) + } + } - var stale, overviewBinds []string + paths, _ := filepath.Glob(filepath.Join(mangoDir, "dms", "*.conf")) + paths = append([]string{mainPath}, paths...) + var overviewBinds []string for _, path := range paths { data, err := os.ReadFile(path) if err != nil { continue } - if dialect.Translate(string(data)) != string(data) { - stale = append(stale, filepath.Base(path)) - } if mangoLegacyOverviewBinds.Match(data) { overviewBinds = append(overviewBinds, filepath.Base(path)) } } - - var results []checkResult - if len(stale) > 0 { - results = append(results, checkResult{catCompositor, "Mango config keys", statusError, - fmt.Sprintf("Not %s keys: %s", dialect, strings.Join(stale, ", ")), - "This Mango rejects those lines as unknown keywords. Run `dms config mango-migrate --main` (config.conf is backed up).", url}) - } if len(overviewBinds) > 0 { results = append(results, checkResult{catCompositor, "Mango mouse binds", statusWarn, "Obsolete overview binds in " + strings.Join(overviewBinds, ", "), @@ -967,6 +1012,10 @@ func hyprlandLacksLua(ver string) bool { return ver != "" && version.CompareVersions(ver, "0.55.0") < 0 } +func mangoLacksSnakeCase(ver string) bool { + return ver != "" && version.CompareVersions(ver, "0.18.0") < 0 +} + func firstSubmatch(re *regexp.Regexp) func(string) string { return func(s string) string { if m := re.FindStringSubmatch(s); len(m) > 1 { diff --git a/core/cmd/dms/commands_doctor_test.go b/core/cmd/dms/commands_doctor_test.go index e419165e5..dfeb4d7ba 100644 --- a/core/cmd/dms/commands_doctor_test.go +++ b/core/cmd/dms/commands_doctor_test.go @@ -8,8 +8,6 @@ import ( "path/filepath" "strings" "testing" - - "github.com/duxlabs-dev/dux-shell/core/internal/mangoconf" ) func TestQuickshellVersionFailureDetails(t *testing.T) { @@ -385,19 +383,41 @@ func TestCheckXDGMenuFile(t *testing.T) { } } -func TestCheckMangoConfigFlagsWrongDialectAndOverviewBinds(t *testing.T) { +func TestCheckMangoConfigReportsUnknownKeysAndOverviewBinds(t *testing.T) { dir := t.TempDir() if err := os.MkdirAll(filepath.Join(dir, "dms"), 0o755); err != nil { t.Fatal(err) } _ = os.WriteFile(filepath.Join(dir, "config.conf"), []byte("exec-once=dms run\nmousebind=NONE,btn_left,toggleoverview,1\n"), 0o644) - _ = os.WriteFile(filepath.Join(dir, "dms", "layout.conf"), []byte("border_px=2\n"), 0o644) + // Shape of `mango -p` 0.18.2 output, colours included; the same key twice collapses. + parsed := "\x1b[1m\x1b[31m[ERROR]:\x1b[0m Unknown keyword: \x1b[1m\x1b[31mexec-once\x1b[0m\n" + + "\x1b[1;31m╰─\x1b[1;33m[Index]\x1b[0m \x1b[1;36m" + dir + "/config.conf\x1b[0m:\x1b[1;35m1\x1b[0m\n ╰─exec-once=dms run\n\n" + + "[ERROR]: Unknown keyword: overviewgappo\n╰─[Index] " + dir + "/dms/layout.conf:10\n ╰─overviewgappo=30\n" + + "[ERROR]: Unknown keyword: overviewgappo\n╰─[Index] " + dir + "/dms/layout.conf:12\n" + prev := mangoParseConfig + mangoParseConfig = func(string) (string, error) { return parsed, nil } + t.Cleanup(func() { mangoParseConfig = prev }) - if got := checkMangoConfig(dir, mangoconf.Legacy); len(got) != 2 || got[0].message != "Not legacy keys: layout.conf" { - t.Fatalf("legacy: %+v", got) + got := checkMangoConfig(dir) + if len(got) != 2 || got[0].message != "Unknown keys: exec-once (config.conf), overviewgappo (layout.conf)" || got[1].name != "Mango mouse binds" { + t.Fatalf("got %+v", got) } - if got := checkMangoConfig(dir, mangoconf.Snake); len(got) != 2 || got[0].message != "Not snake keys: config.conf" { - t.Fatalf("snake: %+v", got) + mangoParseConfig = func(string) (string, error) { return "", os.ErrNotExist } + if got := checkMangoConfig(dir); len(got) != 1 { + t.Fatalf("without mango on PATH only the bind check should run, got %+v", got) + } +} + +func TestMangoLacksSnakeCase(t *testing.T) { + for ver, want := range map[string]bool{ + "0.17.5": true, + "0.18.0": false, + "0.18.2": false, + "": false, + } { + if got := mangoLacksSnakeCase(ver); got != want { + t.Errorf("mangoLacksSnakeCase(%q) = %v, want %v", ver, got, want) + } } } diff --git a/core/cmd/dms/commands_setup.go b/core/cmd/dms/commands_setup.go index 226dc2e97..5deb59519 100644 --- a/core/cmd/dms/commands_setup.go +++ b/core/cmd/dms/commands_setup.go @@ -130,7 +130,7 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{ mangoFile: "layout.conf", niriContent: func(_ string) string { return config.NiriLayoutConfig }, hyprContent: func(_ string) string { return config.DMSLayoutLuaConfig }, - mangoContent: func(_ string) string { return mangoconf.Detect().Translate(config.MangoLayoutConfig) }, + mangoContent: func(_ string) string { return config.MangoLayoutConfig }, }, "colors": { niriFile: "colors.kdl", @@ -138,7 +138,7 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{ mangoFile: "colors.conf", niriContent: func(_ string) string { return config.NiriColorsConfig }, hyprContent: func(_ string) string { return config.DMSColorsLuaConfig }, - mangoContent: func(_ string) string { return mangoconf.Detect().Translate(config.MangoColorsConfig) }, + mangoContent: func(_ string) string { return config.MangoColorsConfig }, }, "alttab": { niriFile: "alttab.kdl", diff --git a/core/internal/config/embedded/mango-colors.conf b/core/internal/config/embedded/mango-colors.conf index 009aea48e..d1f199c7b 100644 --- a/core/internal/config/embedded/mango-colors.conf +++ b/core/internal/config/embedded/mango-colors.conf @@ -1,6 +1,6 @@ # Auto-generated by DMS. Overwritten by matugen (dms/colors.conf). # Remove `source=./dms/colors.conf` from config.conf to override manually. -bordercolor = 0x595959ff -focuscolor = 0x8ab4f8ff -urgentcolor = 0xff5555ff +border_color = 0x595959ff +focus_color = 0x8ab4f8ff +urgent_color = 0xff5555ff diff --git a/core/internal/config/embedded/mango-layout.conf b/core/internal/config/embedded/mango-layout.conf index 9631c40c5..da64c5250 100644 --- a/core/internal/config/embedded/mango-layout.conf +++ b/core/internal/config/embedded/mango-layout.conf @@ -1,8 +1,8 @@ # Auto-generated by DMS. Regenerated from DMS settings (dms/layout.conf). border_radius=12 -gappih=5 -gappiv=5 -gappoh=5 -gappov=5 -borderpx=2 +gap_inner_horizontal=5 +gap_inner_vertical=5 +gap_outer_horizontal=5 +gap_outer_vertical=5 +border_px=2 diff --git a/core/internal/config/embedded/mango.conf b/core/internal/config/embedded/mango.conf index 472b686e0..4a3c6b8c8 100644 --- a/core/internal/config/embedded/mango.conf +++ b/core/internal/config/embedded/mango.conf @@ -7,7 +7,7 @@ env=XDG_SESSION_TYPE,wayland # Starts the shell when DMS is not a systemd service. Never exec=: Mango re-runs # exec= on every config reload, and DMS reloads the config. -exec-once=dms run +exec_once=dms run source=./dms/colors.conf source=./dms/layout.conf diff --git a/core/internal/config/mango_deploy.go b/core/internal/config/mango_deploy.go index 92429369f..f7c99f99c 100644 --- a/core/internal/config/mango_deploy.go +++ b/core/internal/config/mango_deploy.go @@ -41,14 +41,13 @@ func nonEmptyFile(path string) (string, bool) { } var ( - mangoBindLine = regexp.MustCompile(`^\s*(bind[a-z]*|mousebind|axisbind|gesturebind|switchbind|keymode|key_mode)\s*=`) - mangoMonitorLine = regexp.MustCompile(`^\s*(monitorrule|monitor_rule)\s*=`) - mangoWindowLine = regexp.MustCompile(`^\s*(windowrule|window_rule|windowrule-once|window_rule_once)\s*=`) - mangoExecOnceDMS = regexp.MustCompile(`(?m)^# Starts the shell when DMS.*\n#.*\n(exec-once|exec_once)=dms run\n`) + mangoBindLine = regexp.MustCompile(`^\s*(bind[a-z]*|mousebind|axisbind|gesturebind|switchbind|key_mode)\s*=`) + mangoMonitorLine = regexp.MustCompile(`^\s*monitor_rule\s*=`) + mangoWindowLine = regexp.MustCompile(`^\s*window_rule(_once)?\s*=`) + mangoExecOnceDMS = regexp.MustCompile(`(?m)^# Starts the shell when DMS.*\n#.*\nexec_once=dms run\n`) ) var ( - mangoDialect = mangoconf.Detect mangoSessionTargetInstalled = mangoconf.SessionTargetInstalled ) @@ -110,17 +109,16 @@ func (cd *ConfigDeployer) deployMangoConfig(terminalCommand string, useSystemd b cd.log(fmt.Sprintf("Backed up existing config to %s", result.BackupPath)) } - dialect := mangoDialect() newConfig := strings.ReplaceAll(MangoConfig, "{{TERMINAL_COMMAND}}", terminalCommand) if cd.systemdAutostart(deps.WindowManagerMango, useSystemd) { newConfig = mangoExecOnceDMS.ReplaceAllString(newConfig, "") } - if err := os.WriteFile(result.Path, []byte(dialect.Translate(newConfig)), 0o644); err != nil { + if err := os.WriteFile(result.Path, []byte(newConfig), 0o644); err != nil { result.Error = fmt.Errorf("failed to write config: %w", err) return result, result.Error } - if err := cd.deployMangoDmsConfigs(dmsDir, terminalCommand, dialect, existing, timestamp); err != nil { + if err := cd.deployMangoDmsConfigs(dmsDir, terminalCommand, existing, timestamp); err != nil { result.Error = fmt.Errorf("failed to deploy dms configs: %w", err) return result, result.Error } @@ -132,7 +130,7 @@ func (cd *ConfigDeployer) deployMangoConfig(terminalCommand string, useSystemd b // Like niri, fragments the user or the shell already filled are left alone; the // replaced config.conf hands its binds, monitor and window rules to empty ones. -func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string, dialect mangoconf.Dialect, oldMain, timestamp string) error { +func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand, oldMain, timestamp string) error { stockBinds := strings.ReplaceAll(MangoBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand) bindsPath := filepath.Join(dmsDir, "binds.conf") if data, err := os.ReadFile(bindsPath); err == nil && len(data) > 0 { @@ -142,7 +140,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string, } cd.log(fmt.Sprintf("Backed up existing binds to %s", backup)) if cd.replaceBinds { - if err := os.WriteFile(bindsPath, []byte(dialect.Translate(stockBinds)), 0o644); err != nil { + if err := os.WriteFile(bindsPath, []byte(stockBinds), 0o644); err != nil { return fmt.Errorf("failed to write binds.conf: %w", err) } cd.log("Deployed DMS stock binds.conf") @@ -160,7 +158,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string, binds = mangoconf.BindsMovedHeader + "\n" + old cd.log("Moved existing binds from config.conf to binds.conf") } - if err := os.WriteFile(bindsPath, []byte(dialect.Translate(binds)), 0o644); err != nil { + if err := os.WriteFile(bindsPath, []byte(binds), 0o644); err != nil { return fmt.Errorf("failed to write binds.conf: %w", err) } } @@ -182,7 +180,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string, cd.log(fmt.Sprintf("Skipping %s (already exists)", cfg.name)) continue } - if err := os.WriteFile(path, []byte(dialect.Translate(cfg.content)), 0o644); err != nil { + if err := os.WriteFile(path, []byte(cfg.content), 0o644); err != nil { return fmt.Errorf("failed to write %s: %w", cfg.name, err) } cd.log(fmt.Sprintf("Deployed %s", cfg.name)) diff --git a/core/internal/config/mango_deploy_test.go b/core/internal/config/mango_deploy_test.go index 9b08c22e8..64b2cd4bc 100644 --- a/core/internal/config/mango_deploy_test.go +++ b/core/internal/config/mango_deploy_test.go @@ -13,12 +13,11 @@ import ( "github.com/stretchr/testify/require" ) -func stubMango(t *testing.T, dialect mangoconf.Dialect, target bool, addWants error) *int { +func stubMango(t *testing.T, target bool, addWants error) *int { t.Helper() - prevDialect, prevTarget := mangoDialect, mangoSessionTargetInstalled - mangoDialect = func() mangoconf.Dialect { return dialect } + prevTarget := mangoSessionTargetInstalled mangoSessionTargetInstalled = func() bool { return target } - t.Cleanup(func() { mangoDialect, mangoSessionTargetInstalled = prevDialect, prevTarget }) + t.Cleanup(func() { mangoSessionTargetInstalled = prevTarget }) return stubAddWants(t, addWants) } @@ -31,16 +30,16 @@ func readMango(t *testing.T, home, rel string) string { const oldMangoConfig = `exec-once=waybar bind=SUPER,Return,spawn,foot -keymode=resize +key_mode=resize bind=NONE,h,resizewin,-10,0 -monitorrule=name:^DP-1$,width:2560,height:1440,refresh:144,x:0,y:0,scale:1,rr:0,vrr:1,hdr:1 -windowrule=appid:pavucontrol,isfloating:1 +monitor_rule=name:^DP-1$,width:2560,height:1440,refresh:144,x:0,y:0,scale:1,rr:0,vrr:1,hdr:1 +window_rule=app_id:pavucontrol,is_floating:1 ` func TestMangoDeployMovesUserRulesAndKeepsBindsByDefault(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - stubMango(t, mangoconf.Legacy, false, nil) + stubMango(t, false, nil) require.NoError(t, os.MkdirAll(filepath.Join(home, ".config", "mango"), 0o755)) require.NoError(t, os.WriteFile(filepath.Join(home, ".config", "mango", "config.conf"), []byte(oldMangoConfig), 0o644)) @@ -48,25 +47,26 @@ func TestMangoDeployMovesUserRulesAndKeepsBindsByDefault(t *testing.T) { require.NoError(t, err) assert.NotEmpty(t, result.BackupPath) - assert.Contains(t, readMango(t, home, "config.conf"), "exec-once=dms run") + assert.Contains(t, readMango(t, home, "config.conf"), "exec_once=dms run") assert.Contains(t, readMango(t, home, "config.conf"), "source=./dms/input.conf") assert.Equal(t, "", readMango(t, home, "dms/input.conf")) binds := readMango(t, home, "dms/binds.conf") - assert.Contains(t, binds, "bind=SUPER,Return,spawn,foot\nkeymode=resize\nbind=NONE,h,resizewin,-10,0") + assert.Contains(t, binds, "bind=SUPER,Return,spawn,foot\nkey_mode=resize\nbind=NONE,h,resizewin,-10,0") assert.NotContains(t, binds, "{{TERMINAL_COMMAND}}") + assert.Contains(t, readMango(t, home, "dms/outputs.conf"), "monitor_rule=name:^DP-1$") assert.Contains(t, readMango(t, home, "dms/outputs.conf"), "hdr:1") - assert.Contains(t, readMango(t, home, "dms/windowrules.conf"), "windowrule=appid:pavucontrol,isfloating:1") + assert.Contains(t, readMango(t, home, "dms/windowrules.conf"), "window_rule=app_id:pavucontrol,is_floating:1") } func TestMangoDeployBindsChoiceAlwaysBacksUp(t *testing.T) { for _, replace := range []bool{false, true} { home := t.TempDir() t.Setenv("HOME", home) - stubMango(t, mangoconf.Legacy, false, nil) + stubMango(t, false, nil) dms := filepath.Join(home, ".config", "mango", "dms") require.NoError(t, os.MkdirAll(dms, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(dms, "binds.conf"), []byte("bind=SUPER,x,killclient\n"), 0o644)) - require.NoError(t, os.WriteFile(filepath.Join(dms, "windowrules.conf"), []byte("windowrule=appid:keep,isglobal:1\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dms, "binds.conf"), []byte("bind=SUPER,x,killclient\nkey_mode=resize\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(dms, "windowrules.conf"), []byte("window_rule=app_id:keep,is_global:1\n"), 0o644)) cd := NewConfigDeployer(nil) cd.SetReplaceBinds(replace) @@ -76,8 +76,8 @@ func TestMangoDeployBindsChoiceAlwaysBacksUp(t *testing.T) { backups, _ := filepath.Glob(filepath.Join(dms, "binds.conf.backup.*")) assert.Len(t, backups, 1, "replace=%v", replace) binds := readMango(t, home, "dms/binds.conf") - assert.Equal(t, !replace, binds == mangoconf.BindsKeptHeader+"\nbind=SUPER,x,killclient\n", "replace=%v", replace) - assert.Equal(t, "windowrule=appid:keep,isglobal:1\n", readMango(t, home, "dms/windowrules.conf")) + assert.Equal(t, !replace, binds == mangoconf.BindsKeptHeader+"\nbind=SUPER,x,killclient\nkey_mode=resize\n", "replace=%v", replace) + assert.Equal(t, "window_rule=app_id:keep,is_global:1\n", readMango(t, home, "dms/windowrules.conf"), "kept fragments are left alone") } } @@ -97,7 +97,7 @@ func TestMangoDeploySystemdAutostart(t *testing.T) { t.Run(tc.name, func(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) - calls := stubMango(t, mangoconf.Snake, tc.target, tc.addWants) + calls := stubMango(t, tc.target, tc.addWants) _, err := NewConfigDeployer(nil).DeployCompositor(deps.WindowManagerMango, "kitty", true) require.NoError(t, err) main := readMango(t, home, "config.conf") diff --git a/core/internal/config/session_units_test.go b/core/internal/config/session_units_test.go index cde61d312..320a1736a 100644 --- a/core/internal/config/session_units_test.go +++ b/core/internal/config/session_units_test.go @@ -43,7 +43,7 @@ func TestDisableSessionRemovesEveryWantsLink(t *testing.T) { func TestEnableSessionMangoWithoutTarget(t *testing.T) { t.Setenv("HOME", t.TempDir()) - calls := stubMango(t, mangoconf.Snake, false, nil) + calls := stubMango(t, false, nil) unit, err := EnableSession(deps.WindowManagerMango) require.Error(t, err) assert.Empty(t, unit) diff --git a/core/internal/mangoconf/check.go b/core/internal/mangoconf/check.go new file mode 100644 index 000000000..448135f9c --- /dev/null +++ b/core/internal/mangoconf/check.go @@ -0,0 +1,19 @@ +// Package mangoconf holds what DMS knows about Mango's config files: where they +// live, the session target, and the markers DMS leaves in the binds fragment. +package mangoconf + +import ( + "fmt" + "strings" +) + +// Mango reads each line into a 512-byte buffer and the value into 255 bytes; longer is cut silently. +const maxValueLen = 255 + +func CheckLine(line string) error { + _, value, ok := strings.Cut(line, "=") + if !ok || len(strings.TrimSpace(value)) <= maxValueLen { + return nil + } + return fmt.Errorf("line is %d characters after '='; Mango truncates past %d", len(strings.TrimSpace(value)), maxValueLen) +} diff --git a/core/internal/mangoconf/dialect.go b/core/internal/mangoconf/dialect.go deleted file mode 100644 index e2aa2c795..000000000 --- a/core/internal/mangoconf/dialect.go +++ /dev/null @@ -1,216 +0,0 @@ -// Package mangoconf maps Mango config keys between the run-together names of -// Mango <= 0.17.5 and the snake_case names of upstream 8adc6bc3 (mangowm/mango#1491). -// Neither parser accepts the other's names. -package mangoconf - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "regexp" - "strings" - "sync" -) - -type Dialect int - -const ( - Legacy Dialect = iota - Snake -) - -// Upstream migration order: where two legacy names share a snake name, the -// window-rule spelling comes first and wins the reverse lookup. -var renames = [][2]string{ - {"windowrule-once", "window_rule_once"}, - {"windowrule", "window_rule"}, - {"monitorrule", "monitor_rule"}, - {"tagrule", "tag_rule"}, - {"layerrule", "layer_rule"}, - {"devicerule", "device_rule"}, - {"source-optional", "source_optional"}, - {"exec-once", "exec_once"}, - {"keymode", "key_mode"}, - {"isnoborder", "no_border"}, - {"isnoshadow", "no_shadow"}, - {"isnoradius", "no_radius"}, - {"isnoanimation", "no_animation"}, - {"isnosizehint", "no_size_hint"}, - {"noblur", "no_blur"}, - {"noshadow", "no_shadow"}, - {"noanim", "no_animation"}, - {"nofocus", "no_focus"}, - {"nofadein", "no_fade_in"}, - {"nofadeout", "no_fade_out"}, - {"noswallow", "no_swallow"}, - {"special_gappih", "special_gap_inner_horizontal"}, - {"special_gappiv", "special_gap_inner_vertical"}, - {"special_gappoh", "special_gap_outer_horizontal"}, - {"special_gappov", "special_gap_outer_vertical"}, - {"gappih", "gap_inner_horizontal"}, - {"gappiv", "gap_inner_vertical"}, - {"gappoh", "gap_outer_horizontal"}, - {"gappov", "gap_outer_vertical"}, - {"overviewgappi", "overview_gap_inner"}, - {"overviewgappo", "overview_gap_outer"}, - {"default_mfact", "default_master_factor"}, - {"default_nmaster", "default_master_count"}, - {"animation_curve_opafadein", "animation_curve_opacity_fade_in"}, - {"animation_curve_opafadeout", "animation_curve_opacity_fade_out"}, - {"dwindle_hsplit", "dwindle_horizontal_split"}, - {"dwindle_vsplit", "dwindle_vertical_split"}, - {"fadein_begin_opacity", "fade_in_begin_opacity"}, - {"fadeout_begin_opacity", "fade_out_begin_opacity"}, - {"focusdir_only_zone_overlap", "focus_direction_only_zone_overlap"}, - {"force_fakemaximize", "force_fake_maximize"}, - {"idleinhibit_ignore_visible", "idle_inhibit_ignore_visible"}, - {"idleinhibit_when_focus", "idle_inhibit_when_focus"}, - {"idleinhibit_when_fullscreen", "idle_inhibit_when_fullscreen"}, - {"syncobj_enable", "sync_obj_enable"}, - {"isfakefullscreen", "is_fake_fullscreen"}, - {"isfloating", "is_floating"}, - {"isfullscreen", "is_fullscreen"}, - {"isunglobal", "is_unmanaged_global"}, - {"isglobal", "is_global"}, - {"isnamedscratchpad", "is_named_scratchpad"}, - {"isopensilent", "is_open_silent"}, - {"isoverlay", "is_overlay"}, - {"istagsilent", "is_tag_silent"}, - {"isterm", "is_term"}, - {"appid", "app_id"}, - {"bordercolor", "border_color"}, - {"borderpx", "border_px"}, - {"dropcolor", "drop_color"}, - {"focuscolor", "focus_color"}, - {"globalcolor", "global_color"}, - {"globalkeybinding", "global_key_binding"}, - {"maximizescreencolor", "maximized_screen_color"}, - {"mfact", "master_factor"}, - {"nmaster", "master_count"}, - {"numlockon", "numlock_on"}, - {"offsetx", "offset_x"}, - {"offsety", "offset_y"}, - {"overlaycolor", "overlay_color"}, - {"rootcolor", "root_color"}, - {"scratchpadcolor", "scratchpad_color"}, - {"shadowscolor", "shadows_color"}, - {"sloppyfocus", "sloppy_focus"}, - {"smartgaps", "smart_gaps"}, - {"splitcolor", "split_color"}, - {"urgentcolor", "urgent_color"}, - {"warpcursor", "warp_cursor"}, -} - -var toSnake, toLegacy = func() (map[string]string, map[string]string) { - fwd := make(map[string]string, len(renames)) - rev := make(map[string]string, len(renames)) - for _, r := range renames { - fwd[r[0]] = r[1] - if _, ok := rev[r[1]]; !ok { - rev[r[1]] = r[0] - } - } - return fwd, rev -}() - -// Normalize returns the snake_case name for a key in either dialect. -func Normalize(key string) string { - if s, ok := toSnake[key]; ok { - return s - } - return key -} - -// Key spells a snake_case key in this dialect. -func (d Dialect) Key(snake string) string { - if d == Snake { - return snake - } - if l, ok := toLegacy[snake]; ok { - return l - } - return snake -} - -var keyLine = regexp.MustCompile(`^(\s*)([A-Za-z][A-Za-z_-]*)(\s*=\s*)(.*)$`) - -// Translate respells every line key, and the field keys of rule lines, in this dialect. -func (d Dialect) Translate(text string) string { - lines := strings.Split(text, "\n") - for i, line := range lines { - m := keyLine.FindStringSubmatch(line) - if m == nil { - continue - } - key := Normalize(m[2]) - value := m[4] - switch key { - case "window_rule", "window_rule_once", "tag_rule": - value = d.translateFields(value, nil) - case "layer_rule": - value = d.translateFields(value, legacyLayerFields) - } - lines[i] = m[1] + d.Key(key) + m[3] + value - } - return strings.Join(lines, "\n") -} - -// Layer rules spell these without the window rules' "is" prefix. -var legacyLayerFields = map[string]string{"no_shadow": "noshadow", "no_animation": "noanim"} - -func (d Dialect) translateFields(value string, legacyOverride map[string]string) string { - parts := strings.Split(value, ",") - for i, part := range parts { - k, v, ok := strings.Cut(part, ":") - if !ok { - continue - } - trimmed := strings.TrimSpace(k) - key := d.Key(Normalize(trimmed)) - if l, ok := legacyOverride[Normalize(trimmed)]; ok && d == Legacy { - key = l - } - parts[i] = strings.Replace(k, trimmed, key, 1) + ":" + v - } - return strings.Join(parts, ",") -} - -// Legacy builds carry exec_once as a symbol name, so it cannot tell the dialects apart. -var snakeMarker = []byte("gap_inner_horizontal") - -var ( - detectOnce sync.Once - detected Dialect -) - -// Detect reads the mango binary: snake_case shipped on master builds still reporting 0.17.5, before the 0.18.0 release. -func Detect() Dialect { - detectOnce.Do(func() { - path, err := exec.LookPath("mango") - if err != nil { - return - } - detected = DetectBinary(path) - }) - return detected -} - -func DetectBinary(path string) Dialect { - data, err := os.ReadFile(path) - if err != nil || !bytes.Contains(data, snakeMarker) { - return Legacy - } - return Snake -} - -// Mango reads each line into a 512-byte buffer and the value into 255 bytes; longer is cut silently. -const maxValueLen = 255 - -func CheckLine(line string) error { - _, value, ok := strings.Cut(line, "=") - if !ok || len(strings.TrimSpace(value)) <= maxValueLen { - return nil - } - return fmt.Errorf("line is %d characters after '='; Mango truncates past %d", len(strings.TrimSpace(value)), maxValueLen) -} diff --git a/core/internal/mangoconf/dialect_test.go b/core/internal/mangoconf/dialect_test.go deleted file mode 100644 index ec5758452..000000000 --- a/core/internal/mangoconf/dialect_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package mangoconf - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestTranslateRespellsKeysBothWays(t *testing.T) { - legacy := "exec-once=dms run\nsource=./dms/colors.conf\ngappih = 5\nbordercolor = 0x595959ff\nwindowrule=appid:foo,isfloating:1,isnoanimation:1\nlayerrule=noanim:1,noshadow:1,layer_name:rofi\n# exec-once=kept\n" - snake := "exec_once=dms run\nsource=./dms/colors.conf\ngap_inner_horizontal = 5\nborder_color = 0x595959ff\nwindow_rule=app_id:foo,is_floating:1,no_animation:1\nlayer_rule=no_animation:1,no_shadow:1,layer_name:rofi\n# exec-once=kept\n" - assert.Equal(t, snake, Snake.Translate(legacy)) - assert.Equal(t, legacy, Legacy.Translate(snake)) - assert.Equal(t, legacy, Legacy.Translate(legacy)) - assert.Equal(t, "tag_rule=id:1,master_factor:0.6,master_count:2\n", Snake.Translate("tagrule=id:1,mfact:0.6,nmaster:2\n")) -} - -func TestDetectBinary(t *testing.T) { - dir := t.TempDir() - old := filepath.Join(dir, "old") - cur := filepath.Join(dir, "new") - require.NoError(t, os.WriteFile(old, []byte("\x00run_exec_once\x00gappih\x00"), 0o755)) - require.NoError(t, os.WriteFile(cur, []byte("\x00exec_once\x00gap_inner_horizontal\x00"), 0o755)) - assert.Equal(t, Legacy, DetectBinary(old)) - assert.Equal(t, Snake, DetectBinary(cur)) - assert.Equal(t, Legacy, DetectBinary(filepath.Join(dir, "missing"))) -} - -func TestMigrateLeavesUserConfigUnlessAsked(t *testing.T) { - dir := t.TempDir() - require.NoError(t, os.MkdirAll(filepath.Join(dir, "dms"), 0o755)) - main := filepath.Join(dir, "config.conf") - layout := filepath.Join(dir, "dms", "layout.conf") - input := filepath.Join(dir, "dms", "input.conf") - require.NoError(t, os.WriteFile(main, []byte("exec-once=dms run\n"), 0o644)) - require.NoError(t, os.WriteFile(layout, []byte("borderpx=2\n"), 0o644)) - require.NoError(t, os.WriteFile(input, []byte("numlockon=1\n"), 0o644)) - - changed, err := Snake.Migrate(dir, false) - require.NoError(t, err) - assert.Equal(t, []string{layout, input}, changed) - data, _ := os.ReadFile(main) - assert.Equal(t, "exec-once=dms run\n", string(data)) - data, _ = os.ReadFile(input) - assert.Equal(t, "numlock_on=1\n", string(data)) - - changed, err = Snake.Migrate(dir, true) - require.NoError(t, err) - assert.Equal(t, []string{main}, changed) - backups, _ := filepath.Glob(main + ".backup.*") - assert.Len(t, backups, 1) -} - -func TestMigrateReportsUnwritableFragmentAndContinues(t *testing.T) { - dir := t.TempDir() - require.NoError(t, os.MkdirAll(filepath.Join(dir, "dms"), 0o755)) - binds := filepath.Join(dir, "dms", "binds.conf") - layout := filepath.Join(dir, "dms", "layout.conf") - require.NoError(t, os.WriteFile(binds, []byte("exec-once=foo\n"), 0o444)) - require.NoError(t, os.WriteFile(layout, []byte("borderpx=2\n"), 0o644)) - - changed, err := Snake.Migrate(dir, false) - assert.ErrorContains(t, err, "binds.conf") - assert.Equal(t, []string{layout}, changed) -} diff --git a/core/internal/mangoconf/migrate.go b/core/internal/mangoconf/migrate.go deleted file mode 100644 index c06e89fb8..000000000 --- a/core/internal/mangoconf/migrate.go +++ /dev/null @@ -1,57 +0,0 @@ -package mangoconf - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "time" -) - -// DMS-owned fragments under mango/dms; config.conf is the user's and only -// migrated on request. -var fragments = []string{"binds.conf", "colors.conf", "layout.conf", "cursor.conf", "input.conf", "outputs.conf", "windowrules.conf"} - -// Migrate respells the dms fragments, and config.conf (after a backup) when includeMain is set. -// It returns the files rewritten; an unwritable file is reported without stopping the others. -func (d Dialect) Migrate(mangoDir string, includeMain bool) ([]string, error) { - var changed []string - var errs []error - paths := make([]string, 0, len(fragments)+1) - for _, f := range fragments { - paths = append(paths, filepath.Join(mangoDir, "dms", f)) - } - if includeMain { - paths = append(paths, filepath.Join(mangoDir, "config.conf")) - } - for _, path := range paths { - data, err := os.ReadFile(path) - if err != nil { - continue - } - translated := d.Translate(string(data)) - if translated == string(data) { - continue - } - if filepath.Base(path) == "config.conf" { - backup := path + ".backup." + time.Now().Format("2006-01-02_15-04-05") - if err := os.WriteFile(backup, data, 0o644); err != nil { - errs = append(errs, fmt.Errorf("backup %s: %w", path, err)) - continue - } - } - if err := os.WriteFile(path, []byte(translated), 0o644); err != nil { - errs = append(errs, fmt.Errorf("write %s: %w", path, err)) - continue - } - changed = append(changed, path) - } - return changed, errors.Join(errs...) -} - -func (d Dialect) String() string { - if d == Snake { - return "snake" - } - return "legacy" -} diff --git a/core/internal/matugen/matugen.go b/core/internal/matugen/matugen.go index 0e79f3474..762724a36 100644 --- a/core/internal/matugen/matugen.go +++ b/core/internal/matugen/matugen.go @@ -20,7 +20,6 @@ import ( "github.com/duxlabs-dev/dux-shell/core/internal/dank16" "github.com/duxlabs-dev/dux-shell/core/internal/log" - "github.com/duxlabs-dev/dux-shell/core/internal/mangoconf" "github.com/duxlabs-dev/dux-shell/core/internal/utils" "github.com/godbus/dbus/v5" "github.com/lucasb-eyer/go-colorful" @@ -44,7 +43,6 @@ const ( TemplateKindGTK TemplateKindVSCode TemplateKindEmacs - TemplateKindMango ) type TemplateDef struct { @@ -63,7 +61,7 @@ var templateRegistry = []TemplateDef{ {ID: "gtk", Kind: TemplateKindGTK, RunUnconditionally: true}, {ID: "niri", Commands: []string{"niri"}, ConfigFile: "niri.toml"}, {ID: "hyprland", Commands: []string{"Hyprland"}, ConfigFile: "hyprland.toml"}, - {ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml", RequiredEnv: "MANGO_INSTANCE_SIGNATURE", Kind: TemplateKindMango}, + {ID: "mangowc", Commands: []string{"mango"}, ConfigFile: "mangowc.toml", RequiredEnv: "MANGO_INSTANCE_SIGNATURE"}, {ID: "qt5ct", Commands: []string{"qt5ct"}, ConfigFile: "qt5ct.toml"}, {ID: "qt6ct", Commands: []string{"qt6ct"}, ConfigFile: "qt6ct.toml"}, {ID: "fcitx5", Commands: []string{"fcitx5"}, ConfigDirs: []string{"fcitx5"}, ConfigFile: "fcitx5.toml"}, @@ -725,12 +723,6 @@ output_path = '%s' for _, editor := range vscodeEditors { appendVSCodeConfig(cfgFile, editor.name, editor.extensionsDir(homeDir), opts.ShellDir) } - case TemplateKindMango: - configFile := tmpl.ConfigFile - if mangoconf.Detect() == mangoconf.Snake { - configFile = "mangowc-snake.toml" - } - appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, configFile) case TemplateKindEmacs: if utils.EmacsConfigDir() != "" { appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile) diff --git a/core/internal/matugen/matugen_test.go b/core/internal/matugen/matugen_test.go index 6a37cc984..1ef9a1ddf 100644 --- a/core/internal/matugen/matugen_test.go +++ b/core/internal/matugen/matugen_test.go @@ -511,7 +511,7 @@ func TestBuildMergedConfigColorsOnly(t *testing.T) { assert.False(t, strings.Contains(content, "output_path = 'CONFIG_DIR/"), "colors-only config should not emit app template outputs") } -func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) { +func TestBuildMergedConfigMangoSession(t *testing.T) { t.Setenv("MANGO_INSTANCE_SIGNATURE", "") tempDir := t.TempDir() @@ -555,6 +555,31 @@ func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) { t.Fatalf("failed to read merged config: %v", err) } assert.NotContains(t, string(output), "[templates.dmsmango]") + + socket := filepath.Join(tempDir, "mango.sock") + if err := os.WriteFile(socket, nil, 0o600); err != nil { + t.Fatal(err) + } + t.Setenv("MANGO_INSTANCE_SIGNATURE", socket) + checker := mocks_utils.NewMockAppChecker(t) + checker.EXPECT().AnyCommandExists("mango").Return(true) + opts.AppChecker = checker + cfgFile, err = os.CreateTemp(tempDir, "merged-active-*.toml") + if err != nil { + t.Fatal(err) + } + defer cfgFile.Close() + if err := buildMergedConfig(opts, cfgFile, filepath.Join(tempDir, "templates")); err != nil { + t.Fatal(err) + } + if err := cfgFile.Close(); err != nil { + t.Fatal(err) + } + output, err = os.ReadFile(cfgFile.Name()) + if err != nil { + t.Fatal(err) + } + assert.Contains(t, string(output), mangowcConfig) } func TestAppendConfigConfigDirExists(t *testing.T) { diff --git a/core/internal/tui/views_config.go b/core/internal/tui/views_config.go index 58147dab0..1f8792f5d 100644 --- a/core/internal/tui/views_config.go +++ b/core/internal/tui/views_config.go @@ -179,7 +179,7 @@ func (m Model) configReplacementNote() string { if m.useSystemdConfig() && mangoconf.SessionTargetInstalled() { return "Replacing Mango writes the DMS Mango template and starts the user systemd dms service with mango-session.target. Existing binds, monitor and window rules move into the dms/ files." } - return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with " + mangoconf.Detect().Key("exec_once") + "=dms run. Existing binds, monitor and window rules move into the dms/ files." + return "Replacing Mango writes the DMS Mango template and starts DMS from Mango with exec_once=dms run. Existing binds, monitor and window rules move into the dms/ files." case config.BindsConfigType: return "Keep uses your binds as they are; replace starts from the DMS stock binds. A timestamped backup is kept either way." case "Ghostty": diff --git a/core/internal/tui/views_install.go b/core/internal/tui/views_install.go index ca9ae2940..5f5a0eb46 100644 --- a/core/internal/tui/views_install.go +++ b/core/internal/tui/views_install.go @@ -197,7 +197,7 @@ func (m Model) troubleshootingHints() (autostart, logs string) { case deps.WindowManagerHyprland: autostart = `remove hl.exec_cmd("dms run") from ~/.config/hypr/hyprland.lua` default: - autostart = "remove '" + mangoconf.Detect().Key("exec_once") + "=dms run' from ~/.config/mango/config.conf" + autostart = "remove 'exec_once=dms run' from ~/.config/mango/config.conf" } return autostart, logs } @@ -206,7 +206,7 @@ func (m Model) troubleshootingHints() (autostart, logs string) { if mangoconf.SessionTargetInstalled() { return "rm ~/.config/systemd/user/mango-session.target.wants/dms.service", "journalctl --user -u dms" } - return "remove '" + mangoconf.Detect().Key("exec_once") + "=dms run' from ~/.config/mango/config.conf", "qs -p ~/.config/quickshell/dms log" + return "remove 'exec_once=dms run' from ~/.config/mango/config.conf", "qs -p ~/.config/quickshell/dms log" } return "systemctl --user disable dms", "journalctl --user -u dms" } diff --git a/core/internal/windowrules/providers/mango_parser.go b/core/internal/windowrules/providers/mango_parser.go index cb888f698..5755ff563 100644 --- a/core/internal/windowrules/providers/mango_parser.go +++ b/core/internal/windowrules/providers/mango_parser.go @@ -11,19 +11,17 @@ import ( "github.com/duxlabs-dev/dux-shell/core/internal/windowrules" ) -// Mango window rules are flat `windowrule=key:value,...` lines (`window_rule=` -// with snake_case fields after the upstream rename; both are read). DMS-managed rules -// live in dms/windowrules.conf (sourced from config.conf), each preceded by an -// `# @id= @name=` comment so they round-trip. +// DMS-managed rules live in dms/windowrules.conf (sourced from config.conf), +// each preceded by an `# @id= @name=` comment so they round-trip. type MangoWindowRule struct { Source string Fields map[string]string } -var mangoWindowRuleRegex = regexp.MustCompile(`^(?:windowrule|window_rule)\s*=\s*(.+)$`) +var mangoWindowRuleRegex = regexp.MustCompile(`^window_rule\s*=\s*(.+)$`) var mangoMetaCommentRegex = regexp.MustCompile(`^#\s*@id=(\S*)\s*@name=(.*)$`) -var mangoOnceRuleRegex = regexp.MustCompile(`^(?:windowrule-once|window_rule_once)\s*=`) +var mangoOnceRuleRegex = regexp.MustCompile(`^window_rule_once\s*=`) // Fields the editor models; anything else on a DMS rule line is carried through saves. var mangoModeledFields = map[string]bool{ @@ -42,7 +40,7 @@ func mangoUnmodeledFields(value string) [][2]string { if !ok { continue } - key := mangoconf.Normalize(strings.TrimSpace(k)) + key := strings.TrimSpace(k) sized := (key == "width" || key == "height") && (!hasWidth || !hasHeight) if key != "" && (!mangoModeledFields[key] || sized) { extra = append(extra, [2]string{key, strings.TrimSpace(v)}) @@ -62,7 +60,7 @@ func parseMangoWindowRuleLine(value string) map[string]string { if !ok { continue } - key := mangoconf.Normalize(strings.TrimSpace(before)) + key := strings.TrimSpace(before) val := strings.TrimSpace(after) if key != "" { fields[key] = val @@ -89,7 +87,7 @@ func mangoOverridePath(configDir string) string { return filepath.Join(configDir, "dms", "windowrules.conf") } -// parseMangoRulesFile reads a config file and returns its windowrule= lines. +// parseMangoRulesFile reads a config file and returns its window_rule= lines. func parseMangoRulesFile(path, source string) []MangoWindowRule { data, err := os.ReadFile(path) if err != nil { @@ -215,11 +213,11 @@ func ConvertMangoRulesToWindowRules(mangoRules []MangoWindowRule) []windowrules. return result } -func formatMangoRule(rule windowrules.WindowRule, dialect mangoconf.Dialect, extra ...[2]string) string { +func formatMangoRule(rule windowrules.WindowRule, extra ...[2]string) string { var parts []string add := func(k, v string) { if v != "" { - parts = append(parts, dialect.Key(k)+":"+v) + parts = append(parts, k+":"+v) } } @@ -235,7 +233,7 @@ func formatMangoRule(rule windowrules.WindowRule, dialect mangoconf.Dialect, ext addBool := func(k string, b *bool) { if b != nil { - parts = append(parts, dialect.Key(k)+":"+mangoBoolStr(b)) + parts = append(parts, k+":"+mangoBoolStr(b)) } } addBool("is_floating", rule.Actions.OpenFloating) @@ -249,19 +247,18 @@ func formatMangoRule(rule windowrules.WindowRule, dialect mangoconf.Dialect, ext add(kv[0], kv[1]) } - return dialect.Key("window_rule") + "=" + strings.Join(parts, ",") + return "window_rule=" + strings.Join(parts, ",") } type MangoWritableProvider struct { configDir string - dialect mangoconf.Dialect // Filled by LoadDMSRules so the following write keeps what the editor cannot show. extraFields map[string][][2]string onceLines []string } func NewMangoWritableProvider(configDir string) *MangoWritableProvider { - return &MangoWritableProvider{configDir: configDir, dialect: mangoconf.Detect()} + return &MangoWritableProvider{configDir: configDir} } func (p *MangoWritableProvider) Name() string { return "mango" } @@ -360,7 +357,7 @@ func (p *MangoWritableProvider) WriteDMSRules(rules []windowrules.WindowRule) er if id == "" { id = fmt.Sprintf("rule_%d", i) } - line := formatMangoRule(r, p.dialect, p.extraFields[r.ID]...) + line := formatMangoRule(r, p.extraFields[r.ID]...) if err := mangoconf.CheckLine(line); err != nil { return fmt.Errorf("rule %q: %w", r.Name, err) } @@ -369,7 +366,7 @@ func (p *MangoWritableProvider) WriteDMSRules(rules []windowrules.WindowRule) er sb.WriteString("\n\n") } for _, line := range p.onceLines { - sb.WriteString(p.dialect.Translate(line)) + sb.WriteString(line) sb.WriteString("\n") } diff --git a/core/internal/windowrules/providers/mango_parser_test.go b/core/internal/windowrules/providers/mango_parser_test.go index 1953bc6eb..8debcf1eb 100644 --- a/core/internal/windowrules/providers/mango_parser_test.go +++ b/core/internal/windowrules/providers/mango_parser_test.go @@ -6,14 +6,13 @@ import ( "strings" "testing" - "github.com/duxlabs-dev/dux-shell/core/internal/mangoconf" "github.com/duxlabs-dev/dux-shell/core/internal/windowrules" ) -func TestParseMangoWindowRuleLineReadsBothDialects(t *testing.T) { +func TestParseMangoWindowRuleLineSplitsFields(t *testing.T) { for _, line := range []string{ - "appid:firefox,title:Gmail,isfloating:1,tags:2,monitor:HDMI-A-1", "app_id:firefox,title:Gmail,is_floating:1,tags:2,monitor:HDMI-A-1", + "app_id: firefox , title:Gmail,is_floating:1,tags:2,monitor:HDMI-A-1", } { fields := parseMangoWindowRuleLine(line) want := map[string]string{"app_id": "firefox", "title": "Gmail", "is_floating": "1", "tags": "2", "monitor": "HDMI-A-1"} @@ -25,13 +24,10 @@ func TestParseMangoWindowRuleLineReadsBothDialects(t *testing.T) { } } -func TestMangoRulesWrittenInInstalledDialect(t *testing.T) { +func TestMangoRulesWrittenInSnakeCase(t *testing.T) { floating := true rule := windowrules.WindowRule{MatchCriteria: windowrules.MatchCriteria{AppID: "discord"}, Actions: windowrules.Actions{OpenFloating: &floating, NoAnim: &floating}} - if got, want := formatMangoRule(rule, mangoconf.Legacy), "windowrule=appid:discord,isfloating:1,isnoanimation:1"; got != want { - t.Errorf("legacy = %q, want %q", got, want) - } - if got, want := formatMangoRule(rule, mangoconf.Snake), "window_rule=app_id:discord,is_floating:1,no_animation:1"; got != want { + if got, want := formatMangoRule(rule), "window_rule=app_id:discord,is_floating:1,no_animation:1"; got != want { t.Errorf("snake = %q, want %q", got, want) } @@ -39,7 +35,7 @@ func TestMangoRulesWrittenInInstalledDialect(t *testing.T) { rulesPath := filepath.Join(dir, "dms", "windowrules.conf") _ = os.MkdirAll(filepath.Dir(rulesPath), 0o755) _ = os.WriteFile(rulesPath, []byte("# @id=a @name=A\nwindow_rule=app_id:discord,is_floating:1\n"), 0o644) - loaded, err := (&MangoWritableProvider{configDir: dir, dialect: mangoconf.Snake}).LoadDMSRules() + loaded, err := (&MangoWritableProvider{configDir: dir}).LoadDMSRules() if err != nil || len(loaded) != 1 || loaded[0].MatchCriteria.AppID != "discord" || loaded[0].Actions.OpenFloating == nil { t.Fatalf("snake rule not loaded: %+v, %v", loaded, err) } @@ -47,7 +43,7 @@ func TestMangoRulesWrittenInInstalledDialect(t *testing.T) { func TestConvertMangoRulesToWindowRules(t *testing.T) { mangoRules := []MangoWindowRule{ - {Source: "config.conf", Fields: parseMangoWindowRuleLine("appid:discord,tags:9,isfloating:1,noblur:1")}, + {Source: "config.conf", Fields: parseMangoWindowRuleLine("app_id:discord,tags:9,is_floating:1,no_blur:1")}, } rules := ConvertMangoRulesToWindowRules(mangoRules) if len(rules) != 1 { @@ -141,9 +137,9 @@ func TestMangoRuleSaveKeepsUnmodeledFieldsAndOnceRules(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "dms", "windowrules.conf") _ = os.MkdirAll(filepath.Dir(path), 0o755) - _ = os.WriteFile(path, []byte("# @id=a @name=A\nwindowrule=appid:mpv,isglobal:1,offsetx:20,width:0.5\nwindowrule-once=appid:firefox,tags:2\n"), 0o644) + _ = os.WriteFile(path, []byte("# @id=a @name=A\nwindow_rule=app_id:mpv,is_global:1,offset_x:20,width:0.5\nwindow_rule_once=app_id:firefox,tags:2\n"), 0o644) - p := &MangoWritableProvider{configDir: dir, dialect: mangoconf.Legacy} + p := &MangoWritableProvider{configDir: dir} floating := true if err := p.SetRule(windowrules.WindowRule{ID: "b", Name: "B", MatchCriteria: windowrules.MatchCriteria{AppID: "foot"}, Actions: windowrules.Actions{OpenFloating: &floating}}); err != nil { t.Fatal(err) @@ -151,9 +147,9 @@ func TestMangoRuleSaveKeepsUnmodeledFieldsAndOnceRules(t *testing.T) { data, _ := os.ReadFile(path) got := string(data) for _, want := range []string{ - "windowrule=appid:mpv,isglobal:1,offsetx:20,width:0.5\n", - "windowrule=appid:foot,isfloating:1\n", - "windowrule-once=appid:firefox,tags:2\n", + "window_rule=app_id:mpv,is_global:1,offset_x:20,width:0.5\n", + "window_rule=app_id:foot,is_floating:1\n", + "window_rule_once=app_id:firefox,tags:2\n", } { if !strings.Contains(got, want) { t.Errorf("missing %q in:\n%s", want, got) diff --git a/quickshell/Common/OutputModel.js b/quickshell/Common/OutputModel.js index 36a7620eb..aa8e433e1 100644 --- a/quickshell/Common/OutputModel.js +++ b/quickshell/Common/OutputModel.js @@ -420,7 +420,7 @@ function parseMangoOutputs(content) { const result = {}; for (const line of content.split("\n")) { const trimmed = line.trim(); - const match = trimmed.match(/^(?:monitorrule|monitor_rule)\s*=\s*(.*)$/); + const match = trimmed.match(/^monitor_rule\s*=\s*(.*)$/); if (!match) continue; const entry = parseMangoRule(match[1]); @@ -445,10 +445,10 @@ function mangoRuleFields(rule) { // Mango applies only the first matching monitor rule and resets hdr/icc/disable a rule omits, // so unmodeled fields of every existing rule for an output are carried into DMS's rule, first one winning. -function mangoMonitorRuleLines(outputsData, existingText, snakeKeys) { +function mangoMonitorRuleLines(outputsData, existingText) { const carried = {}; for (const line of (existingText || "").split("\n")) { - const match = line.trim().match(/^(?:monitorrule|monitor_rule)\s*=\s*(.*)$/); + const match = line.trim().match(/^monitor_rule\s*=\s*(.*)$/); if (!match) continue; const fields = mangoRuleFields(match[1]); @@ -496,7 +496,7 @@ function mangoMonitorRuleLines(outputsData, existingText, snakeKeys) { } if (output.enabled === false) fields.push("disable:1"); - lines.push((snakeKeys ? "monitor_rule=" : "monitorrule=") + fields.join(",")); + lines.push("monitor_rule=" + fields.join(",")); } return lines; } diff --git a/quickshell/Modules/Settings/CompositorLayoutTab.qml b/quickshell/Modules/Settings/CompositorLayoutTab.qml index 4b47c9e5a..292fdfa10 100644 --- a/quickshell/Modules/Settings/CompositorLayoutTab.qml +++ b/quickshell/Modules/Settings/CompositorLayoutTab.qml @@ -739,7 +739,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`; } SettingsSliderRow { - tags: ["mangowc", "mango", "gaps", "override", "inner", "gappih", "gappiv"] + tags: ["mangowc", "mango", "gaps", "override", "inner", "gap_inner_horizontal", "gap_inner_vertical", "gappih", "gappiv"] settingKey: "mangoLayoutGapsOverride" resetKeys: [] text: I18n.tr("Inner gaps") @@ -752,7 +752,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`; } SettingsSliderRow { - tags: ["mangowc", "mango", "gaps", "override", "outer", "edge", "gappoh", "gappov"] + tags: ["mangowc", "mango", "gaps", "override", "outer", "edge", "gap_outer_horizontal", "gap_outer_vertical", "gappoh", "gappov"] settingKey: "mangoLayoutGapsOutOverride" text: I18n.tr("Outer gaps") visible: SettingsData.mangoLayoutGapsOverride >= 0 @@ -772,7 +772,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`; } SettingsToggleSliderRow { - tags: ["mangowc", "mango", "border", "override", "borderpx"] + tags: ["mangowc", "mango", "border", "override", "border_px", "borderpx"] settingKey: "mangoLayoutBorderSizeEnabled" text: I18n.tr("Override border size") checked: SettingsData.mangoLayoutBorderSize >= 0 diff --git a/quickshell/Services/MangoService.qml b/quickshell/Services/MangoService.qml index 9122634c0..ba2b1b5d0 100644 --- a/quickshell/Services/MangoService.qml +++ b/quickshell/Services/MangoService.qml @@ -31,9 +31,6 @@ Singleton { readonly property string inputPath: mangoDmsDir + "/input.conf" readonly property string windowRulesPath: mangoDmsDir + "/windowrules.conf" - // Mango after 0.17.5 renamed every config key to snake_case with no aliases. - property bool snakeKeys: false - property bool dialectReady: false property bool inputGenerationPending: false property bool inputGenerationRunning: false property int _lastGapValue: -1 @@ -587,21 +584,11 @@ Singleton { Connections { target: CompositorService function onIsMangoChanged() { - if (CompositorService.isMango) - detectDialect(); - } - } - - // Also respells the DMS fragments for the installed Mango; an older dms keeps legacy keys. - function detectDialect() { - Proc.runCommand("mango-dialect", [Proc.dmsBin, "config", "mango-migrate"], (output, exitCode) => { - if (exitCode !== 0) - log.warn("mango-migrate exited", exitCode, "- some fragments may keep the old key spelling"); - root.snakeKeys = output.trim().split("\n").pop() === "snake"; - root.dialectReady = true; + if (!CompositorService.isMango) + return; generateLayoutConfig(); generateInputConfig(); - }); + } } function generateOutputsConfig(outputsData, callback, skipReload) { @@ -612,7 +599,7 @@ Singleton { } // outputs.conf is sourced first, so its rules win over config.conf's; read both for carried fields. Proc.runCommand("mango-read-outputs", ["sh", "-c", `cat "${outputsPath}" "${configPath}" 2>/dev/null`], existing => { - const lines = ["# Auto-generated by DMS - do not edit manually", ""].concat(OutputModel.mangoMonitorRuleLines(outputsData, existing, root.snakeKeys), [""]); + const lines = ["# Auto-generated by DMS - do not edit manually", ""].concat(OutputModel.mangoMonitorRuleLines(outputsData, existing), [""]); const content = lines.join("\n"); suppressWatchedConfigReloads(1500); @@ -646,21 +633,16 @@ Singleton { const gapsOut = (gapsOverride >= 0 && SettingsData.mangoLayoutGapsOutOverride >= 0) ? SettingsData.mangoLayoutGapsOutOverride : gapsIn; const borderSize = (typeof SettingsData !== "undefined" && SettingsData.mangoLayoutBorderSize >= 0) ? SettingsData.mangoLayoutBorderSize : defaultBorderSize; - const snake = root.snakeKeys; let content = `# Auto-generated by DMS - do not edit manually border_radius=${cornerRadius} -${snake ? "border_px" : "borderpx"}=${borderSize} +border_px=${borderSize} `; if (manageGaps) - content += snake ? `gap_inner_horizontal=${gapsIn} + content += `gap_inner_horizontal=${gapsIn} gap_inner_vertical=${gapsIn} gap_outer_horizontal=${gapsOut} gap_outer_vertical=${gapsOut} -` : `gappih=${gapsIn} -gappiv=${gapsIn} -gappoh=${gapsOut} -gappov=${gapsOut} `; suppressWatchedConfigReloads(1500); @@ -727,14 +709,9 @@ cursor_size=${size}`; inputGenerationPending = false; if (!CompositorService.isMango) return; - // numlock is spelled per dialect, so the first write waits for mango-migrate - if (!dialectReady) { - inputGenerationPending = true; - return; - } inputGenerationRunning = true; - const content = ["# Auto-generated by DMS - do not edit manually"].concat(mangoInputLines(SettingsData, snakeKeys), [""]).join("\n"); + const content = ["# Auto-generated by DMS - do not edit manually"].concat(mangoInputLines(SettingsData), [""]).join("\n"); // Exit 3 = unchanged, so a shell start does not reload Mango for nothing const script = `mkdir -p "${mangoDmsDir}" && cat > "${inputPath}.tmp" << 'EOF'\n${content}EOF\nif cmp -s "${inputPath}.tmp" "${inputPath}"; then rm -f "${inputPath}.tmp"; exit 3; fi\nmv -f "${inputPath}.tmp" "${inputPath}"`; suppressWatchedConfigReloads(1500); @@ -757,7 +734,7 @@ cursor_size=${size}`; } // Mouse and trackpad keys are the split names Mango has had since 0.16.1; libinput enums are numeric - function mangoInputLines(s, snake) { + function mangoInputLines(s) { const flag = value => value ? 1 : 0; const profile = name => name === "flat" ? 1 : name === "adaptive" ? 2 : -1; const scrollMethod = name => ({ @@ -806,7 +783,7 @@ cursor_size=${size}`; add("repeat_delay", s.keyboardRepeatDelay); if (s.keyboardRepeatRate > 0) add("repeat_rate", s.keyboardRepeatRate); - add(snake ? "numlock_on" : "numlockon", flag(s.keyboardNumlock)); + add("numlock_on", flag(s.keyboardNumlock)); return lines; } } diff --git a/quickshell/matugen/configs/mangowc-snake.toml b/quickshell/matugen/configs/mangowc-snake.toml deleted file mode 100644 index f4f00c0b5..000000000 --- a/quickshell/matugen/configs/mangowc-snake.toml +++ /dev/null @@ -1,4 +0,0 @@ -[templates.dmsmango] -input_path = 'SHELL_DIR/matugen/templates/mango-colors-snake.conf' -output_path = 'CONFIG_DIR/mango/dms/colors.conf' -post_hook = 'sh -c "[ -n \"$MANGO_INSTANCE_SIGNATURE\" ] && mmsg dispatch reload_config || true"' diff --git a/quickshell/matugen/templates/mango-colors-snake.conf b/quickshell/matugen/templates/mango-colors-snake.conf deleted file mode 100644 index 88cae24b9..000000000 --- a/quickshell/matugen/templates/mango-colors-snake.conf +++ /dev/null @@ -1,6 +0,0 @@ -# ! Auto-generated file. Do not edit directly. -# Remove source = ./dms/colors.conf from your config to override. - -border_color = 0x{{colors.outline.default.hex_stripped}}ff -focus_color = 0x{{colors.primary.default.hex_stripped}}ff -urgent_color = 0x{{colors.error.default.hex_stripped}}ff diff --git a/quickshell/matugen/templates/mango-colors.conf b/quickshell/matugen/templates/mango-colors.conf index 72d8ece20..88cae24b9 100644 --- a/quickshell/matugen/templates/mango-colors.conf +++ b/quickshell/matugen/templates/mango-colors.conf @@ -1,6 +1,6 @@ # ! Auto-generated file. Do not edit directly. # Remove source = ./dms/colors.conf from your config to override. -bordercolor = 0x{{colors.outline.default.hex_stripped}}ff -focuscolor = 0x{{colors.primary.default.hex_stripped}}ff -urgentcolor = 0x{{colors.error.default.hex_stripped}}ff +border_color = 0x{{colors.outline.default.hex_stripped}}ff +focus_color = 0x{{colors.primary.default.hex_stripped}}ff +urgent_color = 0x{{colors.error.default.hex_stripped}}ff diff --git a/quickshell/tests/config-include.test.mjs b/quickshell/tests/config-include.test.mjs index 2d1af4e14..528203863 100644 --- a/quickshell/tests/config-include.test.mjs +++ b/quickshell/tests/config-include.test.mjs @@ -54,7 +54,7 @@ test("repair preserves config and fragments, backs up, and adds live includes on } }); -test("mango input fragment maps choices onto libinput numbers and spells numlock per dialect", () => { +test("mango input fragment maps choices onto libinput numbers and spells numlock in snake_case", () => { const SPEC = loadScript(new URL("../Common/settings/SettingsSpec.js", import.meta.url)).SPEC; const source = readFileSync(new URL("../Services/MangoService.qml", import.meta.url), "utf8"); const found = source.match(/^ function mangoInputLines\([^)]*\) \{\n[\s\S]*?^ \}/m); @@ -62,7 +62,7 @@ test("mango input fragment maps choices onto libinput numbers and spells numlock const scope = vm.createContext({}); vm.runInContext(found[0], scope); const defaults = Object.fromEntries(Object.keys(SPEC).filter(key => /^(mouse|touchpad|keyboard)[A-Z]|^focusFollowsMouse$/.test(key)).map(key => [key, SPEC[key].def])); - const lines = (overrides, snake = false) => [...scope.mangoInputLines(Object.assign({}, defaults, overrides), snake)]; + const lines = overrides => [...scope.mangoInputLines(Object.assign({}, defaults, overrides))]; const value = (list, key) => list.find(line => line.startsWith(key + "="))?.slice(key.length + 1); const stock = lines({}); @@ -79,8 +79,8 @@ test("mango input fragment maps choices onto libinput numbers and spells numlock assert.equal(value(chosen, "xkb_rules_layout"), "us,de"); assert.equal(value(chosen, "xkb_rules_variant"), undefined, "empty fields stay out"); assert.equal(value(chosen, "repeat_delay"), "300"); - assert.equal(value(chosen, "numlockon"), "0"); - assert.equal(value(lines({ keyboardNumlock: true }, true), "numlock_on"), "1"); + assert.equal(value(chosen, "numlock_on"), "0"); + assert.equal(value(lines({ keyboardNumlock: true }), "numlock_on"), "1"); assert.equal(value(stock, "sloppy_focus"), undefined, "untouched focus mode stays Mango's"); for (const mode of [0, 1]) diff --git a/quickshell/tests/fixtures/outputs/mango-monitors.conf b/quickshell/tests/fixtures/outputs/mango-monitors.conf index c9ff4f75c..8a90d27c9 100644 --- a/quickshell/tests/fixtures/outputs/mango-monitors.conf +++ b/quickshell/tests/fixtures/outputs/mango-monitors.conf @@ -1,8 +1,8 @@ # Auto-generated by DMS - do not edit manually -monitorrule=name:^DP-1$,width:2560,height:1440,refresh:143.998,x:0,y:0,scale:1.25,rr:1,vrr:1 -monitorrule=name:^eDP-1$,x:0,y:1440,scale:1,rr:0,vrr:0 -monitorrule=name:HDMI-A-1,width:1920,height:1080,refresh:60,x:2048,y:0,scale:1,rr:5,vrr:0 -monitorrule=width:1920,height:1080 -monitorrule=name:^DP-2$,width:1920,height:1080,refresh:59.95,x:-1920,y:0,scale:1.333,rr:7,vrr:1 +monitor_rule=name:^DP-1$,width:2560,height:1440,refresh:143.998,x:0,y:0,scale:1.25,rr:1,vrr:1 +monitor_rule=name:^eDP-1$,x:0,y:1440,scale:1,rr:0,vrr:0 +monitor_rule=name:HDMI-A-1,width:1920,height:1080,refresh:60,x:2048,y:0,scale:1,rr:5,vrr:0 +monitor_rule=width:1920,height:1080 +monitor_rule=name:^DP-2$,width:1920,height:1080,refresh:59.95,x:-1920,y:0,scale:1.333,rr:7,vrr:1 monitor_rule=name:^DP-3$,rr:9 diff --git a/quickshell/tests/output-model.test.mjs b/quickshell/tests/output-model.test.mjs index f589990c4..d6595eabf 100644 --- a/quickshell/tests/output-model.test.mjs +++ b/quickshell/tests/output-model.test.mjs @@ -79,7 +79,7 @@ test("hyprland lua parser: quoted and long-bracket strings, hdr fields, mode-les assert.equal(model.parseHyprlandLuaMonitorLine("hl.monitor({ scale = 1 })"), null); }); -test("mango parser: both key spellings, anchors stripped, defaults filled, nameless rule skipped, out-of-range rotation is Normal", () => { +test("mango parser: anchors stripped, defaults filled, nameless rule skipped, out-of-range rotation is Normal", () => { const parsed = plain(model.parseMangoOutputs(text("mango-monitors.conf"))); assert.deepEqual(Object.keys(parsed), ["DP-1", "eDP-1", "HDMI-A-1", "DP-2", "DP-3"]); assert.deepEqual(parsed["DP-1"], { name: "DP-1", logical: { x: 0, y: 0, scale: 1.25, transform: "90" }, modes: [{ width: 2560, height: 1440, refresh_rate: 143998 }], current_mode: 0, vrr_enabled: true, vrr_supported: true }); @@ -306,21 +306,20 @@ test("modeAlreadyCurrent tolerates the refresh tolerance, restoreModeValue keeps assert.equal(model.niriCurrentMode({ current_mode: 0 }), null); }); -test("mango rule writer carries hdr/icc/disable from existing rules, keeps fractional refresh, honours the dialect", () => { +test("mango rule writer carries hdr/icc/disable from existing rules, keeps fractional refresh, writes snake_case", () => { const outputs = { "DP-1": { configured_mode: "2560x1440@59.951", logical: { x: 0, y: 0, scale: 1, transform: "Normal" }, vrr_enabled: true, enabled: true }, "HDMI-A-1": { modes: [{ width: 1920, height: 1080, refresh_rate: 60000 }], current_mode: 0, logical: { x: 2560, y: 0, scale: 1, transform: "90" }, enabled: false } }; const existing = [ - "monitorrule=name:^DP-1$,width:2560,height:1440,refresh:60,x:0,y:0,scale:1,rr:0,vrr:0,icc:/p.icc,disable:1", + "monitor_rule=name:^DP-1$,width:2560,height:1440,refresh:60,x:0,y:0,scale:1,rr:0,vrr:0,icc:/p.icc,disable:1", "monitor_rule=name:DP-1,hdr:1,icc:/ignored.icc", - "monitorrule=name:^HDMI-A-1$,primary:1" + "monitor_rule=name:^HDMI-A-1$,primary:1" ].join("\n"); - assert.deepEqual(model.mangoMonitorRuleLines(outputs, existing, false), [ - "monitorrule=name:^DP-1$,width:2560,height:1440,refresh:59.951,x:0,y:0,scale:1,rr:0,vrr:1,icc:/p.icc,hdr:1", - "monitorrule=name:^HDMI-A-1$,width:1920,height:1080,refresh:60,x:2560,y:0,scale:1,rr:1,vrr:0,primary:1,disable:1" + assert.deepEqual(model.mangoMonitorRuleLines(outputs, existing), [ + "monitor_rule=name:^DP-1$,width:2560,height:1440,refresh:59.951,x:0,y:0,scale:1,rr:0,vrr:1,icc:/p.icc,hdr:1", + "monitor_rule=name:^HDMI-A-1$,width:1920,height:1080,refresh:60,x:2560,y:0,scale:1,rr:1,vrr:0,primary:1,disable:1" ]); - assert.ok(model.mangoMonitorRuleLines(outputs, "", true).every(line => line.startsWith("monitor_rule="))); }); test("hyprland hardwareDetails gate vrr and color modes like Hyprland does, and gate nothing when absent", () => { diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 7a4cf6419..e57af0cfe 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -12697,6 +12697,8 @@ "tabIndex": 37, "category": "System & integrations", "keywords": [ + "gap_inner_horizontal", + "gap_inner_vertical", "gappih", "gappiv", "gaps", @@ -12964,6 +12966,8 @@ "category": "System & integrations", "keywords": [ "edge", + "gap_outer_horizontal", + "gap_outer_vertical", "gappoh", "gappov", "gaps", @@ -12999,6 +13003,7 @@ "category": "System & integrations", "keywords": [ "border", + "border_px", "borderpx", "integrations", "mango",