From dade416ca0e846275e007b27a9bdaa6e40ca18b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hu=E1=BB=B3nh=20Thi=E1=BB=87n=20L=E1=BB=99c?= Date: Fri, 7 Aug 2026 05:40:17 +0700 Subject: [PATCH] feat(settings): add keyboard configuration tab for Niri (#2937) * feat(settings): add dedicated keyboard configuration tab * chore(settings): configure default keyboard settings and clean up embedded niri config * settings/keyboard: emit niri keyboard config only when set, dedup setup banner, index tab --------- Co-authored-by: bbedward --- quickshell/Common/SettingsData.qml | 10 + quickshell/Common/settings/SettingsSpec.js | 10 + .../Modals/Settings/SettingsContent.qml | 15 + .../Modals/Settings/SettingsSidebar.qml | 7 + quickshell/Modules/Settings/KeyboardTab.qml | 267 ++++++++ .../Modules/Settings/MouseTouchpadTab.qml | 192 +----- .../Settings/Widgets/NiriInputSetupBanner.qml | 156 +++++ quickshell/Services/NiriService.qml | 51 ++ .../translations/extract_settings_index.py | 6 + .../translations/settings_search_index.json | 596 +++++++++++++++++- 10 files changed, 1145 insertions(+), 165 deletions(-) create mode 100644 quickshell/Modules/Settings/KeyboardTab.qml create mode 100644 quickshell/Modules/Settings/Widgets/NiriInputSetupBanner.qml diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 4c57a8ae8..9960614b8 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -203,6 +203,16 @@ Singleton { property bool touchpadTapAndDrag: true property bool touchpadTapToClick: true + property string keyboardLayouts: "" + property string keyboardVariants: "" + property string keyboardModel: "" + property string keyboardOptions: "" + property string keyboardKeymapFile: "" + property string keyboardTrackLayout: "" + property int keyboardRepeatDelay: 0 + property int keyboardRepeatRate: 0 + property bool keyboardNumlock: false + property int firstDayOfWeek: -1 property bool showWeekNumber: false property string calendarBackend: "auto" diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 3f6128279..5003a2da0 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -57,6 +57,16 @@ var SPEC = { touchpadTapAndDrag: { def: true, onChange: "updateCompositorInput" }, touchpadTapToClick: { def: true, onChange: "updateCompositorInput" }, + keyboardLayouts: { def: "", onChange: "updateCompositorInput" }, + keyboardVariants: { def: "", onChange: "updateCompositorInput" }, + keyboardModel: { def: "", onChange: "updateCompositorInput" }, + keyboardOptions: { def: "", onChange: "updateCompositorInput" }, + keyboardKeymapFile: { def: "", onChange: "updateCompositorInput" }, + keyboardTrackLayout: { def: "", onChange: "updateCompositorInput" }, + keyboardRepeatDelay: { def: 0, onChange: "updateCompositorInput" }, + keyboardRepeatRate: { def: 0, onChange: "updateCompositorInput" }, + keyboardNumlock: { def: false, onChange: "updateCompositorInput" }, + firstDayOfWeek: { def: -1 }, showWeekNumber: { def: false }, calendarBackend: { def: "auto" }, diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index aaab9e19f..af2e8f6e5 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -733,5 +733,20 @@ FocusScope { Qt.callLater(() => item.forceActiveFocus()); } } + + Loader { + id: keyboardLoader + anchors.fill: parent + active: root.currentIndex === 45 + visible: active + focus: active + + sourceComponent: KeyboardTab {} + + onActiveChanged: { + if (active && item) + Qt.callLater(() => item.forceActiveFocus()); + } + } } } diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index 29692d1f6..6d0feb1ca 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -326,6 +326,13 @@ Rectangle { "tabIndex": 44, "niriOnly": true }, + { + "id": "keyboard", + "text": I18n.tr("Keyboard"), + "icon": "keyboard", + "tabIndex": 45, + "niriOnly": true + }, { "id": "locale", "text": I18n.tr("Locale"), diff --git a/quickshell/Modules/Settings/KeyboardTab.qml b/quickshell/Modules/Settings/KeyboardTab.qml new file mode 100644 index 000000000..63324428c --- /dev/null +++ b/quickshell/Modules/Settings/KeyboardTab.qml @@ -0,0 +1,267 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Settings.Widgets + +Item { + id: root + + LayoutMirroring.enabled: I18n.isRtl + LayoutMirroring.childrenInherit: true + + DankFlickable { + anchors.fill: parent + clip: true + contentHeight: settingsColumn.height + Theme.spacingXL + contentWidth: width + + Column { + id: settingsColumn + + topPadding: 4 + width: Math.min(550, parent.width - Theme.spacingL * 2) + anchors.horizontalCenter: parent.horizontalCenter + spacing: Theme.spacingXL + + NiriInputSetupBanner { + width: parent.width + } + + SettingsCard { + width: parent.width + tags: ["keyboard", "layout", "language", "input", "xkb"] + title: I18n.tr("Keyboard Layouts") + settingKey: "keyboardLayoutsSettings" + iconName: "keyboard" + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + + StyledText { + width: parent.width + text: I18n.tr("Keyboard Layouts") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + width: parent.width + text: I18n.tr("Comma-separated list of layout names. Leave empty to use the system keyboard settings.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + DankTextField { + width: parent.width + text: SettingsData.keyboardLayouts + placeholderText: "us,de" + onTextEdited: SettingsData.set("keyboardLayouts", text) + } + } + + SettingsDropdownRow { + tags: ["keyboard", "layout", "switch", "shortcut", "xkb"] + settingKey: "keyboardOptions" + text: I18n.tr("Switch Layout Shortcut") + description: I18n.tr("Choose a shortcut key to cycle between keyboard layouts") + options: [I18n.tr("Alt + Shift"), I18n.tr("Ctrl + Shift"), I18n.tr("Caps Lock"), I18n.tr("Super + Space"), I18n.tr("Custom / None")] + currentValue: { + const opt = SettingsData.keyboardOptions; + if (opt.includes("grp:alt_shift_toggle")) + return options[0]; + if (opt.includes("grp:ctrl_shift_toggle")) + return options[1]; + if (opt.includes("grp:caps_toggle")) + return options[2]; + if (opt.includes("grp:win_space_toggle")) + return options[3]; + return options[4]; + } + onValueChanged: value => { + const idx = options.indexOf(value); + let opt = SettingsData.keyboardOptions.split(",").filter(o => !o.startsWith("grp:")).join(","); + + let newGrp = ""; + switch (idx) { + case 0: + newGrp = "grp:alt_shift_toggle"; + break; + case 1: + newGrp = "grp:ctrl_shift_toggle"; + break; + case 2: + newGrp = "grp:caps_toggle"; + break; + case 3: + newGrp = "grp:win_space_toggle"; + break; + } + + if (newGrp) + opt = opt ? opt + "," + newGrp : newGrp; + SettingsData.set("keyboardOptions", opt); + } + } + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + + StyledText { + width: parent.width + text: I18n.tr("XKB Options") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + width: parent.width + text: I18n.tr("Advanced comma-separated libxkbcommon options.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + DankTextField { + width: parent.width + text: SettingsData.keyboardOptions + placeholderText: "compose:ralt,ctrl:nocaps" + onTextEdited: SettingsData.set("keyboardOptions", text) + } + } + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + + StyledText { + width: parent.width + text: I18n.tr("Keyboard Variant") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + DankTextField { + width: parent.width + text: SettingsData.keyboardVariants + placeholderText: "colemak" + onTextEdited: SettingsData.set("keyboardVariants", text) + } + } + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + + StyledText { + width: parent.width + text: I18n.tr("Keyboard Model") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + DankTextField { + width: parent.width + text: SettingsData.keyboardModel + placeholderText: "pc104" + onTextEdited: SettingsData.set("keyboardModel", text) + } + } + + Column { + width: parent?.width ?? 0 + spacing: Theme.spacingS + + StyledText { + width: parent.width + text: I18n.tr("Keymap File Path") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + } + + StyledText { + width: parent.width + text: I18n.tr("Direct path to a .xkb keymap file. Overrides layouts/variants above.") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } + + DankTextField { + width: parent.width + text: SettingsData.keyboardKeymapFile + placeholderText: "~/.config/keymap.xkb" + onTextEdited: SettingsData.set("keyboardKeymapFile", text) + } + } + } + + SettingsCard { + width: parent.width + tags: ["keyboard", "repeat", "delay", "rate", "numlock", "behavior"] + title: I18n.tr("Keyboard Behavior") + settingKey: "keyboardBehaviorSettings" + iconName: "settings" + + SettingsButtonGroupRow { + tags: ["keyboard", "track", "layout"] + settingKey: "keyboardTrackLayout" + text: I18n.tr("Remember Layout") + description: I18n.tr("How layout changes are remembered across apps") + model: [I18n.tr("Globally"), I18n.tr("Per Window")] + currentIndex: SettingsData.keyboardTrackLayout === "window" ? 1 : 0 + onSelectionChanged: (index, selected) => { + if (!selected) + return; + SettingsData.set("keyboardTrackLayout", index === 1 ? "window" : "global"); + } + } + + SettingsToggleRow { + tags: ["keyboard", "numlock", "startup"] + settingKey: "keyboardNumlock" + text: I18n.tr("Enable Num Lock") + description: I18n.tr("Automatically turn on Num Lock at startup") + checked: SettingsData.keyboardNumlock + onToggled: checked => SettingsData.set("keyboardNumlock", checked) + } + + SettingsSliderRow { + tags: ["keyboard", "repeat", "delay", "speed"] + settingKey: "keyboardRepeatDelay" + text: I18n.tr("Repeat Delay") + description: I18n.tr("Delay before characters start repeating") + value: SettingsData.keyboardRepeatDelay || 600 + minimum: 100 + maximum: 2000 + step: 50 + defaultValue: 600 + unit: "ms" + onSliderValueChanged: newValue => SettingsData.set("keyboardRepeatDelay", newValue) + } + + SettingsSliderRow { + tags: ["keyboard", "repeat", "rate", "speed"] + settingKey: "keyboardRepeatRate" + text: I18n.tr("Repeat Rate") + description: I18n.tr("Characters per second while holding down key") + value: SettingsData.keyboardRepeatRate || 25 + minimum: 1 + maximum: 100 + step: 1 + defaultValue: 25 + unit: "" + onSliderValueChanged: newValue => SettingsData.set("keyboardRepeatRate", newValue) + } + } + } + } +} diff --git a/quickshell/Modules/Settings/MouseTouchpadTab.qml b/quickshell/Modules/Settings/MouseTouchpadTab.qml index de1075990..d0fb8a1f7 100644 --- a/quickshell/Modules/Settings/MouseTouchpadTab.qml +++ b/quickshell/Modules/Settings/MouseTouchpadTab.qml @@ -1,10 +1,8 @@ -import QtCore import QtQuick import qs.Common import qs.Services import qs.Widgets import qs.Modules.Settings.Widgets -import "../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve Item { id: root @@ -12,93 +10,6 @@ Item { LayoutMirroring.enabled: I18n.isRtl LayoutMirroring.childrenInherit: true - property var inputIncludeStatus: ({ - "exists": false, - "included": false, - "configFormat": "", - "readOnly": false - }) - property bool checkingInclude: false - property bool fixingInclude: false - - function getInputConfigPaths() { - const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); - if (CompositorService.compositor !== "niri") - return null; - return { - "configFile": configDir + "/niri/config.kdl", - "layoutFile": configDir + "/niri/dms/input.kdl", - "grepPattern": 'include.*"dms/input.kdl"', - "includeLine": 'include "dms/input.kdl"' - }; - } - - function checkInputIncludeStatus() { - if (CompositorService.compositor !== "niri") { - inputIncludeStatus = { - "exists": false, - "included": false, - "configFormat": "", - "readOnly": false - }; - return; - } - - checkingInclude = true; - Proc.runCommand("check-input-include", [Proc.dmsBin, "config", "resolve-include", "niri", "input.kdl"], (output, exitCode) => { - checkingInclude = false; - if (exitCode !== 0) { - inputIncludeStatus = { - "exists": false, - "included": false, - "configFormat": "", - "readOnly": false - }; - return; - } - try { - inputIncludeStatus = JSON.parse(output.trim()); - } catch (e) { - inputIncludeStatus = { - "exists": false, - "included": false, - "configFormat": "", - "readOnly": false - }; - } - }); - } - - function fixInputInclude() { - const paths = getInputConfigPaths(); - if (!paths) - return; - - fixingInclude = true; - const unixTime = Math.floor(Date.now() / 1000); - const backupFile = paths.configFile + ".backup" + unixTime; - const script = ConfigIncludeResolve.buildRepairScript({ - configFile: paths.configFile, - backupFile: backupFile, - fragmentFile: paths.layoutFile, - grepPattern: paths.grepPattern, - includeLine: paths.includeLine - }); - Proc.runCommand("fix-input-include", ["sh", "-c", script], (output, exitCode) => { - fixingInclude = false; - if (exitCode !== 0) - return; - checkInputIncludeStatus(); - SettingsData.updateCompositorInput(); - }); - } - - Component.onCompleted: { - if (CompositorService.isNiri) { - checkInputIncludeStatus(); - } - } - DankFlickable { anchors.fill: parent clip: true @@ -113,67 +24,8 @@ Item { anchors.horizontalCenter: parent.horizontalCenter spacing: Theme.spacingXL - StyledRect { - id: warningBox + NiriInputSetupBanner { width: parent.width - height: warningContent.implicitHeight + Theme.spacingL * 2 - radius: Theme.cornerRadius - - readonly property bool showSetup: !root.inputIncludeStatus.included - - color: showSetup ? Theme.withAlpha(Theme.primary, 0.15) : Theme.withAlpha(Theme.primary, 0) - border.color: showSetup ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.primary, 0) - border.width: 1 - visible: showSetup && !root.checkingInclude && CompositorService.isNiri - - Row { - id: warningContent - anchors.fill: parent - anchors.margins: Theme.spacingL - spacing: Theme.spacingM - - DankIcon { - name: "warning" - size: Theme.iconSize - color: Theme.primary - anchors.verticalCenter: parent.verticalCenter - } - - Column { - width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM - spacing: Theme.spacingXS - anchors.verticalCenter: parent.verticalCenter - - StyledText { - text: I18n.tr("First Time Setup") - font.pixelSize: Theme.fontSizeMedium - font.weight: Font.Medium - color: Theme.primary - width: parent.width - horizontalAlignment: Text.AlignLeft - } - - StyledText { - text: I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg("dms/input") - font.pixelSize: Theme.fontSizeSmall - color: Theme.surfaceVariantText - wrapMode: Text.WordWrap - width: parent.width - horizontalAlignment: Text.AlignLeft - } - } - - DankButton { - id: fixButton - visible: warningBox.showSetup - text: root.fixingInclude ? I18n.tr("Setting up...") : I18n.tr("Setup") - backgroundColor: Theme.primary - textColor: Theme.primaryText - enabled: !root.fixingInclude - anchors.verticalCenter: parent.verticalCenter - onClicked: root.fixInputInclude() - } - } } SettingsCard { @@ -212,12 +64,15 @@ Item { description: I18n.tr("Flat uses constant speed; Adaptive scales with movement speed") model: [I18n.tr("Default"), I18n.tr("Flat"), I18n.tr("Adaptive")] currentIndex: { - if (SettingsData.mouseAccelProfile === "flat") return 1; - if (SettingsData.mouseAccelProfile === "adaptive") return 2; + if (SettingsData.mouseAccelProfile === "flat") + return 1; + if (SettingsData.mouseAccelProfile === "adaptive") + return 2; return 0; } onSelectionChanged: (index, selected) => { - if (!selected) return; + if (!selected) + return; const profiles = ["default", "flat", "adaptive"]; SettingsData.set("mouseAccelProfile", profiles[index]); } @@ -252,12 +107,15 @@ Item { description: I18n.tr("Choose when to generate scrolling events") model: [I18n.tr("Default"), I18n.tr("No Scroll"), I18n.tr("On Button Down")] currentIndex: { - if (SettingsData.mouseScrollMethod === "no-scroll") return 1; - if (SettingsData.mouseScrollMethod === "on-button-down") return 2; + if (SettingsData.mouseScrollMethod === "no-scroll") + return 1; + if (SettingsData.mouseScrollMethod === "on-button-down") + return 2; return 0; } onSelectionChanged: (index, selected) => { - if (!selected) return; + if (!selected) + return; const methods = ["default", "no-scroll", "on-button-down"]; SettingsData.set("mouseScrollMethod", methods[index]); } @@ -327,12 +185,15 @@ Item { description: I18n.tr("Flat uses constant speed; Adaptive scales with movement speed") model: [I18n.tr("Default"), I18n.tr("Flat"), I18n.tr("Adaptive")] currentIndex: { - if (SettingsData.touchpadAccelProfile === "flat") return 1; - if (SettingsData.touchpadAccelProfile === "adaptive") return 2; + if (SettingsData.touchpadAccelProfile === "flat") + return 1; + if (SettingsData.touchpadAccelProfile === "adaptive") + return 2; return 0; } onSelectionChanged: (index, selected) => { - if (!selected) return; + if (!selected) + return; const profiles = ["default", "flat", "adaptive"]; SettingsData.set("touchpadAccelProfile", profiles[index]); } @@ -367,14 +228,19 @@ Item { description: I18n.tr("Choose when to generate scrolling events") model: [I18n.tr("Default"), I18n.tr("Two Finger"), I18n.tr("Edge"), I18n.tr("No Scroll"), I18n.tr("On Button Down")] currentIndex: { - if (SettingsData.touchpadScrollMethod === "two-finger") return 1; - if (SettingsData.touchpadScrollMethod === "edge") return 2; - if (SettingsData.touchpadScrollMethod === "no-scroll") return 3; - if (SettingsData.touchpadScrollMethod === "on-button-down") return 4; + if (SettingsData.touchpadScrollMethod === "two-finger") + return 1; + if (SettingsData.touchpadScrollMethod === "edge") + return 2; + if (SettingsData.touchpadScrollMethod === "no-scroll") + return 3; + if (SettingsData.touchpadScrollMethod === "on-button-down") + return 4; return 0; } onSelectionChanged: (index, selected) => { - if (!selected) return; + if (!selected) + return; const methods = ["default", "two-finger", "edge", "no-scroll", "on-button-down"]; SettingsData.set("touchpadScrollMethod", methods[index]); } diff --git a/quickshell/Modules/Settings/Widgets/NiriInputSetupBanner.qml b/quickshell/Modules/Settings/Widgets/NiriInputSetupBanner.qml new file mode 100644 index 000000000..e0a165393 --- /dev/null +++ b/quickshell/Modules/Settings/Widgets/NiriInputSetupBanner.qml @@ -0,0 +1,156 @@ +import QtCore +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets +import "../../../Common/ConfigIncludeResolve.js" as ConfigIncludeResolve + +StyledRect { + id: root + + property var includeStatus: ({ + "exists": false, + "included": false, + "configFormat": "", + "readOnly": false + }) + property bool checking: false + property bool fixing: false + + readonly property bool showSetup: !includeStatus.included + + function getInputConfigPaths() { + if (CompositorService.compositor !== "niri") + return null; + + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + return { + "configFile": configDir + "/niri/config.kdl", + "layoutFile": configDir + "/niri/dms/input.kdl", + "grepPattern": 'include.*"dms/input.kdl"', + "includeLine": 'include "dms/input.kdl"' + }; + } + + function checkIncludeStatus() { + if (CompositorService.compositor !== "niri") { + includeStatus = { + "exists": false, + "included": false, + "configFormat": "", + "readOnly": false + }; + return; + } + + checking = true; + Proc.runCommand("check-input-include", [Proc.dmsBin, "config", "resolve-include", "niri", "input.kdl"], (output, exitCode) => { + checking = false; + if (exitCode !== 0) { + includeStatus = { + "exists": false, + "included": false, + "configFormat": "", + "readOnly": false + }; + return; + } + try { + includeStatus = JSON.parse(output.trim()); + } catch (e) { + includeStatus = { + "exists": false, + "included": false, + "configFormat": "", + "readOnly": false + }; + } + }); + } + + function fixInclude() { + const paths = getInputConfigPaths(); + if (!paths) + return; + + fixing = true; + const unixTime = Math.floor(Date.now() / 1000); + const backupFile = paths.configFile + ".backup" + unixTime; + const script = ConfigIncludeResolve.buildRepairScript({ + configFile: paths.configFile, + backupFile: backupFile, + fragmentFile: paths.layoutFile, + grepPattern: paths.grepPattern, + includeLine: paths.includeLine + }); + Proc.runCommand("fix-input-include", ["sh", "-c", script], (output, exitCode) => { + fixing = false; + if (exitCode !== 0) + return; + checkIncludeStatus(); + SettingsData.updateCompositorInput(); + }); + } + + Component.onCompleted: { + if (CompositorService.isNiri) { + checkIncludeStatus(); + } + } + + height: warningContent.implicitHeight + Theme.spacingL * 2 + radius: Theme.cornerRadius + color: showSetup ? Theme.withAlpha(Theme.primary, 0.15) : Theme.withAlpha(Theme.primary, 0) + border.color: showSetup ? Theme.withAlpha(Theme.primary, 0.3) : Theme.withAlpha(Theme.primary, 0) + border.width: 1 + visible: showSetup && !checking && CompositorService.isNiri + + Row { + id: warningContent + anchors.fill: parent + anchors.margins: Theme.spacingL + spacing: Theme.spacingM + + DankIcon { + name: "warning" + size: Theme.iconSize + color: Theme.primary + anchors.verticalCenter: parent.verticalCenter + } + + Column { + width: parent.width - Theme.iconSize - (fixButton.visible ? fixButton.width + Theme.spacingM : 0) - Theme.spacingM + spacing: Theme.spacingXS + anchors.verticalCenter: parent.verticalCenter + + StyledText { + text: I18n.tr("First Time Setup") + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.primary + width: parent.width + horizontalAlignment: Text.AlignLeft + } + + StyledText { + text: I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg("dms/input") + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + width: parent.width + horizontalAlignment: Text.AlignLeft + } + } + + DankButton { + id: fixButton + visible: root.showSetup + text: root.fixing ? I18n.tr("Setting up...") : I18n.tr("Setup") + backgroundColor: Theme.primary + textColor: Theme.primaryText + enabled: !root.fixing + anchors.verticalCenter: parent.verticalCenter + onClicked: root.fixInclude() + } + } +} diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index eb43c2b88..bf0309ff2 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -1674,6 +1674,16 @@ window-rule { const touchpadTap = typeof SettingsData !== "undefined" ? SettingsData.touchpadTapToClick : defaultTouchpadTap; const touchpadTapAndDrag = typeof SettingsData !== "undefined" ? SettingsData.touchpadTapAndDrag : defaultTouchpadTapAndDrag; + const keyboardLayouts = typeof SettingsData !== "undefined" ? SettingsData.keyboardLayouts : ""; + const keyboardVariants = typeof SettingsData !== "undefined" ? SettingsData.keyboardVariants : ""; + const keyboardModel = typeof SettingsData !== "undefined" ? SettingsData.keyboardModel : ""; + const keyboardOptions = typeof SettingsData !== "undefined" ? SettingsData.keyboardOptions : ""; + const keyboardKeymapFile = typeof SettingsData !== "undefined" ? SettingsData.keyboardKeymapFile : ""; + const keyboardTrackLayout = typeof SettingsData !== "undefined" ? SettingsData.keyboardTrackLayout : ""; + const keyboardRepeatDelay = typeof SettingsData !== "undefined" ? SettingsData.keyboardRepeatDelay : 0; + const keyboardRepeatRate = typeof SettingsData !== "undefined" ? SettingsData.keyboardRepeatRate : 0; + const keyboardNumlock = typeof SettingsData !== "undefined" ? SettingsData.keyboardNumlock : false; + const dmsWarning = `// ! DO NOT EDIT ! // ! AUTO-GENERATED BY DMS ! // ! CHANGES WILL BE OVERWRITTEN ! @@ -1741,6 +1751,47 @@ window-rule { } inputContent += " }\n"; + // Keyboard block — emitted only when configured so untouched settings keep + // niri's locale1 fallback and any keyboard block in the user's own config. + const kdlString = value => `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; + const keyboardConfigured = keyboardKeymapFile || keyboardLayouts || keyboardVariants || keyboardModel || keyboardOptions || keyboardTrackLayout || keyboardRepeatDelay > 0 || keyboardRepeatRate > 0 || keyboardNumlock; + if (keyboardConfigured) { + inputContent += " keyboard {\n"; + if (keyboardKeymapFile) { + inputContent += " xkb {\n"; + inputContent += ` file ${kdlString(keyboardKeymapFile)}\n`; + inputContent += " }\n"; + } else if (keyboardLayouts || keyboardVariants || keyboardModel || keyboardOptions) { + inputContent += " xkb {\n"; + if (keyboardLayouts) { + inputContent += ` layout ${kdlString(keyboardLayouts)}\n`; + } + if (keyboardVariants) { + inputContent += ` variant ${kdlString(keyboardVariants)}\n`; + } + if (keyboardModel) { + inputContent += ` model ${kdlString(keyboardModel)}\n`; + } + if (keyboardOptions) { + inputContent += ` options ${kdlString(keyboardOptions)}\n`; + } + inputContent += " }\n"; + } + if (keyboardTrackLayout) { + inputContent += ` track-layout "${keyboardTrackLayout}"\n`; + } + if (keyboardRepeatDelay > 0) { + inputContent += ` repeat-delay ${keyboardRepeatDelay}\n`; + } + if (keyboardRepeatRate > 0) { + inputContent += ` repeat-rate ${keyboardRepeatRate}\n`; + } + if (keyboardNumlock) { + inputContent += " numlock\n"; + } + inputContent += " }\n"; + } + inputContent += "}\n"; const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); diff --git a/quickshell/translations/extract_settings_index.py b/quickshell/translations/extract_settings_index.py index 61105dba1..bf2835331 100755 --- a/quickshell/translations/extract_settings_index.py +++ b/quickshell/translations/extract_settings_index.py @@ -135,10 +135,14 @@ TAB_INDEX_MAP = { "UsersTab.qml": 35, "AutoStartTab.qml": 36, "BatteryTab.qml": 42, + "MouseTouchpadTab.qml": 44, + "KeyboardTab.qml": 45, } FILE_CONDITION_MAP = { "GreeterTab.qml": "greeterAvailable", + "MouseTouchpadTab.qml": "isNiri", + "KeyboardTab.qml": "isNiri", } TAB_CATEGORY_MAP = { @@ -185,6 +189,8 @@ TAB_CATEGORY_MAP = { 41: "Network", 42: "Power & Security", 43: "Dank Dash", + 44: "System", + 45: "System", } SEARCHABLE_COMPONENTS = [ diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index a8ee37514..8446ed8c6 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -10921,17 +10921,609 @@ ], "icon": "dashboard" }, + { + "section": "mouseAccelProfile", + "label": "Acceleration Profile", + "tabIndex": 44, + "category": "System", + "keywords": [ + "acceleration", + "adaptive", + "constant", + "flat", + "linux", + "mouse", + "movement", + "os", + "profile", + "scales", + "speed", + "system", + "uses" + ], + "description": "Flat uses constant speed; Adaptive scales with movement speed", + "conditionKey": "isNiri" + }, + { + "section": "touchpadAccelProfile", + "label": "Acceleration Profile", + "tabIndex": 44, + "category": "System", + "keywords": [ + "acceleration", + "adaptive", + "constant", + "flat", + "linux", + "movement", + "os", + "profile", + "scales", + "speed", + "system", + "touchpad", + "uses" + ], + "description": "Flat uses constant speed; Adaptive scales with movement speed", + "conditionKey": "isNiri" + }, + { + "section": "touchpadDisableWhileTyping", + "label": "Disable While Typing", + "tabIndex": 44, + "category": "System", + "keywords": [ + "accidental", + "cursor", + "disable", + "dwt", + "jumps", + "linux", + "os", + "prevent", + "system", + "touchpad", + "typing", + "while" + ], + "description": "Prevent accidental cursor jumps while typing", + "conditionKey": "isNiri" + }, + { + "section": "touchpadDisableOnExternalMouse", + "label": "Disable on External Mouse", + "tabIndex": 44, + "category": "System", + "keywords": [ + "connected", + "disable", + "external", + "linux", + "mouse", + "os", + "system", + "touchpad" + ], + "description": "Disable touchpad when an external mouse is connected", + "conditionKey": "isNiri" + }, + { + "section": "touchpadDragLock", + "label": "Drag Lock", + "tabIndex": 44, + "category": "System", + "keywords": [ + "briefly", + "drag", + "dragging", + "finger", + "keep", + "lifted", + "linux", + "lock", + "os", + "system", + "touchpad" + ], + "description": "Keep dragging when finger is briefly lifted", + "conditionKey": "isNiri" + }, + { + "section": "mouseLeftHanded", + "label": "Left-Handed Mode", + "tabIndex": 44, + "category": "System", + "keywords": [ + "button", + "buttons", + "handed", + "left", + "linux", + "mode", + "mouse", + "os", + "primary", + "secondary", + "swap", + "system" + ], + "description": "Swap primary and secondary mouse buttons", + "conditionKey": "isNiri" + }, + { + "section": "mouseMiddleEmulation", + "label": "Middle Click Emulation", + "tabIndex": 44, + "category": "System", + "keywords": [ + "buttons", + "click", + "emulate", + "emulation", + "left", + "linux", + "middle", + "mouse", + "os", + "pressing", + "right", + "system" + ], + "description": "Emulate middle click by pressing left and right buttons", + "conditionKey": "isNiri" + }, + { + "section": "touchpadMiddleEmulation", + "label": "Middle Click Emulation", + "tabIndex": 44, + "category": "System", + "keywords": [ + "buttons", + "click", + "emulate", + "emulation", + "left", + "linux", + "middle", + "os", + "pressing", + "right", + "system", + "touchpad" + ], + "description": "Emulate middle click by pressing left and right buttons", + "conditionKey": "isNiri" + }, { "section": "_tab_44", "label": "Mouse & Touchpad", "tabIndex": 44, - "category": "Settings", + "category": "System", "keywords": [ + "linux", "mouse", - "settings", + "os", + "system", "touchpad" ], "icon": "mouse", "conditionKey": "isNiri" + }, + { + "section": "mouseSettings", + "label": "Mouse Settings", + "tabIndex": 44, + "category": "System", + "keywords": [ + "acceleration", + "buttons", + "input", + "linux", + "mouse", + "os", + "pointer", + "primary", + "secondary", + "sensitivity", + "settings", + "swap", + "system" + ], + "icon": "mouse", + "description": "Swap primary and secondary mouse buttons", + "conditionKey": "isNiri" + }, + { + "section": "mouseNaturalScroll", + "label": "Natural Scrolling", + "tabIndex": 44, + "category": "System", + "keywords": [ + "direction", + "linux", + "mouse", + "natural", + "os", + "reverse", + "scroll", + "scrolling", + "system", + "wheel" + ], + "description": "Reverse mouse wheel scrolling direction", + "conditionKey": "isNiri" + }, + { + "section": "touchpadNaturalScroll", + "label": "Natural Scrolling", + "tabIndex": 44, + "category": "System", + "keywords": [ + "direction", + "finger", + "linux", + "natural", + "os", + "reverse", + "scroll", + "scrolling", + "system", + "touchpad" + ], + "description": "Reverse two-finger scrolling direction", + "conditionKey": "isNiri" + }, + { + "section": "mouseAccelSpeed", + "label": "Pointer Speed", + "tabIndex": 44, + "category": "System", + "keywords": [ + "accel", + "adjust", + "linux", + "mouse", + "os", + "pointer", + "sensitivity", + "speed", + "system" + ], + "description": "Adjust pointer sensitivity speed", + "conditionKey": "isNiri" + }, + { + "section": "mouseScrollMethod", + "label": "Scroll Method", + "tabIndex": 44, + "category": "System", + "keywords": [ + "choose", + "events", + "generate", + "linux", + "method", + "mouse", + "os", + "scroll", + "scrolling", + "system" + ], + "description": "Choose when to generate scrolling events", + "conditionKey": "isNiri" + }, + { + "section": "touchpadScrollMethod", + "label": "Scroll Method", + "tabIndex": 44, + "category": "System", + "keywords": [ + "choose", + "events", + "generate", + "linux", + "method", + "os", + "scroll", + "scrolling", + "system", + "touchpad" + ], + "description": "Choose when to generate scrolling events", + "conditionKey": "isNiri" + }, + { + "section": "mouseScrollFactor", + "label": "Scrolling Speed", + "tabIndex": 44, + "category": "System", + "keywords": [ + "adjust", + "factor", + "linux", + "mouse", + "multiplier", + "os", + "scroll", + "scrolling", + "sensitivity", + "speed", + "system" + ], + "description": "Adjust scrolling sensitivity multiplier", + "conditionKey": "isNiri" + }, + { + "section": "touchpadScrollFactor", + "label": "Scrolling Speed", + "tabIndex": 44, + "category": "System", + "keywords": [ + "adjust", + "factor", + "linux", + "multiplier", + "os", + "scroll", + "scrolling", + "sensitivity", + "speed", + "system", + "touchpad" + ], + "description": "Adjust scrolling sensitivity multiplier", + "conditionKey": "isNiri" + }, + { + "section": "touchpadTapAndDrag", + "label": "Tap and Drag", + "tabIndex": 44, + "category": "System", + "keywords": [ + "drag", + "items", + "linux", + "move", + "os", + "system", + "tap", + "touchpad" + ], + "description": "Tap and drag on the touchpad to move items", + "conditionKey": "isNiri" + }, + { + "section": "touchpadTapToClick", + "label": "Tap to Click", + "tabIndex": 44, + "category": "System", + "keywords": [ + "click", + "clicks", + "left", + "linux", + "os", + "surface", + "system", + "tap", + "touchpad", + "trigger" + ], + "description": "Tap the touchpad surface to trigger left click clicks", + "conditionKey": "isNiri" + }, + { + "section": "touchpadSettings", + "label": "Touchpad Settings", + "tabIndex": 44, + "category": "System", + "keywords": [ + "click", + "clicks", + "input", + "left", + "linux", + "natural", + "os", + "sensitivity", + "settings", + "surface", + "system", + "tap", + "touchpad", + "trigger" + ], + "icon": "trackpad_input_2", + "description": "Tap the touchpad surface to trigger left click clicks", + "conditionKey": "isNiri" + }, + { + "section": "touchpadAccelSpeed", + "label": "Touchpad Speed", + "tabIndex": 44, + "category": "System", + "keywords": [ + "accel", + "adjust", + "linux", + "os", + "pointer", + "sensitivity", + "speed", + "system", + "touchpad" + ], + "description": "Adjust touchpad pointer speed", + "conditionKey": "isNiri" + }, + { + "section": "keyboardNumlock", + "label": "Enable Num Lock", + "tabIndex": 45, + "category": "System", + "keywords": [ + "automatically", + "enable", + "keyboard", + "linux", + "lock", + "num", + "numlock", + "os", + "startup", + "system", + "turn" + ], + "description": "Automatically turn on Num Lock at startup", + "conditionKey": "isNiri" + }, + { + "section": "_tab_45", + "label": "Keyboard", + "tabIndex": 45, + "category": "System", + "keywords": [ + "keyboard", + "linux", + "os", + "system" + ], + "icon": "keyboard", + "conditionKey": "isNiri" + }, + { + "section": "keyboardBehaviorSettings", + "label": "Keyboard Behavior", + "tabIndex": 45, + "category": "System", + "keywords": [ + "across", + "apps", + "behavior", + "changes", + "delay", + "keyboard", + "layout", + "linux", + "numlock", + "os", + "rate", + "remembered", + "repeat", + "system" + ], + "icon": "settings", + "description": "How layout changes are remembered across apps", + "conditionKey": "isNiri" + }, + { + "section": "keyboardLayoutsSettings", + "label": "Keyboard Layouts", + "tabIndex": 45, + "category": "System", + "keywords": [ + "between", + "choose", + "cycle", + "input", + "keyboard", + "language", + "layout", + "layouts", + "linux", + "os", + "shortcut", + "system", + "xkb" + ], + "icon": "keyboard", + "description": "Choose a shortcut key to cycle between keyboard layouts", + "conditionKey": "isNiri" + }, + { + "section": "keyboardTrackLayout", + "label": "Remember Layout", + "tabIndex": 45, + "category": "System", + "keywords": [ + "across", + "apps", + "changes", + "keyboard", + "layout", + "linux", + "os", + "remember", + "remembered", + "system", + "track" + ], + "description": "How layout changes are remembered across apps", + "conditionKey": "isNiri" + }, + { + "section": "keyboardRepeatDelay", + "label": "Repeat Delay", + "tabIndex": 45, + "category": "System", + "keywords": [ + "before", + "characters", + "delay", + "keyboard", + "linux", + "os", + "repeat", + "repeating", + "speed", + "start", + "system" + ], + "description": "Delay before characters start repeating", + "conditionKey": "isNiri" + }, + { + "section": "keyboardRepeatRate", + "label": "Repeat Rate", + "tabIndex": 45, + "category": "System", + "keywords": [ + "characters", + "down", + "holding", + "keyboard", + "linux", + "os", + "rate", + "repeat", + "second", + "speed", + "system", + "while" + ], + "description": "Characters per second while holding down key", + "conditionKey": "isNiri" + }, + { + "section": "keyboardOptions", + "label": "Switch Layout Shortcut", + "tabIndex": 45, + "category": "System", + "keywords": [ + "between", + "choose", + "cycle", + "keyboard", + "layout", + "layouts", + "linux", + "os", + "shortcut", + "switch", + "system", + "xkb" + ], + "description": "Choose a shortcut key to cycle between keyboard layouts", + "conditionKey": "isNiri" } ]