diff --git a/core/cmd/dms/commands_config.go b/core/cmd/dms/commands_config.go index dc9505e5b..250ac044d 100644 --- a/core/cmd/dms/commands_config.go +++ b/core/cmd/dms/commands_config.go @@ -40,6 +40,7 @@ var resolveIncludeCmd = &cobra.Command{ "layout.kdl", "outputs.kdl", "binds.kdl", + "input.kdl", "cursor.conf", "layout.conf", "outputs.conf", diff --git a/core/cmd/dms/commands_setup.go b/core/cmd/dms/commands_setup.go index 69bab36d0..5f5998aed 100644 --- a/core/cmd/dms/commands_setup.go +++ b/core/cmd/dms/commands_setup.go @@ -143,6 +143,10 @@ var dmsConfigSpecs = map[string]dmsConfigSpec{ niriFile: "alttab.kdl", niriContent: func(_ string) string { return config.NiriAlttabConfig }, }, + "input": { + niriFile: "input.kdl", + niriContent: func(_ string) string { return config.NiriInputConfig }, + }, "outputs": { niriFile: "outputs.kdl", hyprFile: "outputs.lua", diff --git a/core/internal/config/deployer.go b/core/internal/config/deployer.go index 0f99ced74..5aa80c0da 100644 --- a/core/internal/config/deployer.go +++ b/core/internal/config/deployer.go @@ -264,6 +264,7 @@ func (cd *ConfigDeployer) deployNiriDmsConfigs(dmsDir, terminalCommand string) e {"layout.kdl", NiriLayoutConfig}, {"alttab.kdl", NiriAlttabConfig}, {"binds.kdl", strings.ReplaceAll(NiriBindsConfig, "{{TERMINAL_COMMAND}}", terminalCommand)}, + {"input.kdl", NiriInputConfig}, {"outputs.kdl", ""}, {"cursor.kdl", ""}, {"windowrules.kdl", ""}, diff --git a/core/internal/config/embedded/niri-input.kdl b/core/internal/config/embedded/niri-input.kdl new file mode 100644 index 000000000..714904594 --- /dev/null +++ b/core/internal/config/embedded/niri-input.kdl @@ -0,0 +1,15 @@ +// ! DO NOT EDIT ! +// ! AUTO-GENERATED BY DMS ! +// ! CHANGES WILL BE OVERWRITTEN ! +// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE ! + +input { + mouse { + accel-speed 0.0 + } + touchpad { + tap + accel-speed 0.0 + natural-scroll + } +} diff --git a/core/internal/config/embedded/niri.kdl b/core/internal/config/embedded/niri.kdl index f56f478aa..e0124563f 100644 --- a/core/internal/config/embedded/niri.kdl +++ b/core/internal/config/embedded/niri.kdl @@ -34,31 +34,6 @@ input { numlock } - // Next sections include libinput settings. - // Omitting settings disables them, or leaves them at their default values. - // All commented-out settings here are examples, not defaults. - touchpad { - // off - tap - // dwt - // dwtp - // drag false - // drag-lock - natural-scroll - // accel-speed 0.2 - // accel-profile "flat" - // scroll-method "two-finger" - // disabled-on-external-mouse - } - - mouse { - // off - // natural-scroll - // accel-speed 0.2 - // accel-profile "flat" - // scroll-method "no-scroll" - } - trackpoint { // off // natural-scroll @@ -77,6 +52,7 @@ input { // Setting max-scroll-amount="0%" makes it work only on windows already fully on screen. // focus-follows-mouse max-scroll-amount="0%" } + // You can configure outputs by their name, which you can find // by running `niri msg outputs` while inside a niri instance. // The built-in laptop monitor is usually called "eDP-1". @@ -271,3 +247,4 @@ include optional=true "dms/alttab.kdl" include optional=true "dms/binds.kdl" include optional=true "dms/outputs.kdl" include optional=true "dms/cursor.kdl" +include optional=true "dms/input.kdl" diff --git a/core/internal/config/niri.go b/core/internal/config/niri.go index e68f77e03..334f87f20 100644 --- a/core/internal/config/niri.go +++ b/core/internal/config/niri.go @@ -16,3 +16,6 @@ var NiriAlttabConfig string //go:embed embedded/niri-binds.kdl var NiriBindsConfig string + +//go:embed embedded/niri-input.kdl +var NiriInputConfig string diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index b0b4e0a15..5cf6a16ec 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -184,6 +184,24 @@ Singleton { property int mangoLayoutRadiusOverride: -1 property int mangoLayoutBorderSize: -1 property bool mangoTrackpadNaturalScrolling: true + property string mouseAccelProfile: "default" + property real mouseAccelSpeed: 0.0 + property bool mouseLeftHanded: false + property bool mouseMiddleEmulation: false + property bool mouseNaturalScroll: false + property real mouseScrollFactor: 1.0 + property string mouseScrollMethod: "default" + property string touchpadAccelProfile: "default" + property real touchpadAccelSpeed: 0.0 + property bool touchpadDisableOnExternalMouse: false + property bool touchpadDisableWhileTyping: true + property bool touchpadDragLock: false + property bool touchpadMiddleEmulation: false + property bool touchpadNaturalScroll: true + property real touchpadScrollFactor: 1.0 + property string touchpadScrollMethod: "default" + property bool touchpadTapAndDrag: true + property bool touchpadTapToClick: true property int firstDayOfWeek: -1 property bool showWeekNumber: false @@ -1479,6 +1497,13 @@ Singleton { MangoService.generateLayoutConfig(); } + function updateCompositorInput() { + if (typeof CompositorService === "undefined") + return; + if (CompositorService.isNiri && typeof NiriService !== "undefined") + NiriService.generateNiriInputConfig(); + } + function updateFrameCompositorLayout() { // Generate before begin() so compositor readiness is already pending at transitionRequested if (typeof CompositorService !== "undefined") { @@ -1707,6 +1732,7 @@ Singleton { "applyStoredTheme": applyStoredTheme, "regenSystemThemes": regenSystemThemes, "updateCompositorLayout": updateCompositorLayout, + "updateCompositorInput": updateCompositorInput, "applyStoredIconTheme": applyStoredIconTheme, "updateBarConfigs": updateBarConfigs, "updateCompositorCursor": updateCompositorCursor, diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 1fbb21049..dd4f14e1a 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -38,6 +38,24 @@ var SPEC = { mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" }, mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" }, mangoTrackpadNaturalScrolling: { def: true, onChange: "updateCompositorCursor" }, + mouseAccelProfile: { def: "default", onChange: "updateCompositorInput" }, + mouseAccelSpeed: { def: 0.0, onChange: "updateCompositorInput" }, + mouseLeftHanded: { def: false, onChange: "updateCompositorInput" }, + mouseMiddleEmulation: { def: false, onChange: "updateCompositorInput" }, + mouseNaturalScroll: { def: false, onChange: "updateCompositorInput" }, + mouseScrollFactor: { def: 1.0, onChange: "updateCompositorInput" }, + mouseScrollMethod: { def: "default", onChange: "updateCompositorInput" }, + touchpadAccelProfile: { def: "default", onChange: "updateCompositorInput" }, + touchpadAccelSpeed: { def: 0.0, onChange: "updateCompositorInput" }, + touchpadDisableOnExternalMouse: { def: false, onChange: "updateCompositorInput" }, + touchpadDisableWhileTyping: { def: true, onChange: "updateCompositorInput" }, + touchpadDragLock: { def: false, onChange: "updateCompositorInput" }, + touchpadMiddleEmulation: { def: false, onChange: "updateCompositorInput" }, + touchpadNaturalScroll: { def: true, onChange: "updateCompositorInput" }, + touchpadScrollFactor: { def: 1.0, onChange: "updateCompositorInput" }, + touchpadScrollMethod: { def: "default", onChange: "updateCompositorInput" }, + touchpadTapAndDrag: { def: true, onChange: "updateCompositorInput" }, + touchpadTapToClick: { def: true, onChange: "updateCompositorInput" }, firstDayOfWeek: { def: -1 }, showWeekNumber: { def: false }, diff --git a/quickshell/Modals/Settings/SettingsContent.qml b/quickshell/Modals/Settings/SettingsContent.qml index 4de081855..aaab9e19f 100644 --- a/quickshell/Modals/Settings/SettingsContent.qml +++ b/quickshell/Modals/Settings/SettingsContent.qml @@ -718,5 +718,20 @@ FocusScope { Qt.callLater(() => item.forceActiveFocus()); } } + + Loader { + id: mouseTouchpadLoader + anchors.fill: parent + active: root.currentIndex === 44 + visible: active + focus: active + + sourceComponent: MouseTouchpadTab {} + + onActiveChanged: { + if (active && item) + Qt.callLater(() => item.forceActiveFocus()); + } + } } } diff --git a/quickshell/Modals/Settings/SettingsSidebar.qml b/quickshell/Modals/Settings/SettingsSidebar.qml index 5ae7482b7..29692d1f6 100644 --- a/quickshell/Modals/Settings/SettingsSidebar.qml +++ b/quickshell/Modals/Settings/SettingsSidebar.qml @@ -319,6 +319,13 @@ Rectangle { "icon": "headphones", "tabIndex": 29 }, + { + "id": "mouse_touchpad", + "text": I18n.tr("Mouse & Touchpad"), + "icon": "mouse", + "tabIndex": 44, + "niriOnly": true + }, { "id": "locale", "text": I18n.tr("Locale"), diff --git a/quickshell/Modules/Settings/MouseTouchpadTab.qml b/quickshell/Modules/Settings/MouseTouchpadTab.qml new file mode 100644 index 000000000..de1075990 --- /dev/null +++ b/quickshell/Modules/Settings/MouseTouchpadTab.qml @@ -0,0 +1,412 @@ +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 + + 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 + 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 + + StyledRect { + id: warningBox + 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 { + width: parent.width + tags: ["mouse", "input", "sensitivity", "acceleration", "pointer"] + title: I18n.tr("Mouse Settings") + settingKey: "mouseSettings" + iconName: "mouse" + + SettingsToggleRow { + tags: ["mouse", "left", "handed", "button", "swap"] + settingKey: "mouseLeftHanded" + text: I18n.tr("Left-Handed Mode") + description: I18n.tr("Swap primary and secondary mouse buttons") + checked: SettingsData.mouseLeftHanded + onToggled: checked => SettingsData.set("mouseLeftHanded", checked) + } + + SettingsSliderRow { + tags: ["mouse", "sensitivity", "speed", "accel"] + settingKey: "mouseAccelSpeed" + text: I18n.tr("Pointer Speed") + description: I18n.tr("Adjust pointer sensitivity speed") + value: Math.round(SettingsData.mouseAccelSpeed * 10) + minimum: -10 + maximum: 10 + step: 1 + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("mouseAccelSpeed", newValue / 10.0) + } + + SettingsButtonGroupRow { + tags: ["mouse", "acceleration", "profile"] + settingKey: "mouseAccelProfile" + text: I18n.tr("Acceleration Profile") + 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; + return 0; + } + onSelectionChanged: (index, selected) => { + if (!selected) return; + const profiles = ["default", "flat", "adaptive"]; + SettingsData.set("mouseAccelProfile", profiles[index]); + } + } + + SettingsToggleRow { + tags: ["mouse", "natural", "scroll", "direction"] + settingKey: "mouseNaturalScroll" + text: I18n.tr("Natural Scrolling") + description: I18n.tr("Reverse mouse wheel scrolling direction") + checked: SettingsData.mouseNaturalScroll + onToggled: checked => SettingsData.set("mouseNaturalScroll", checked) + } + + SettingsSliderRow { + tags: ["mouse", "scroll", "speed", "factor"] + settingKey: "mouseScrollFactor" + text: I18n.tr("Scrolling Speed") + description: I18n.tr("Adjust scrolling sensitivity multiplier") + value: Math.round(SettingsData.mouseScrollFactor * 10) + minimum: 1 + maximum: 30 + step: 1 + defaultValue: 10 + onSliderValueChanged: newValue => SettingsData.set("mouseScrollFactor", newValue / 10.0) + } + + SettingsButtonGroupRow { + tags: ["mouse", "scroll", "method"] + settingKey: "mouseScrollMethod" + text: I18n.tr("Scroll Method") + 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; + return 0; + } + onSelectionChanged: (index, selected) => { + if (!selected) return; + const methods = ["default", "no-scroll", "on-button-down"]; + SettingsData.set("mouseScrollMethod", methods[index]); + } + } + + SettingsToggleRow { + tags: ["mouse", "middle", "click", "emulation"] + settingKey: "mouseMiddleEmulation" + text: I18n.tr("Middle Click Emulation") + description: I18n.tr("Emulate middle click by pressing left and right buttons") + checked: SettingsData.mouseMiddleEmulation + onToggled: checked => SettingsData.set("mouseMiddleEmulation", checked) + } + } + + SettingsCard { + width: parent.width + tags: ["touchpad", "input", "sensitivity", "tap", "click", "natural"] + title: I18n.tr("Touchpad Settings") + settingKey: "touchpadSettings" + iconName: "trackpad_input_2" + + SettingsToggleRow { + tags: ["touchpad", "tap", "click"] + settingKey: "touchpadTapToClick" + text: I18n.tr("Tap to Click") + description: I18n.tr("Tap the touchpad surface to trigger left click clicks") + checked: SettingsData.touchpadTapToClick + onToggled: checked => SettingsData.set("touchpadTapToClick", checked) + } + + SettingsToggleRow { + tags: ["touchpad", "tap", "drag"] + settingKey: "touchpadTapAndDrag" + text: I18n.tr("Tap and Drag") + description: I18n.tr("Tap and drag on the touchpad to move items") + checked: SettingsData.touchpadTapAndDrag + onToggled: checked => SettingsData.set("touchpadTapAndDrag", checked) + } + + SettingsToggleRow { + tags: ["touchpad", "drag", "lock"] + settingKey: "touchpadDragLock" + text: I18n.tr("Drag Lock") + description: I18n.tr("Keep dragging when finger is briefly lifted") + checked: SettingsData.touchpadDragLock + onToggled: checked => SettingsData.set("touchpadDragLock", checked) + } + + SettingsSliderRow { + tags: ["touchpad", "sensitivity", "speed", "accel"] + settingKey: "touchpadAccelSpeed" + text: I18n.tr("Touchpad Speed") + description: I18n.tr("Adjust touchpad pointer speed") + value: Math.round(SettingsData.touchpadAccelSpeed * 10) + minimum: -10 + maximum: 10 + step: 1 + defaultValue: 0 + onSliderValueChanged: newValue => SettingsData.set("touchpadAccelSpeed", newValue / 10.0) + } + + SettingsButtonGroupRow { + tags: ["touchpad", "acceleration", "profile"] + settingKey: "touchpadAccelProfile" + text: I18n.tr("Acceleration Profile") + 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; + return 0; + } + onSelectionChanged: (index, selected) => { + if (!selected) return; + const profiles = ["default", "flat", "adaptive"]; + SettingsData.set("touchpadAccelProfile", profiles[index]); + } + } + + SettingsToggleRow { + tags: ["touchpad", "natural", "scroll", "direction"] + settingKey: "touchpadNaturalScroll" + text: I18n.tr("Natural Scrolling") + description: I18n.tr("Reverse two-finger scrolling direction") + checked: SettingsData.touchpadNaturalScroll + onToggled: checked => SettingsData.set("touchpadNaturalScroll", checked) + } + + SettingsSliderRow { + tags: ["touchpad", "scroll", "speed", "factor"] + settingKey: "touchpadScrollFactor" + text: I18n.tr("Scrolling Speed") + description: I18n.tr("Adjust scrolling sensitivity multiplier") + value: Math.round(SettingsData.touchpadScrollFactor * 10) + minimum: 1 + maximum: 30 + step: 1 + defaultValue: 10 + onSliderValueChanged: newValue => SettingsData.set("touchpadScrollFactor", newValue / 10.0) + } + + SettingsButtonGroupRow { + tags: ["touchpad", "scroll", "method"] + settingKey: "touchpadScrollMethod" + text: I18n.tr("Scroll Method") + 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; + return 0; + } + onSelectionChanged: (index, selected) => { + if (!selected) return; + const methods = ["default", "two-finger", "edge", "no-scroll", "on-button-down"]; + SettingsData.set("touchpadScrollMethod", methods[index]); + } + } + + SettingsToggleRow { + tags: ["touchpad", "disable", "typing", "dwt"] + settingKey: "touchpadDisableWhileTyping" + text: I18n.tr("Disable While Typing") + description: I18n.tr("Prevent accidental cursor jumps while typing") + checked: SettingsData.touchpadDisableWhileTyping + onToggled: checked => SettingsData.set("touchpadDisableWhileTyping", checked) + } + + SettingsToggleRow { + tags: ["touchpad", "disable", "external", "mouse"] + settingKey: "touchpadDisableOnExternalMouse" + text: I18n.tr("Disable on External Mouse") + description: I18n.tr("Disable touchpad when an external mouse is connected") + checked: SettingsData.touchpadDisableOnExternalMouse + onToggled: checked => SettingsData.set("touchpadDisableOnExternalMouse", checked) + } + + SettingsToggleRow { + tags: ["touchpad", "middle", "click", "emulation"] + settingKey: "touchpadMiddleEmulation" + text: I18n.tr("Middle Click Emulation") + description: I18n.tr("Emulate middle click by pressing left and right buttons") + checked: SettingsData.touchpadMiddleEmulation + onToggled: checked => SettingsData.set("touchpadMiddleEmulation", checked) + } + } + } + } +} diff --git a/quickshell/Services/NiriService.qml b/quickshell/Services/NiriService.qml index e8202484c..d1c1bc96b 100644 --- a/quickshell/Services/NiriService.qml +++ b/quickshell/Services/NiriService.qml @@ -73,6 +73,7 @@ Singleton { Component.onCompleted: { fetchOutputs(); Paths.mkdir(screenshotsDir); + generateNiriInputConfig(); } Timer { @@ -105,6 +106,15 @@ Singleton { } } + Connections { + target: CompositorService + function onIsNiriChanged() { + if (CompositorService.isNiri) { + generateNiriInputConfig(); + } + } + } + Process { id: validateProcess command: ["niri", "validate"] @@ -175,6 +185,20 @@ Singleton { } } + Process { + id: writeInputProcess + property string inputContent: "" + property string inputPath: "" + + onExited: exitCode => { + if (exitCode === 0) { + log.info("Generated input config at", inputPath); + return; + } + log.warn("Failed to write input config, exit code:", exitCode); + } + } + Process { id: writeCursorProcess property string cursorContent: "" @@ -1261,7 +1285,7 @@ window-rule { writeAlttabProcess.running = true; } - for (const name of ["outputs", "binds", "cursor", "windowrules", "colors", "alttab", "layout"]) { + for (const name of ["outputs", "binds", "cursor", "windowrules", "colors", "alttab", "layout", "input"]) { const path = niriDmsDir + "/" + name + ".kdl"; Proc.runCommand("niri-ensure-" + name, ["sh", "-c", `mkdir -p "${niriDmsDir}" && [ ! -f "${path}" ] && touch "${path}" || true`], (output, exitCode) => { if (exitCode !== 0) @@ -1641,6 +1665,129 @@ window-rule { }); } + function generateNiriInputConfig() { + if (!CompositorService.isNiri) + return; + + const defaultMouseLeftHanded = false; + const defaultMouseMiddleEmulation = false; + const defaultMouseNaturalScroll = false; + const defaultMouseProfile = "default"; + const defaultMouseScrollFactor = 1.0; + const defaultMouseScrollMethod = "default"; + const defaultMouseSpeed = 0.0; + + const defaultTouchpadDisableOnExternalMouse = false; + const defaultTouchpadDisableWhileTyping = true; + const defaultTouchpadDragLock = false; + const defaultTouchpadMiddleEmulation = false; + const defaultTouchpadNaturalScroll = true; + const defaultTouchpadProfile = "default"; + const defaultTouchpadScrollFactor = 1.0; + const defaultTouchpadScrollMethod = "default"; + const defaultTouchpadSpeed = 0.0; + const defaultTouchpadTap = true; + const defaultTouchpadTapAndDrag = true; + + const mouseLeftHanded = typeof SettingsData !== "undefined" ? SettingsData.mouseLeftHanded : defaultMouseLeftHanded; + const mouseMiddleEmulation = typeof SettingsData !== "undefined" ? SettingsData.mouseMiddleEmulation : defaultMouseMiddleEmulation; + const mouseNaturalScroll = typeof SettingsData !== "undefined" ? SettingsData.mouseNaturalScroll : defaultMouseNaturalScroll; + const mouseProfile = typeof SettingsData !== "undefined" ? SettingsData.mouseAccelProfile : defaultMouseProfile; + const mouseScrollFactor = typeof SettingsData !== "undefined" ? SettingsData.mouseScrollFactor : defaultMouseScrollFactor; + const mouseScrollMethod = typeof SettingsData !== "undefined" ? SettingsData.mouseScrollMethod : defaultMouseScrollMethod; + const mouseSpeed = typeof SettingsData !== "undefined" ? SettingsData.mouseAccelSpeed : defaultMouseSpeed; + + const touchpadDisableOnExternalMouse = typeof SettingsData !== "undefined" ? SettingsData.touchpadDisableOnExternalMouse : defaultTouchpadDisableOnExternalMouse; + const touchpadDisableWhileTyping = typeof SettingsData !== "undefined" ? SettingsData.touchpadDisableWhileTyping : defaultTouchpadDisableWhileTyping; + const touchpadDragLock = typeof SettingsData !== "undefined" ? SettingsData.touchpadDragLock : defaultTouchpadDragLock; + const touchpadMiddleEmulation = typeof SettingsData !== "undefined" ? SettingsData.touchpadMiddleEmulation : defaultTouchpadMiddleEmulation; + const touchpadNaturalScroll = typeof SettingsData !== "undefined" ? SettingsData.touchpadNaturalScroll : defaultTouchpadNaturalScroll; + const touchpadProfile = typeof SettingsData !== "undefined" ? SettingsData.touchpadAccelProfile : defaultTouchpadProfile; + const touchpadScrollFactor = typeof SettingsData !== "undefined" ? SettingsData.touchpadScrollFactor : defaultTouchpadScrollFactor; + const touchpadScrollMethod = typeof SettingsData !== "undefined" ? SettingsData.touchpadScrollMethod : defaultTouchpadScrollMethod; + const touchpadSpeed = typeof SettingsData !== "undefined" ? SettingsData.touchpadAccelSpeed : defaultTouchpadSpeed; + const touchpadTap = typeof SettingsData !== "undefined" ? SettingsData.touchpadTapToClick : defaultTouchpadTap; + const touchpadTapAndDrag = typeof SettingsData !== "undefined" ? SettingsData.touchpadTapAndDrag : defaultTouchpadTapAndDrag; + + const dmsWarning = `// ! DO NOT EDIT ! +// ! AUTO-GENERATED BY DMS ! +// ! CHANGES WILL BE OVERWRITTEN ! +// ! PLACE YOUR CUSTOM CONFIGURATION ELSEWHERE ! + +`; + + let inputContent = dmsWarning + "input {\n"; + + // Mouse block + inputContent += " mouse {\n"; + inputContent += ` accel-speed ${mouseSpeed.toFixed(1)}\n`; + if (mouseProfile !== "default") { + inputContent += ` accel-profile "${mouseProfile}"\n`; + } + if (mouseNaturalScroll) { + inputContent += " natural-scroll\n"; + } + if (mouseLeftHanded) { + inputContent += " left-handed\n"; + } + if (mouseMiddleEmulation) { + inputContent += " middle-emulation\n"; + } + if (mouseScrollFactor !== 1.0) { + inputContent += ` scroll-factor ${mouseScrollFactor.toFixed(1)}\n`; + } + if (mouseScrollMethod !== "default") { + inputContent += ` scroll-method "${mouseScrollMethod}"\n`; + } + inputContent += " }\n"; + + // Touchpad block + inputContent += " touchpad {\n"; + if (touchpadTap) { + inputContent += " tap\n"; + } + inputContent += ` accel-speed ${touchpadSpeed.toFixed(1)}\n`; + if (touchpadProfile !== "default") { + inputContent += ` accel-profile "${touchpadProfile}"\n`; + } + if (touchpadNaturalScroll) { + inputContent += " natural-scroll\n"; + } + if (touchpadDisableOnExternalMouse) { + inputContent += " disabled-on-external-mouse\n"; + } + if (touchpadDisableWhileTyping) { + inputContent += " dwt\n"; + } + if (touchpadMiddleEmulation) { + inputContent += " middle-emulation\n"; + } + if (touchpadScrollFactor !== 1.0) { + inputContent += ` scroll-factor ${touchpadScrollFactor.toFixed(1)}\n`; + } + if (touchpadScrollMethod !== "default") { + inputContent += ` scroll-method "${touchpadScrollMethod}"\n`; + } + if (!touchpadTapAndDrag) { + inputContent += " drag false\n"; + } + if (touchpadDragLock) { + inputContent += " drag-lock\n"; + } + inputContent += " }\n"; + + inputContent += "}\n"; + + const configDir = Paths.strip(StandardPaths.writableLocation(StandardPaths.ConfigLocation)); + const niriDmsDir = configDir + "/niri/dms"; + const inputPath = niriDmsDir + "/input.kdl"; + + writeInputProcess.inputContent = inputContent; + writeInputProcess.inputPath = inputPath; + writeInputProcess.command = ["sh", "-c", `mkdir -p "${niriDmsDir}" && cat > "${inputPath}" << 'EOF'\n${inputContent}\nEOF`]; + writeInputProcess.running = true; + } + IpcHandler { function screenshot(): string { if (!CompositorService.isNiri) { diff --git a/quickshell/translations/settings_search_index.json b/quickshell/translations/settings_search_index.json index 5a7a348f0..583435dbd 100644 --- a/quickshell/translations/settings_search_index.json +++ b/quickshell/translations/settings_search_index.json @@ -9916,5 +9916,18 @@ "widgets" ], "icon": "dashboard" + }, + { + "section": "_tab_44", + "label": "Mouse & Touchpad", + "tabIndex": 44, + "category": "Settings", + "keywords": [ + "mouse", + "settings", + "touchpad" + ], + "icon": "mouse", + "conditionKey": "isNiri" } ]