mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-23 11:44:00 -04:00
refactor(mango): deprecate pre-mango v18.0 binds & legacy management
(cherry picked from commit d8734bbe1a)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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=<id> @name=<name>` comment so they round-trip.
|
||||
// DMS-managed rules live in dms/windowrules.conf (sourced from config.conf),
|
||||
// each preceded by an `# @id=<id> @name=<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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"'
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
@@ -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: "[email protected]", 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", () => {
|
||||
|
||||
@@ -12684,6 +12684,8 @@
|
||||
"tabIndex": 37,
|
||||
"category": "System & integrations",
|
||||
"keywords": [
|
||||
"gap_inner_horizontal",
|
||||
"gap_inner_vertical",
|
||||
"gappih",
|
||||
"gappiv",
|
||||
"gaps",
|
||||
@@ -12951,6 +12953,8 @@
|
||||
"category": "System & integrations",
|
||||
"keywords": [
|
||||
"edge",
|
||||
"gap_outer_horizontal",
|
||||
"gap_outer_vertical",
|
||||
"gappoh",
|
||||
"gappov",
|
||||
"gaps",
|
||||
@@ -12986,6 +12990,7 @@
|
||||
"category": "System & integrations",
|
||||
"keywords": [
|
||||
"border",
|
||||
"border_px",
|
||||
"borderpx",
|
||||
"integrations",
|
||||
"mango",
|
||||
|
||||
Reference in New Issue
Block a user