From e10102edc0d32230711eeddbd1f4647b0a756961 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 9 Oct 2026 05:55:47 -0400 Subject: [PATCH] feat(mango): native mouse/touchpad & keyboard input support - Hide blur toggle options until mango support is added --- core/cmd/dms/commands_config.go | 1 + core/cmd/dms/commands_setup.go | 10 +- core/internal/config/embedded/mango.conf | 1 + core/internal/config/mango_deploy.go | 1 + core/internal/config/mango_deploy_test.go | 2 + core/internal/mangoconf/dialect_test.go | 6 +- core/internal/mangoconf/migrate.go | 2 +- quickshell/Common/BlurStrength.js | 3 +- quickshell/Common/ConfigIncludeResolve.js | 3 +- quickshell/Common/SettingsData.qml | 1 - quickshell/Common/settings/SettingsSpec.js | 4 - quickshell/Modules/Settings/KeyboardTab.qml | 1 + .../Modules/Settings/MouseTouchpadTab.qml | 21 +--- .../Modules/Settings/ThemeSurfacesTab.qml | 13 +- quickshell/Services/BlurService.qml | 2 + quickshell/Services/CompositorService.qml | 4 +- quickshell/Services/MangoService.qml | 115 +++++++++++++++++- quickshell/Services/SettingsSearchService.qml | 1 + quickshell/tests/config-include.test.mjs | 40 +++++- .../translations/extract_settings_index.py | 4 + .../translations/settings_search_index.json | 55 ++------- 21 files changed, 199 insertions(+), 91 deletions(-) diff --git a/core/cmd/dms/commands_config.go b/core/cmd/dms/commands_config.go index f2bb155c5..6e8523974 100644 --- a/core/cmd/dms/commands_config.go +++ b/core/cmd/dms/commands_config.go @@ -44,6 +44,7 @@ var resolveIncludeCmd = &cobra.Command{ "binds.kdl", "input.kdl", "cursor.conf", + "input.conf", "layout.conf", "outputs.conf", "binds.conf", diff --git a/core/cmd/dms/commands_setup.go b/core/cmd/dms/commands_setup.go index 900c45a9d..c898a8629 100644 --- a/core/cmd/dms/commands_setup.go +++ b/core/cmd/dms/commands_setup.go @@ -145,10 +145,12 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{ niriContent: func(_ string) string { return config.NiriAlttabConfig }, }, "input": { - niriFile: "input.kdl", - hyprFile: "input.lua", - niriContent: func(_ string) string { return config.NiriInputConfig }, - hyprContent: func(_ string) string { return config.DMSInputLuaConfig }, + niriFile: "input.kdl", + hyprFile: "input.lua", + mangoFile: "input.conf", + niriContent: func(_ string) string { return config.NiriInputConfig }, + hyprContent: func(_ string) string { return config.DMSInputLuaConfig }, + mangoContent: func(_ string) string { return "" }, }, "outputs": { niriFile: "outputs.kdl", diff --git a/core/internal/config/embedded/mango.conf b/core/internal/config/embedded/mango.conf index fdc21e92a..d81977141 100644 --- a/core/internal/config/embedded/mango.conf +++ b/core/internal/config/embedded/mango.conf @@ -12,6 +12,7 @@ exec-once=dms run source=./dms/colors.conf source=./dms/layout.conf source=./dms/cursor.conf +source=./dms/input.conf source=./dms/outputs.conf source=./dms/windowrules.conf source=./dms/binds.conf diff --git a/core/internal/config/mango_deploy.go b/core/internal/config/mango_deploy.go index e236c82ae..b8c4b8c81 100644 --- a/core/internal/config/mango_deploy.go +++ b/core/internal/config/mango_deploy.go @@ -174,6 +174,7 @@ func (cd *ConfigDeployer) deployMangoDmsConfigs(dmsDir, terminalCommand string, {"layout.conf", MangoLayoutConfig}, {"outputs.conf", extractMangoLines(oldMain, mangoMonitorLine)}, {"cursor.conf", ""}, + {"input.conf", ""}, {"windowrules.conf", extractMangoLines(oldMain, mangoWindowLine)}, } for _, cfg := range configs { diff --git a/core/internal/config/mango_deploy_test.go b/core/internal/config/mango_deploy_test.go index 862058c7e..bc0add661 100644 --- a/core/internal/config/mango_deploy_test.go +++ b/core/internal/config/mango_deploy_test.go @@ -51,6 +51,8 @@ func TestMangoDeployMovesUserRulesAndKeepsBindsByDefault(t *testing.T) { 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"), "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.NotContains(t, binds, "{{TERMINAL_COMMAND}}") diff --git a/core/internal/mangoconf/dialect_test.go b/core/internal/mangoconf/dialect_test.go index b8b1d0b30..ec5758452 100644 --- a/core/internal/mangoconf/dialect_test.go +++ b/core/internal/mangoconf/dialect_test.go @@ -34,14 +34,18 @@ func TestMigrateLeavesUserConfigUnlessAsked(t *testing.T) { 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}, changed) + 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) diff --git a/core/internal/mangoconf/migrate.go b/core/internal/mangoconf/migrate.go index 4a3317b1d..c06e89fb8 100644 --- a/core/internal/mangoconf/migrate.go +++ b/core/internal/mangoconf/migrate.go @@ -10,7 +10,7 @@ import ( // 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", "outputs.conf", "windowrules.conf"} +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. diff --git a/quickshell/Common/BlurStrength.js b/quickshell/Common/BlurStrength.js index 6cc6f9dfa..90ab78170 100644 --- a/quickshell/Common/BlurStrength.js +++ b/quickshell/Common/BlurStrength.js @@ -1,7 +1,6 @@ .pragma library -// One shell-wide blur reach in logical px, solved into each compositor's own knobs. -// Hyprland's stock 8/1 reaches 16px and reads as plain transparency, so DMS defaults it near niri. +// One shell-wide blur reach in logical px, per-compositor var HYPRLAND_DEFAULT = 48; var NIRI_DEFAULT = 42; var AQUEOUS_DEFAULT = 40; diff --git a/quickshell/Common/ConfigIncludeResolve.js b/quickshell/Common/ConfigIncludeResolve.js index c3c1fc88b..f9baab9a4 100644 --- a/quickshell/Common/ConfigIncludeResolve.js +++ b/quickshell/Common/ConfigIncludeResolve.js @@ -105,7 +105,8 @@ const includeTable = { }, input: { niri: [kdlInclude("input")], - hyprland: [luaInclude("input")] + hyprland: [luaInclude("input")], + mango: [confInclude("input")] }, windowrules: { niri: [kdlInclude("windowrules")], diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index e499e97f3..5283ace81 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -245,7 +245,6 @@ Singleton { property int mangoLayoutGapsOutOverride: Spec.SPEC.mangoLayoutGapsOutOverride.def property int mangoLayoutRadiusOverride: Spec.SPEC.mangoLayoutRadiusOverride.def property int mangoLayoutBorderSize: Spec.SPEC.mangoLayoutBorderSize.def - property bool mangoTrackpadNaturalScrolling: Spec.SPEC.mangoTrackpadNaturalScrolling.def property string mouseAccelProfile: Spec.SPEC.mouseAccelProfile.def property real mouseAccelSpeed: Spec.SPEC.mouseAccelSpeed.def property bool mouseLeftHanded: Spec.SPEC.mouseLeftHanded.def diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 2e7a19d50..5419447c0 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -315,10 +315,6 @@ var LOCAL_SPEC = { def: -1, onChange: "updateCompositorLayout" }, - mangoTrackpadNaturalScrolling: { - def: true, - onChange: "updateCompositorCursor" - }, mouseAccelProfile: { def: "default", onChange: "updateCompositorInput" diff --git a/quickshell/Modules/Settings/KeyboardTab.qml b/quickshell/Modules/Settings/KeyboardTab.qml index 03ad563b1..fda6d7fbe 100644 --- a/quickshell/Modules/Settings/KeyboardTab.qml +++ b/quickshell/Modules/Settings/KeyboardTab.qml @@ -114,6 +114,7 @@ Item { } SettingsTextFieldRow { + visible: CompositorService.isNiri || CompositorService.isHyprland resetKeys: ["keyboardKeymapFile"] leftIconName: "description" text: I18n.tr("Keymap file") diff --git a/quickshell/Modules/Settings/MouseTouchpadTab.qml b/quickshell/Modules/Settings/MouseTouchpadTab.qml index 08af91caa..268c1d84f 100644 --- a/quickshell/Modules/Settings/MouseTouchpadTab.qml +++ b/quickshell/Modules/Settings/MouseTouchpadTab.qml @@ -113,7 +113,7 @@ Item { } SettingsToggleRow { - visible: CompositorService.isNiri + visible: CompositorService.isNiri || CompositorService.isMango tags: ["mouse", "middle", "click", "emulation"] settingKey: "mouseMiddleEmulation" text: I18n.tr("Middle click emulation") @@ -122,23 +122,6 @@ Item { } } - SettingsCard { - width: parent.width - visible: CompositorService.isMango - tags: ["touchpad", "trackpad", "natural", "scrolling", "invert"] - title: I18n.tr("Touchpad") - settingKey: "mangoTouchpadSettings" - iconName: "trackpad_input_2" - - SettingsToggleRow { - tags: ["touchpad", "trackpad", "natural", "scrolling", "invert"] - settingKey: "mangoTrackpadNaturalScrolling" - text: I18n.tr("Natural scrolling") - checked: SettingsData.mangoTrackpadNaturalScrolling - onToggled: checked => SettingsData.set("mangoTrackpadNaturalScrolling", checked) - } - } - SettingsCard { width: parent.width visible: CompositorService.supportsInputConfig @@ -270,7 +253,7 @@ Item { } SettingsToggleRow { - visible: CompositorService.isNiri + visible: CompositorService.isNiri || CompositorService.isMango tags: ["touchpad", "disable", "external", "mouse"] settingKey: "touchpadDisableOnExternalMouse" text: I18n.tr("Disable with external mouse") diff --git a/quickshell/Modules/Settings/ThemeSurfacesTab.qml b/quickshell/Modules/Settings/ThemeSurfacesTab.qml index 4bbf2b7ef..caaf2c4af 100644 --- a/quickshell/Modules/Settings/ThemeSurfacesTab.qml +++ b/quickshell/Modules/Settings/ThemeSurfacesTab.qml @@ -192,9 +192,8 @@ Column { settingKey: "blurEnabled" valueKeys: ["blurStrength"] text: I18n.tr("Background blur") - description: BlurService.available ? I18n.tr("Sets the compositor's blur for every window, not just the shell", "Blur strength slider description") : I18n.tr("Your compositor does not support background blur (ext-background-effect-v1)") + visible: BlurService.available checked: SettingsData.blurEnabled ?? false - enabled: BlurService.available value: BlurService.strength minimum: 16 maximum: 160 @@ -210,11 +209,19 @@ Column { onSliderValueChanged: newValue => SettingsData.set("blurStrength", newValue) } + SettingsNoteRow { + visible: BlurService.probed && !BlurService.available + noteIconName: "info" + tint: Theme.surfaceVariantText + tintBackground: Theme.surfaceVariantAlpha + text: I18n.tr("Your compositor does not support ext-background blur") + } + SettingsNavRow { tab: "theme" tags: ["blur", "xray", "compositor", "layout"] settingKey: "blurXrayLink" - visible: CompositorService.isNiri || CompositorService.isHyprland + visible: BlurService.available && (CompositorService.isNiri || CompositorService.isHyprland) title: I18n.tr("Xray options are in Compositor → Layout") onClicked: keyboard => root.parentModal?.navigateTo("compositor_layout", keyboard) } diff --git a/quickshell/Services/BlurService.qml b/quickshell/Services/BlurService.qml index 3c96fafec..41ee7581b 100644 --- a/quickshell/Services/BlurService.qml +++ b/quickshell/Services/BlurService.qml @@ -14,6 +14,7 @@ Singleton { property bool compositorSupported: false property bool _probed: false + property bool probed: false readonly property bool available: compositorSupported readonly property bool blurRequested: SettingsData.blurEnabled ?? false onBlurRequestedChanged: { @@ -64,6 +65,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { root.compositorSupported = text.trim() === "supported"; + root.probed = true; if (root.compositorSupported) log.info("Compositor supports ext-background-effect-v1"); else diff --git a/quickshell/Services/CompositorService.qml b/quickshell/Services/CompositorService.qml index 4acc23405..65ac79ebe 100644 --- a/quickshell/Services/CompositorService.qml +++ b/quickshell/Services/CompositorService.qml @@ -151,7 +151,7 @@ Singleton { readonly property bool supportsSmartDock: isNiri || isHyprland || isMango || isAqueous readonly property bool supportsNativeOverview: isNiri || isAqueous readonly property bool supportsPointerConfig: isNiri || isHyprland || isMango - readonly property bool supportsInputConfig: isNiri || isHyprland + readonly property bool supportsInputConfig: isNiri || isHyprland || isMango readonly property string displayName: { switch (compositor) { @@ -2184,6 +2184,8 @@ Singleton { NiriService.generateNiriInputConfig(); if (root.isHyprland && typeof HyprlandService !== "undefined") HyprlandService.generateInputConfig(); + if (root.isMango && typeof MangoService !== "undefined") + MangoService.generateInputConfig(); } function onCompositorCursorRefreshNeeded() { diff --git a/quickshell/Services/MangoService.qml b/quickshell/Services/MangoService.qml index 964801ac8..62a03de86 100644 --- a/quickshell/Services/MangoService.qml +++ b/quickshell/Services/MangoService.qml @@ -28,10 +28,14 @@ Singleton { readonly property string outputsPath: mangoDmsDir + "/outputs.conf" readonly property string layoutPath: mangoDmsDir + "/layout.conf" readonly property string cursorPath: mangoDmsDir + "/cursor.conf" + 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 property real _ignoreWatchedReloadUntil: 0 property string _lastMonitorsLine: "" @@ -83,6 +87,13 @@ Singleton { onFileChanged: root.handleWatchedConfigChanged() } + FileView { + id: mangoInputWatcher + path: CompositorService.isMango ? root.inputPath : "" + watchChanges: CompositorService.isMango + onFileChanged: root.handleWatchedConfigChanged() + } + FileView { id: mangoOutputsWatcher path: CompositorService.isMango ? root.outputsPath : "" @@ -453,6 +464,11 @@ Singleton { onTriggered: root.reloadConfig(false, false) } + DeferredAction { + id: inputGenerationAction + onTriggered: root.doGenerateInputConfig() + } + function handleWatchedConfigChanged() { if (!CompositorService.isMango || !root.available) return; @@ -582,7 +598,9 @@ Singleton { 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; generateLayoutConfig(); + generateInputConfig(); }); } @@ -673,10 +691,8 @@ gappov=${gapsOut} const themeName = settings.theme === "System Default" ? (SettingsData.systemDefaultCursorTheme || "") : settings.theme; const size = settings.size || 24; const hideTimeout = settings.mango?.cursorHideTimeout || 0; - const naturalScrolling = SettingsData.mangoTrackpadNaturalScrolling ? 1 : 0; let content = `# Auto-generated by DMS - do not edit manually -trackpad_natural_scrolling=${naturalScrolling} cursor_size=${size}`; if (themeName) @@ -697,4 +713,99 @@ cursor_size=${size}`; reloadConfig(false); }); } + + function generateInputConfig() { + if (!CompositorService.isMango) + return; + inputGenerationPending = true; + inputGenerationAction.schedule(); + } + + function doGenerateInputConfig() { + if (inputGenerationRunning) + return; + 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"); + // 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); + Proc.runCommand("mango-write-input", ["sh", "-c", script], (output, exitCode) => { + inputGenerationRunning = false; + if (exitCode === 3) { + if (inputGenerationPending) + inputGenerationAction.schedule(); + return; + } + if (exitCode !== 0) { + log.warn("Failed to write input config:", output); + } else { + log.info("Generated input config at", inputPath); + reloadConfig(false); + } + if (inputGenerationPending) + inputGenerationAction.schedule(); + }); + } + + // Mouse and trackpad keys are the split names Mango has had since 0.16.1; libinput enums are numeric + function mangoInputLines(s, snake) { + const flag = value => value ? 1 : 0; + const profile = name => name === "flat" ? 1 : name === "adaptive" ? 2 : -1; + const scrollMethod = name => ({ + "no-scroll": 0, + "two-finger": 1, + "edge": 2, + "on-button-down": 4 + })[name] ?? -1; + const clickMethod = name => name === "button-areas" ? 1 : name === "clickfinger" ? 2 : -1; + const lines = []; + // -1 is a "default" choice: the key stays out so Mango's own default applies + const add = (key, value) => { + if (value !== -1 && value !== "") + lines.push(`${key}=${value}`); + }; + add("mouse_accel_speed", s.mouseAccelSpeed.toFixed(2)); + add("mouse_accel_profile", profile(s.mouseAccelProfile)); + add("mouse_natural_scrolling", flag(s.mouseNaturalScroll)); + add("mouse_left_handed", flag(s.mouseLeftHanded)); + add("mouse_middle_button_emulation", flag(s.mouseMiddleEmulation)); + add("axis_scroll_factor", s.mouseScrollFactor.toFixed(2)); + add("mouse_scroll_method", scrollMethod(s.mouseScrollMethod)); + add("tap_to_click", flag(s.touchpadTapToClick)); + add("tap_and_drag", flag(s.touchpadTapAndDrag)); + add("drag_lock", flag(s.touchpadDragLock)); + add("trackpad_accel_speed", s.touchpadAccelSpeed.toFixed(2)); + add("trackpad_accel_profile", profile(s.touchpadAccelProfile)); + add("trackpad_natural_scrolling", flag(s.touchpadNaturalScroll)); + add("trackpad_scroll_factor", s.touchpadScrollFactor.toFixed(2)); + add("trackpad_scroll_method", scrollMethod(s.touchpadScrollMethod)); + add("trackpad_click_method", clickMethod(s.touchpadClickMethod)); + add("trackpad_disable_while_typing", flag(s.touchpadDisableWhileTyping)); + add("trackpad_middle_button_emulation", flag(s.touchpadMiddleEmulation)); + add("trackpad_send_events_mode", s.touchpadDisableOnExternalMouse ? 2 : 0); + + // Unconfigured keyboard keeps the user's own xkb_rules_* keys and the XKB defaults + const keyboardConfigured = s.keyboardLayouts || s.keyboardVariants || s.keyboardModel || s.keyboardOptions || s.keyboardRepeatDelay > 0 || s.keyboardRepeatRate > 0 || s.keyboardNumlock; + if (!keyboardConfigured) + return lines; + add("xkb_rules_layout", s.keyboardLayouts); + add("xkb_rules_variant", s.keyboardVariants); + add("xkb_rules_model", s.keyboardModel); + add("xkb_rules_options", s.keyboardOptions); + if (s.keyboardRepeatDelay > 0) + add("repeat_delay", s.keyboardRepeatDelay); + if (s.keyboardRepeatRate > 0) + add("repeat_rate", s.keyboardRepeatRate); + add(snake ? "numlock_on" : "numlockon", flag(s.keyboardNumlock)); + return lines; + } } diff --git a/quickshell/Services/SettingsSearchService.qml b/quickshell/Services/SettingsSearchService.qml index 26b7fd2d3..8cb4c6160 100644 --- a/quickshell/Services/SettingsSearchService.qml +++ b/quickshell/Services/SettingsSearchService.qml @@ -68,6 +68,7 @@ Singleton { "smartDockCapable": () => CompositorService.supportsSmartDock, "workspaceFollowFocusCapable": () => CompositorService.supportsWorkspaceFollowFocus, "isHyprlandOrNiri": () => CompositorService.isHyprland || CompositorService.isNiri, + "isNiriOrMango": () => CompositorService.isNiri || CompositorService.isMango, "windowRulesCapable": () => CompositorService.supportsWindowRules, "layoutCapable": () => CompositorService.supportsLayoutConfig, "keybindsAvailable": () => KeybindsService.available, diff --git a/quickshell/tests/config-include.test.mjs b/quickshell/tests/config-include.test.mjs index cb5d5f34b..1330b59ac 100644 --- a/quickshell/tests/config-include.test.mjs +++ b/quickshell/tests/config-include.test.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { execFileSync } from "node:child_process"; +import { loadScript } from "./qml-script.mjs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import vm from "node:vm"; @@ -11,7 +12,7 @@ vm.runInContext(readFileSync(new URL("../Common/ConfigIncludeResolve.js", import const plain = value => JSON.parse(JSON.stringify(value)); test("includeSpec returns null off the table", () => { - assert.equal(resolve.includeSpec("input", "mango"), null, "input exists for niri and hyprland only"); + assert.equal(resolve.includeSpec("colors", "mango"), null, "colors is deployed by setup, never repaired"); assert.equal(resolve.includeSpec("colors", "niri"), null, "unknown kind"); assert.equal(resolve.includeSpec("outputs", "sway"), null, "unknown compositor"); assert.equal(resolve.includeSpec("outputs", ""), null, "empty compositor"); @@ -34,7 +35,8 @@ test("includePaths joins the compositor directory, with hypr for hyprland", () = assert.deepEqual(plain(mango.fragmentFiles), ["/home/u/.config/mango/dms/cursor.conf"]); const binds = resolve.includePaths("binds", "hyprland", "/home/u/.config"); assert.deepEqual(plain(binds.fragmentFiles), ["/home/u/.config/hypr/dms/binds.lua", "/home/u/.config/hypr/dms/binds-user.lua"]); - assert.equal(resolve.includePaths("input", "mango", "/home/u/.config"), null, "no spec, no paths"); + assert.deepEqual(plain(resolve.includePaths("input", "mango", "/home/u/.config").fragmentFiles), ["/home/u/.config/mango/dms/input.conf"]); + assert.equal(resolve.includePaths("colors", "mango", "/home/u/.config"), null, "no spec, no paths"); }); test("resolveIncludeArgs spells mango as mangowc and names the fragment", () => { @@ -42,11 +44,12 @@ test("resolveIncludeArgs spells mango as mangowc and names the fragment", () => assert.deepEqual(plain(resolve.resolveIncludeArgs("layout", "hyprland")), ["hyprland", "layout.lua"]); assert.deepEqual(plain(resolve.resolveIncludeArgs("input", "hyprland")), ["hyprland", "input.lua"]); assert.deepEqual(plain(resolve.resolveIncludeArgs("windowrules", "mango")), ["mangowc", "windowrules.conf"]); - assert.equal(resolve.resolveIncludeArgs("input", "mango"), null, "no spec, no args"); + assert.deepEqual(plain(resolve.resolveIncludeArgs("input", "mango")), ["mangowc", "input.conf"]); + assert.equal(resolve.resolveIncludeArgs("colors", "mango"), null, "no spec, no args"); }); test("repairScriptFor returns an empty script off the table", () => { - assert.equal(resolve.repairScriptFor("input", "mango", "/home/u/.config", "/x"), ""); + assert.equal(resolve.repairScriptFor("colors", "mango", "/home/u/.config", "/x"), ""); assert.equal(resolve.repairScriptFor("nope", "niri", "/home/u/.config", "/x"), ""); }); @@ -79,3 +82,32 @@ test("repair preserves config and fragments, backs up, and adds live includes on assert.equal(readFileSync(paths.configFile, "utf8"), repaired); } }); + +test("mango input fragment maps choices onto libinput numbers and spells numlock per dialect", () => { + 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); + assert.ok(found, "mangoInputLines not found in MangoService.qml"); + const scope = vm.createContext({}); + vm.runInContext(found[0], scope); + const defaults = Object.fromEntries(Object.keys(SPEC).filter(key => /^(mouse|touchpad|keyboard)[A-Z]/.test(key)).map(key => [key, SPEC[key].def])); + const lines = (overrides, snake = false) => [...scope.mangoInputLines(Object.assign({}, defaults, overrides), snake)]; + const value = (list, key) => list.find(line => line.startsWith(key + "="))?.slice(key.length + 1); + + const stock = lines({}); + assert.equal(value(stock, "trackpad_natural_scrolling"), "1"); + assert.equal(value(stock, "mouse_accel_profile"), undefined, "default profile leaves the key to Mango"); + assert.ok(!stock.some(line => /^(xkb_rules_|repeat_|numlock)/.test(line)), "unconfigured keyboard writes nothing"); + + const chosen = lines({ mouseScrollMethod: "no-scroll", touchpadAccelProfile: "adaptive", touchpadScrollMethod: "on-button-down", touchpadClickMethod: "clickfinger", touchpadDisableOnExternalMouse: true, keyboardLayouts: "us,de", keyboardRepeatDelay: 300 }); + assert.equal(value(chosen, "mouse_scroll_method"), "0"); + assert.equal(value(chosen, "trackpad_accel_profile"), "2"); + assert.equal(value(chosen, "trackpad_scroll_method"), "4"); + assert.equal(value(chosen, "trackpad_click_method"), "2"); + assert.equal(value(chosen, "trackpad_send_events_mode"), "2"); + 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"); +}); diff --git a/quickshell/translations/extract_settings_index.py b/quickshell/translations/extract_settings_index.py index 68c4e6fd9..769d5f878 100644 --- a/quickshell/translations/extract_settings_index.py +++ b/quickshell/translations/extract_settings_index.py @@ -487,6 +487,10 @@ def find_settings_components(content, filename, wrappers, tab_meta, hub_meta): condition_key = "isAqueous" elif all(c in visible_raw for c in ("CompositorService.isNiri", "CompositorService.isHyprland", "CompositorService.isMango")): condition_key = "windowRulesCapable" + elif all(c in visible_raw for c in ("CompositorService.isNiri", "CompositorService.isHyprland")): + condition_key = "isHyprlandOrNiri" + elif all(c in visible_raw for c in ("CompositorService.isNiri", "CompositorService.isMango")): + condition_key = "isNiriOrMango" elif "CompositorService.isNiri" in visible_raw: condition_key = "isNiri" elif "CompositorService.isHyprland" in visible_raw: diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index e237e87ef..34b7d7ebe 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -251,24 +251,18 @@ "keywords": [ "background", "blur", - "every", "frosted", "glass", "interface", - "just", "passes", "radius", "reach", - "sets", - "shell", "size", "strength", "style", - "transparency", - "window" + "transparency" ], - "page": "theme_surfaces", - "description": "Sets the compositor's blur for every window, not just the shell" + "page": "theme_surfaces" }, { "section": "wallpaperBackgroundColorMode", @@ -1229,7 +1223,7 @@ "wallpaper" ], "page": "personalization", - "conditionKey": "isNiri" + "conditionKey": "isHyprlandOrNiri" }, { "section": "notificationHistory", @@ -2704,7 +2698,7 @@ "xray" ], "page": "theme_surfaces", - "conditionKey": "isNiri" + "conditionKey": "isHyprlandOrNiri" }, { "section": "_hub_personalization", @@ -13040,7 +13034,7 @@ "toast" ], "icon": "notifications", - "conditionKey": "isNiri" + "conditionKey": "isNiriOrMango" }, { "section": "hyprlandLayoutGapsOutOverride", @@ -14319,7 +14313,7 @@ "touchpad" ], "parentLabel": "Input", - "conditionKey": "isNiri" + "conditionKey": "isNiriOrMango" }, { "section": "touchpadDragLock", @@ -14367,7 +14361,7 @@ "touchpad" ], "parentLabel": "Input", - "conditionKey": "isNiri" + "conditionKey": "isNiriOrMango" }, { "section": "touchpadMiddleEmulation", @@ -14420,23 +14414,6 @@ "description": "Speed, scrolling, tap to click", "conditionKey": "pointerCapable" }, - { - "section": "mangoTrackpadNaturalScrolling", - "label": "Natural scrolling", - "tabIndex": 44, - "category": "Mouse & touchpad", - "keywords": [ - "input", - "invert", - "mouse", - "natural", - "scrolling", - "touchpad", - "trackpad" - ], - "parentLabel": "Input", - "conditionKey": "pointerCapable" - }, { "section": "mouseNaturalScroll", "label": "Natural scrolling", @@ -14597,24 +14574,6 @@ "parentLabel": "Input", "conditionKey": "pointerCapable" }, - { - "section": "mangoTouchpadSettings", - "label": "Touchpad", - "tabIndex": 44, - "category": "Mouse & touchpad", - "keywords": [ - "input", - "invert", - "mouse", - "natural", - "scrolling", - "touchpad", - "trackpad" - ], - "parentLabel": "Input", - "icon": "trackpad_input_2", - "conditionKey": "isMango" - }, { "section": "touchpadSettings", "label": "Touchpad",