detached 819389e

feat(input): configure mice and left-handed pointers (#108)

* refactor(input): share pointer settings between touchpad and mouse

Move natural-scroll, accel and left-handed into PointerConfig, embedded by TouchpadConfig and used by the new mouse.* keys. Add InputDevicesConfig for the input adapter.

* refactor(app): send latest config and scenes through one helper

Relay the touchpad and mouse config as one InputDevicesConfig so a change to either reaches the input adapter. Share the keep-newest channel send between the config relay and the scene consumer.

* feat(input): configure mice and left-handed pointers

Configure devices through an unexported deviceConfig seam (zero-size
libinputDevices implementation, Mockery mock) and pick settings by device
type: touchpads take touchpad.* and tapping, other pointers take mouse.*,
keyboards and the like are left alone. This stops touchpad.natural-scroll
from being applied to mice. Left-handed is set through libinput when the
device offers it.

Refs #98

* docs(input): document mouse natural scroll in the overview and trackpoint scope

* refactor(input): embed device config in Config and tighten pointer tests
commit
819389ed0febe5d2a23cf495e771044e63e22c10
author
bnema · · 2026-10-03 13:25 UTC
committer
GitHub · · 2026-10-03 13:25 UTC
parent
f28b0f0
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
12 files · +938 -176
M .mockery.yml +7 -0
@@ -31,6 +31,13 @@ packages:
3131 dir: internal/adapters/drm
3232 pkgname: drm
3333 filename: kms_mock_test.go
34+ github.com/bnema/neferwl/internal/adapters/libinput:
35+ interfaces:
36+ deviceConfig:
37+ config:
38+ dir: internal/adapters/libinput
39+ pkgname: libinput
40+ filename: device_config_mock_test.go
3441 github.com/bnema/neferwl/internal/adapters/vulkan:
3542 interfaces:
3643 captureSync:
@@ -31,6 +31,13 @@ packages:
31 dir: internal/adapters/drm31 dir: internal/adapters/drm
32 pkgname: drm32 pkgname: drm
33 filename: kms_mock_test.go33 filename: kms_mock_test.go
34+ github.com/bnema/neferwl/internal/adapters/libinput:
35+ interfaces:
36+ deviceConfig:
37+ config:
38+ dir: internal/adapters/libinput
39+ pkgname: libinput
40+ filename: device_config_mock_test.go
34 github.com/bnema/neferwl/internal/adapters/vulkan:41 github.com/bnema/neferwl/internal/adapters/vulkan:
35 interfaces:42 interfaces:
36 captureSync:43 captureSync:
M docs/config.md +6 -1
@@ -37,7 +37,12 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`).
3737 | `touchpad.tap` | `on` | Tap to click: one finger left, two right, three middle |
3838 | `touchpad.accel-speed` | `0` | Touchpad pointer speed, from `-1` (slowest) to `1` (fastest) |
3939 | `touchpad.accel-profile` | `adaptive` | `adaptive`: faster finger moves go further; `flat`: constant speed |
40+| `touchpad.left-handed` | `off` | Swaps the left and right touchpad buttons |
4041 | `touchpad.scroll-factor` | `1` | Multiplies two-finger scroll distance, above `0` up to `10` |
42+| `mouse.natural-scroll` | `off` | Wheel scrolling moves the content with the wheel instead of against it. `mouse.*` applies to every pointer that is not a touchpad: mice, trackballs, trackpoints |
43+| `mouse.accel-speed` | `0` | Mouse pointer speed, from `-1` (slowest) to `1` (fastest) |
44+| `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed |
45+| `mouse.left-handed` | `off` | Swaps the left and right mouse buttons |
4146 | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` |
4247 | `focus.follow-move` | `off` | Follow a column moved to another workspace |
4348 | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, the overview and a window focused as it opens |
@@ -189,7 +194,7 @@ The focus binds (`focus-column-left/right`, `focus-window-up/down`, `cmd+arrows`
189194
190195 Each workspace's [stash](#stash), hidden or not, shows as a pile of cards on the left of its row, which stays centred unless it would overlap the pile: its selected window in front, up to three others behind it, dimmed. In the pile, `h` / `l` browse the stash and `l` past its last window returns to the front card. A successful up/down move through the stack leaves the stash and selects the new front card. `return` or a click on a card closes the overview with the stash shown on that window.
191196
192-A four-finger swipe up opens the overview and a swipe down closes it on the selection, whatever `touchpad.natural-scroll` says. Two-finger scrolling and the mouse wheel move the selection: left and right through the columns, up and down through stack cards before crossing workspaces, following `touchpad.natural-scroll`. A two-finger scroll sideways moves it to the next column after a short distance, then one column per longer distance, and stays on the columns until the fingers lift; up or down it moves one step per scroll, so a card in a stack is easy to pick. The wheel moves one step per notch. A three-finger swipe moves it one step when the fingers lift.
197+A four-finger swipe up opens the overview and a swipe down closes it on the selection, whatever `touchpad.natural-scroll` says. Two-finger scrolling and the mouse wheel move the selection: left and right through the columns, up and down through stack cards before crossing workspaces, following `touchpad.natural-scroll` for two-finger scrolling and `mouse.natural-scroll` for the wheel. A two-finger scroll sideways moves it to the next column after a short distance, then one column per longer distance, and stays on the columns until the fingers lift; up or down it moves one step per scroll, so a card in a stack is easy to pick. The wheel moves one step per notch. A three-finger swipe moves it one step when the fingers lift.
193198
194199 Return or a click on a front tile or peeking card commits it; card changes remain provisional until then. `close-window` targets the selected preview. Move-to-workspace binds (`move-column-to-workspace*`, `move-window-to-workspace*`) move the selected preview at once and keep the overview open; with `focus.follow-move`, the selection follows it. Escape does not undo a move, and the row that received the window keeps it focused. A selected stash card, or a row with no preview, does not move. Other window mutation binds (moving within the row, resizing, maximizing, fullscreen and stash toggles) and moving a workspace to another monitor are disabled while the overview is open. Workspace and monitor navigation, launch and quit binds remain active.
195200
@@ -37,7 +37,12 @@ NeferWL reads `$XDG_CONFIG_HOME/neferwl/config` (or `~/.config/neferwl/config`).
37 | `touchpad.tap` | `on` | Tap to click: one finger left, two right, three middle |37 | `touchpad.tap` | `on` | Tap to click: one finger left, two right, three middle |
38 | `touchpad.accel-speed` | `0` | Touchpad pointer speed, from `-1` (slowest) to `1` (fastest) |38 | `touchpad.accel-speed` | `0` | Touchpad pointer speed, from `-1` (slowest) to `1` (fastest) |
39 | `touchpad.accel-profile` | `adaptive` | `adaptive`: faster finger moves go further; `flat`: constant speed |39 | `touchpad.accel-profile` | `adaptive` | `adaptive`: faster finger moves go further; `flat`: constant speed |
40+| `touchpad.left-handed` | `off` | Swaps the left and right touchpad buttons |
40 | `touchpad.scroll-factor` | `1` | Multiplies two-finger scroll distance, above `0` up to `10` |41 | `touchpad.scroll-factor` | `1` | Multiplies two-finger scroll distance, above `0` up to `10` |
42+| `mouse.natural-scroll` | `off` | Wheel scrolling moves the content with the wheel instead of against it. `mouse.*` applies to every pointer that is not a touchpad: mice, trackballs, trackpoints |
43+| `mouse.accel-speed` | `0` | Mouse pointer speed, from `-1` (slowest) to `1` (fastest) |
44+| `mouse.accel-profile` | `adaptive` | `adaptive`: faster moves go further; `flat`: constant speed |
45+| `mouse.left-handed` | `off` | Swaps the left and right mouse buttons |
41 | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` |46 | `cursor.hide-after` | `5s` | Hides the pointer cursor after this long without motion; the next motion shows it. 100ms to 1h, or `off` |
42 | `focus.follow-move` | `off` | Follow a column moved to another workspace |47 | `focus.follow-move` | `off` | Follow a column moved to another workspace |
43 | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, the overview and a window focused as it opens |48 | `focus.animation` | `pulse` | Focus indicator on a window that keeps the focus for 150 ms: `pulse` (brief rise and fall) or `off`. Skipped for fullscreen windows, the overview and a window focused as it opens |
@@ -189,7 +194,7 @@ The focus binds (`focus-column-left/right`, `focus-window-up/down`, `cmd+arrows`
189 194
190 Each workspace's [stash](#stash), hidden or not, shows as a pile of cards on the left of its row, which stays centred unless it would overlap the pile: its selected window in front, up to three others behind it, dimmed. In the pile, `h` / `l` browse the stash and `l` past its last window returns to the front card. A successful up/down move through the stack leaves the stash and selects the new front card. `return` or a click on a card closes the overview with the stash shown on that window.195 Each workspace's [stash](#stash), hidden or not, shows as a pile of cards on the left of its row, which stays centred unless it would overlap the pile: its selected window in front, up to three others behind it, dimmed. In the pile, `h` / `l` browse the stash and `l` past its last window returns to the front card. A successful up/down move through the stack leaves the stash and selects the new front card. `return` or a click on a card closes the overview with the stash shown on that window.
191 196
192-A four-finger swipe up opens the overview and a swipe down closes it on the selection, whatever `touchpad.natural-scroll` says. Two-finger scrolling and the mouse wheel move the selection: left and right through the columns, up and down through stack cards before crossing workspaces, following `touchpad.natural-scroll`. A two-finger scroll sideways moves it to the next column after a short distance, then one column per longer distance, and stays on the columns until the fingers lift; up or down it moves one step per scroll, so a card in a stack is easy to pick. The wheel moves one step per notch. A three-finger swipe moves it one step when the fingers lift.197+A four-finger swipe up opens the overview and a swipe down closes it on the selection, whatever `touchpad.natural-scroll` says. Two-finger scrolling and the mouse wheel move the selection: left and right through the columns, up and down through stack cards before crossing workspaces, following `touchpad.natural-scroll` for two-finger scrolling and `mouse.natural-scroll` for the wheel. A two-finger scroll sideways moves it to the next column after a short distance, then one column per longer distance, and stays on the columns until the fingers lift; up or down it moves one step per scroll, so a card in a stack is easy to pick. The wheel moves one step per notch. A three-finger swipe moves it one step when the fingers lift.
193 198
194 Return or a click on a front tile or peeking card commits it; card changes remain provisional until then. `close-window` targets the selected preview. Move-to-workspace binds (`move-column-to-workspace*`, `move-window-to-workspace*`) move the selected preview at once and keep the overview open; with `focus.follow-move`, the selection follows it. Escape does not undo a move, and the row that received the window keeps it focused. A selected stash card, or a row with no preview, does not move. Other window mutation binds (moving within the row, resizing, maximizing, fullscreen and stash toggles) and moving a workspace to another monitor are disabled while the overview is open. Workspace and monitor navigation, launch and quit binds remain active.199 Return or a click on a front tile or peeking card commits it; card changes remain provisional until then. `close-window` targets the selected preview. Move-to-workspace binds (`move-column-to-workspace*`, `move-window-to-workspace*`) move the selected preview at once and keep the overview open; with `focus.follow-move`, the selection follows it. Escape does not undo a move, and the row that received the window keeps it focused. A selected stash card, or a row with no preview, does not move. Other window mutation binds (moving within the row, resizing, maximizing, fullscreen and stash toggles) and moving a workspace to another monitor are disabled while the overview is open. Workspace and monitor navigation, launch and quit binds remain active.
195 200
M examples/config +9 -0
@@ -54,9 +54,18 @@ touchpad.tap = on
5454 # Pointer speed from -1 to 1; adaptive speeds up quick moves, flat does not.
5555 touchpad.accel-speed = 0
5656 touchpad.accel-profile = adaptive
57+# left-handed = on swaps the left and right buttons.
58+touchpad.left-handed = off
5759 # Two-finger scroll distance multiplier.
5860 touchpad.scroll-factor = 1
5961
62+# Mouse. natural-scroll = on moves the content with the wheel; speed and
63+# profile work as for the touchpad.
64+mouse.natural-scroll = off
65+mouse.accel-speed = 0
66+mouse.accel-profile = adaptive
67+mouse.left-handed = off
68+
6069 # Hide the pointer cursor after this long without motion (100ms to 1h, or
6170 # off); the next motion shows it.
6271 cursor.hide-after = 5s
@@ -54,9 +54,18 @@ touchpad.tap = on
54 # Pointer speed from -1 to 1; adaptive speeds up quick moves, flat does not.54 # Pointer speed from -1 to 1; adaptive speeds up quick moves, flat does not.
55 touchpad.accel-speed = 055 touchpad.accel-speed = 0
56 touchpad.accel-profile = adaptive56 touchpad.accel-profile = adaptive
57+# left-handed = on swaps the left and right buttons.
58+touchpad.left-handed = off
57 # Two-finger scroll distance multiplier.59 # Two-finger scroll distance multiplier.
58 touchpad.scroll-factor = 160 touchpad.scroll-factor = 1
59 61
62+# Mouse. natural-scroll = on moves the content with the wheel; speed and
63+# profile work as for the touchpad.
64+mouse.natural-scroll = off
65+mouse.accel-speed = 0
66+mouse.accel-profile = adaptive
67+mouse.left-handed = off
68+
60 # Hide the pointer cursor after this long without motion (100ms to 1h, or69 # Hide the pointer cursor after this long without motion (100ms to 1h, or
61 # off); the next motion shows it.70 # off); the next motion shows it.
62 cursor.hide-after = 5s71 cursor.hide-after = 5s
M internal/adapters/config/config.go +41 -17
@@ -125,6 +125,7 @@ func Defaults() ports.Config {
125125 c.Touchpad.Tap = true
126126 c.Touchpad.AccelProfile = ports.AccelAdaptive
127127 c.Touchpad.ScrollFactor = 1
128+ c.Mouse.AccelProfile = ports.AccelAdaptive
128129 c.Cursor.HideAfter = 5 * time.Second
129130 c.Focus.Animation = ports.FocusAnimationPulse
130131 c.Focus.Effect = ports.FocusEffectScreen
@@ -557,29 +558,12 @@ func set(c *ports.Config, key, v string) error {
557558 }
558559 }
559560 c.Layout.Presets = list
560- case "touchpad.natural-scroll":
561- b, err := onOff(v)
562- if err != nil {
563- return err
564- }
565- c.Touchpad.NaturalScroll = b
566561 case "touchpad.tap":
567562 b, err := onOff(v)
568563 if err != nil {
569564 return err
570565 }
571566 c.Touchpad.Tap = b
572- case "touchpad.accel-speed":
573- s, err := strconv.ParseFloat(v, 64)
574- if err != nil || !(s >= -1 && s <= 1) {
575- return fmt.Errorf("must be between -1 and 1")
576- }
577- c.Touchpad.AccelSpeed = s
578- case "touchpad.accel-profile":
579- if v != ports.AccelAdaptive && v != ports.AccelFlat {
580- return fmt.Errorf("must be adaptive or flat")
581- }
582- c.Touchpad.AccelProfile = v
583567 case "touchpad.scroll-factor":
584568 f, err := strconv.ParseFloat(v, 64)
585569 if err != nil || !(f > 0 && f <= 10) {
@@ -666,6 +650,12 @@ func set(c *ports.Config, key, v string) error {
666650 }
667651 c.Log.Debug = list
668652 default:
653+ if rest, ok := strings.CutPrefix(key, "touchpad."); ok {
654+ return parsePointerKey(rest, v, &c.Touchpad.PointerConfig)
655+ }
656+ if rest, ok := strings.CutPrefix(key, "mouse."); ok {
657+ return parsePointerKey(rest, v, &c.Mouse)
658+ }
669659 if rest, ok := strings.CutPrefix(key, "layout."); ok {
670660 if i := strings.LastIndex(rest, "."); i > 0 {
671661 return setOutputLayout(c, rest[:i], rest[i+1:], v)
@@ -676,6 +666,40 @@ func set(c *ports.Config, key, v string) error {
676666 return nil
677667 }
678668
669+// parsePointerKey applies one pointer key shared by touchpads and mice, with
670+// the touchpad. or mouse. prefix already removed. Touchpad-only keys are
671+// handled before; anything else is unknown. On an error p is left unchanged.
672+func parsePointerKey(key, v string, p *ports.PointerConfig) error {
673+ switch key {
674+ case "natural-scroll":
675+ b, err := onOff(v)
676+ if err != nil {
677+ return err
678+ }
679+ p.NaturalScroll = b
680+ case "left-handed":
681+ b, err := onOff(v)
682+ if err != nil {
683+ return err
684+ }
685+ p.LeftHanded = b
686+ case "accel-speed":
687+ s, err := strconv.ParseFloat(v, 64)
688+ if err != nil || !(s >= -1 && s <= 1) {
689+ return fmt.Errorf("must be between -1 and 1")
690+ }
691+ p.AccelSpeed = s
692+ case "accel-profile":
693+ if v != ports.AccelAdaptive && v != ports.AccelFlat {
694+ return fmt.Errorf("must be adaptive or flat")
695+ }
696+ p.AccelProfile = v
697+ default:
698+ return fmt.Errorf("unknown key")
699+ }
700+ return nil
701+}
702+
679703 // setOutputLayout applies one layout.<output>.<field> key.
680704 func setOutputLayout(c *ports.Config, output, field, v string) error {
681705 i := slices.IndexFunc(c.Layout.Outputs, func(o ports.OutputLayout) bool { return o.Output == output })
@@ -125,6 +125,7 @@ func Defaults() ports.Config {
125 c.Touchpad.Tap = true125 c.Touchpad.Tap = true
126 c.Touchpad.AccelProfile = ports.AccelAdaptive126 c.Touchpad.AccelProfile = ports.AccelAdaptive
127 c.Touchpad.ScrollFactor = 1127 c.Touchpad.ScrollFactor = 1
128+ c.Mouse.AccelProfile = ports.AccelAdaptive
128 c.Cursor.HideAfter = 5 * time.Second129 c.Cursor.HideAfter = 5 * time.Second
129 c.Focus.Animation = ports.FocusAnimationPulse130 c.Focus.Animation = ports.FocusAnimationPulse
130 c.Focus.Effect = ports.FocusEffectScreen131 c.Focus.Effect = ports.FocusEffectScreen
@@ -557,29 +558,12 @@ func set(c *ports.Config, key, v string) error {
557 }558 }
558 }559 }
559 c.Layout.Presets = list560 c.Layout.Presets = list
560- case "touchpad.natural-scroll":
561- b, err := onOff(v)
562- if err != nil {
563- return err
564- }
565- c.Touchpad.NaturalScroll = b
566 case "touchpad.tap":561 case "touchpad.tap":
567 b, err := onOff(v)562 b, err := onOff(v)
568 if err != nil {563 if err != nil {
569 return err564 return err
570 }565 }
571 c.Touchpad.Tap = b566 c.Touchpad.Tap = b
572- case "touchpad.accel-speed":
573- s, err := strconv.ParseFloat(v, 64)
574- if err != nil || !(s >= -1 && s <= 1) {
575- return fmt.Errorf("must be between -1 and 1")
576- }
577- c.Touchpad.AccelSpeed = s
578- case "touchpad.accel-profile":
579- if v != ports.AccelAdaptive && v != ports.AccelFlat {
580- return fmt.Errorf("must be adaptive or flat")
581- }
582- c.Touchpad.AccelProfile = v
583 case "touchpad.scroll-factor":567 case "touchpad.scroll-factor":
584 f, err := strconv.ParseFloat(v, 64)568 f, err := strconv.ParseFloat(v, 64)
585 if err != nil || !(f > 0 && f <= 10) {569 if err != nil || !(f > 0 && f <= 10) {
@@ -666,6 +650,12 @@ func set(c *ports.Config, key, v string) error {
666 }650 }
667 c.Log.Debug = list651 c.Log.Debug = list
668 default:652 default:
653+ if rest, ok := strings.CutPrefix(key, "touchpad."); ok {
654+ return parsePointerKey(rest, v, &c.Touchpad.PointerConfig)
655+ }
656+ if rest, ok := strings.CutPrefix(key, "mouse."); ok {
657+ return parsePointerKey(rest, v, &c.Mouse)
658+ }
669 if rest, ok := strings.CutPrefix(key, "layout."); ok {659 if rest, ok := strings.CutPrefix(key, "layout."); ok {
670 if i := strings.LastIndex(rest, "."); i > 0 {660 if i := strings.LastIndex(rest, "."); i > 0 {
671 return setOutputLayout(c, rest[:i], rest[i+1:], v)661 return setOutputLayout(c, rest[:i], rest[i+1:], v)
@@ -676,6 +666,40 @@ func set(c *ports.Config, key, v string) error {
676 return nil666 return nil
677 }667 }
678 668
669+// parsePointerKey applies one pointer key shared by touchpads and mice, with
670+// the touchpad. or mouse. prefix already removed. Touchpad-only keys are
671+// handled before; anything else is unknown. On an error p is left unchanged.
672+func parsePointerKey(key, v string, p *ports.PointerConfig) error {
673+ switch key {
674+ case "natural-scroll":
675+ b, err := onOff(v)
676+ if err != nil {
677+ return err
678+ }
679+ p.NaturalScroll = b
680+ case "left-handed":
681+ b, err := onOff(v)
682+ if err != nil {
683+ return err
684+ }
685+ p.LeftHanded = b
686+ case "accel-speed":
687+ s, err := strconv.ParseFloat(v, 64)
688+ if err != nil || !(s >= -1 && s <= 1) {
689+ return fmt.Errorf("must be between -1 and 1")
690+ }
691+ p.AccelSpeed = s
692+ case "accel-profile":
693+ if v != ports.AccelAdaptive && v != ports.AccelFlat {
694+ return fmt.Errorf("must be adaptive or flat")
695+ }
696+ p.AccelProfile = v
697+ default:
698+ return fmt.Errorf("unknown key")
699+ }
700+ return nil
701+}
702+
679 // setOutputLayout applies one layout.<output>.<field> key.703 // setOutputLayout applies one layout.<output>.<field> key.
680 func setOutputLayout(c *ports.Config, output, field, v string) error {704 func setOutputLayout(c *ports.Config, output, field, v string) error {
681 i := slices.IndexFunc(c.Layout.Outputs, func(o ports.OutputLayout) bool { return o.Output == output })705 i := slices.IndexFunc(c.Layout.Outputs, func(o ports.OutputLayout) bool { return o.Output == output })
M internal/adapters/config/config_test.go +56 -19
@@ -95,20 +95,6 @@ func TestFocusIndicator(t *testing.T) {
9595 }
9696 }
9797
98-func TestTouchpadNaturalScroll(t *testing.T) {
99- if Defaults().Touchpad.NaturalScroll {
100- t.Fatal("natural scroll on by default")
101- }
102- c, w := parseString(t, "touchpad.natural-scroll = on\n")
103- if !c.Touchpad.NaturalScroll || len(w) != 0 {
104- t.Fatal(c.Touchpad, w)
105- }
106- c, w = parseString(t, "touchpad.natural-scroll = maybe\n")
107- if c.Touchpad.NaturalScroll || len(w) != 1 {
108- t.Fatal(c.Touchpad, w)
109- }
110-}
111-
11298 func TestTouchpadTap(t *testing.T) {
11399 if !Defaults().Touchpad.Tap {
114100 t.Fatal("tap to click off by default")
@@ -128,14 +114,11 @@ func TestTouchpadSpeed(t *testing.T) {
128114 if d.AccelSpeed != 0 || d.AccelProfile != ports.AccelAdaptive || d.ScrollFactor != 1 {
129115 t.Fatal(d)
130116 }
131- c, w := parseString(t, "touchpad.accel-speed = -0.5\ntouchpad.accel-profile = flat\ntouchpad.scroll-factor = 0.5\n")
132- if c.Touchpad.AccelSpeed != -0.5 || c.Touchpad.AccelProfile != ports.AccelFlat || c.Touchpad.ScrollFactor != 0.5 || len(w) != 0 {
117+ c, w := parseString(t, "touchpad.scroll-factor = 0.5\n")
118+ if c.Touchpad.ScrollFactor != 0.5 || len(w) != 0 {
133119 t.Fatal(c.Touchpad, w)
134120 }
135121 for _, bad := range []string{
136- "touchpad.accel-speed = 2",
137- "touchpad.accel-speed = NaN",
138- "touchpad.accel-profile = fast",
139122 "touchpad.scroll-factor = 0",
140123 "touchpad.scroll-factor = 11",
141124 } {
@@ -146,6 +129,60 @@ func TestTouchpadSpeed(t *testing.T) {
146129 }
147130 }
148131
132+func TestPointerKeys(t *testing.T) {
133+ d := Defaults()
134+ if d.Mouse != (ports.PointerConfig{AccelProfile: ports.AccelAdaptive}) {
135+ t.Fatal(d.Mouse)
136+ }
137+ if d.Touchpad.LeftHanded || d.Touchpad.NaturalScroll {
138+ t.Fatal(d.Touchpad)
139+ }
140+ touchpad := func(c ports.Config) ports.PointerConfig { return c.Touchpad.PointerConfig }
141+ mouse := func(c ports.Config) ports.PointerConfig { return c.Mouse }
142+ for _, tc := range []struct {
143+ prefix string
144+ pointer, other func(ports.Config) ports.PointerConfig
145+ }{
146+ {"touchpad", touchpad, mouse},
147+ {"mouse", mouse, touchpad},
148+ } {
149+ t.Run(tc.prefix, func(t *testing.T) {
150+ prefix := tc.prefix
151+ c, w := parseString(t, prefix+".natural-scroll = on\n"+prefix+".left-handed = on\n"+prefix+".accel-speed = -0.5\n"+prefix+".accel-profile = flat\n")
152+ want := ports.PointerConfig{NaturalScroll: true, LeftHanded: true, AccelSpeed: -0.5, AccelProfile: ports.AccelFlat}
153+ if tc.pointer(c) != want || len(w) != 0 {
154+ t.Errorf("%+v %v", tc.pointer(c), w)
155+ }
156+ // The other device keeps its defaults.
157+ if other := tc.other(c); other != (ports.PointerConfig{AccelProfile: ports.AccelAdaptive}) {
158+ t.Errorf("leaked into the other device: %+v", other)
159+ }
160+ for _, bad := range []string{
161+ ".natural-scroll = maybe",
162+ ".left-handed = 1",
163+ ".accel-speed = 2",
164+ ".accel-speed = -1.5",
165+ ".accel-speed = NaN",
166+ ".accel-speed = fast",
167+ ".accel-profile = fast",
168+ ".accel-profile = ",
169+ ".unknown = on",
170+ } {
171+ c, w := parseString(t, prefix+bad+"\n")
172+ if c.Mouse != d.Mouse || c.Touchpad != d.Touchpad || len(w) != 1 {
173+ t.Errorf("%s%s: %+v %v", prefix, bad, tc.pointer(c), w)
174+ }
175+ }
176+ })
177+ }
178+ // Touchpad-only keys do not exist for mice.
179+ for _, bad := range []string{"mouse.tap = on", "mouse.scroll-factor = 2"} {
180+ if c, w := parseString(t, bad+"\n"); c.Mouse != d.Mouse || c.Touchpad != d.Touchpad || len(w) != 1 {
181+ t.Errorf("%s: %+v %v", bad, c.Mouse, w)
182+ }
183+ }
184+}
185+
149186 func TestParse(t *testing.T) {
150187 c, w := parseString(t, `
151188 # comment
@@ -95,20 +95,6 @@ func TestFocusIndicator(t *testing.T) {
95 }95 }
96 }96 }
97 97
98-func TestTouchpadNaturalScroll(t *testing.T) {
99- if Defaults().Touchpad.NaturalScroll {
100- t.Fatal("natural scroll on by default")
101- }
102- c, w := parseString(t, "touchpad.natural-scroll = on\n")
103- if !c.Touchpad.NaturalScroll || len(w) != 0 {
104- t.Fatal(c.Touchpad, w)
105- }
106- c, w = parseString(t, "touchpad.natural-scroll = maybe\n")
107- if c.Touchpad.NaturalScroll || len(w) != 1 {
108- t.Fatal(c.Touchpad, w)
109- }
110-}
111-
112 func TestTouchpadTap(t *testing.T) {98 func TestTouchpadTap(t *testing.T) {
113 if !Defaults().Touchpad.Tap {99 if !Defaults().Touchpad.Tap {
114 t.Fatal("tap to click off by default")100 t.Fatal("tap to click off by default")
@@ -128,14 +114,11 @@ func TestTouchpadSpeed(t *testing.T) {
128 if d.AccelSpeed != 0 || d.AccelProfile != ports.AccelAdaptive || d.ScrollFactor != 1 {114 if d.AccelSpeed != 0 || d.AccelProfile != ports.AccelAdaptive || d.ScrollFactor != 1 {
129 t.Fatal(d)115 t.Fatal(d)
130 }116 }
131- c, w := parseString(t, "touchpad.accel-speed = -0.5\ntouchpad.accel-profile = flat\ntouchpad.scroll-factor = 0.5\n")117+ c, w := parseString(t, "touchpad.scroll-factor = 0.5\n")
132- if c.Touchpad.AccelSpeed != -0.5 || c.Touchpad.AccelProfile != ports.AccelFlat || c.Touchpad.ScrollFactor != 0.5 || len(w) != 0 {118+ if c.Touchpad.ScrollFactor != 0.5 || len(w) != 0 {
133 t.Fatal(c.Touchpad, w)119 t.Fatal(c.Touchpad, w)
134 }120 }
135 for _, bad := range []string{121 for _, bad := range []string{
136- "touchpad.accel-speed = 2",
137- "touchpad.accel-speed = NaN",
138- "touchpad.accel-profile = fast",
139 "touchpad.scroll-factor = 0",122 "touchpad.scroll-factor = 0",
140 "touchpad.scroll-factor = 11",123 "touchpad.scroll-factor = 11",
141 } {124 } {
@@ -146,6 +129,60 @@ func TestTouchpadSpeed(t *testing.T) {
146 }129 }
147 }130 }
148 131
132+func TestPointerKeys(t *testing.T) {
133+ d := Defaults()
134+ if d.Mouse != (ports.PointerConfig{AccelProfile: ports.AccelAdaptive}) {
135+ t.Fatal(d.Mouse)
136+ }
137+ if d.Touchpad.LeftHanded || d.Touchpad.NaturalScroll {
138+ t.Fatal(d.Touchpad)
139+ }
140+ touchpad := func(c ports.Config) ports.PointerConfig { return c.Touchpad.PointerConfig }
141+ mouse := func(c ports.Config) ports.PointerConfig { return c.Mouse }
142+ for _, tc := range []struct {
143+ prefix string
144+ pointer, other func(ports.Config) ports.PointerConfig
145+ }{
146+ {"touchpad", touchpad, mouse},
147+ {"mouse", mouse, touchpad},
148+ } {
149+ t.Run(tc.prefix, func(t *testing.T) {
150+ prefix := tc.prefix
151+ c, w := parseString(t, prefix+".natural-scroll = on\n"+prefix+".left-handed = on\n"+prefix+".accel-speed = -0.5\n"+prefix+".accel-profile = flat\n")
152+ want := ports.PointerConfig{NaturalScroll: true, LeftHanded: true, AccelSpeed: -0.5, AccelProfile: ports.AccelFlat}
153+ if tc.pointer(c) != want || len(w) != 0 {
154+ t.Errorf("%+v %v", tc.pointer(c), w)
155+ }
156+ // The other device keeps its defaults.
157+ if other := tc.other(c); other != (ports.PointerConfig{AccelProfile: ports.AccelAdaptive}) {
158+ t.Errorf("leaked into the other device: %+v", other)
159+ }
160+ for _, bad := range []string{
161+ ".natural-scroll = maybe",
162+ ".left-handed = 1",
163+ ".accel-speed = 2",
164+ ".accel-speed = -1.5",
165+ ".accel-speed = NaN",
166+ ".accel-speed = fast",
167+ ".accel-profile = fast",
168+ ".accel-profile = ",
169+ ".unknown = on",
170+ } {
171+ c, w := parseString(t, prefix+bad+"\n")
172+ if c.Mouse != d.Mouse || c.Touchpad != d.Touchpad || len(w) != 1 {
173+ t.Errorf("%s%s: %+v %v", prefix, bad, tc.pointer(c), w)
174+ }
175+ }
176+ })
177+ }
178+ // Touchpad-only keys do not exist for mice.
179+ for _, bad := range []string{"mouse.tap = on", "mouse.scroll-factor = 2"} {
180+ if c, w := parseString(t, bad+"\n"); c.Mouse != d.Mouse || c.Touchpad != d.Touchpad || len(w) != 1 {
181+ t.Errorf("%s: %+v %v", bad, c.Mouse, w)
182+ }
183+ }
184+}
185+
149 func TestParse(t *testing.T) {186 func TestParse(t *testing.T) {
150 c, w := parseString(t, `187 c, w := parseString(t, `
151 # comment188 # comment
A internal/adapters/libinput/configure_test.go +113 -0
@@ -0,0 +1,113 @@
1+package libinput
2+
3+import (
4+ "bytes"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/zerowrap"
10+)
11+
12+const (
13+ testPad uintptr = 1
14+ testMouse uintptr = 2
15+ testKeyboard uintptr = 3
16+)
17+
18+// Every shared field differs between the two, so a mixed-up route fails.
19+var (
20+ padCfg = ports.TouchpadConfig{PointerConfig: ports.PointerConfig{NaturalScroll: true, AccelSpeed: 0.5, AccelProfile: ports.AccelFlat}, Tap: true}
21+ mouseCfg = ports.PointerConfig{AccelSpeed: -0.25, AccelProfile: ports.AccelAdaptive, LeftHanded: true}
22+)
23+
24+// newState returns an input state over a device config mock, and a logger
25+// writing to the returned buffer.
26+func newState(t *testing.T) (*inputState, *mockdeviceConfig, zerowrap.Logger, *bytes.Buffer) {
27+ t.Helper()
28+ dev := newMockdeviceConfig(t)
29+ buf := &bytes.Buffer{}
30+ log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: buf}).WithField("component", "input")
31+ return &inputState{cfg: ports.InputDevicesConfig{Touchpad: padCfg, Mouse: mouseCfg}, dev: dev}, dev, log, buf
32+}
33+
34+func TestConfigure(t *testing.T) {
35+ for _, tc := range []struct {
36+ name string
37+ dev uintptr
38+ expect func(m *mockdeviceConfig)
39+ }{
40+ {"touchpad", testPad, func(m *mockdeviceConfig) {
41+ m.EXPECT().IsTouchpad(testPad).Return(true)
42+ m.EXPECT().SetNaturalScroll(testPad, true).Return(nil).Once()
43+ m.EXPECT().SetAccel(testPad, 0.5, ports.AccelFlat).Return(nil).Once()
44+ m.EXPECT().SetLeftHanded(testPad, false).Return(nil).Once()
45+ m.EXPECT().SetTap(testPad, true).Return(nil).Once()
46+ }},
47+ // The touchpad settings must not reach the mouse, and a mouse is
48+ // never given tapping: SetTap has no expectation.
49+ {"mouse", testMouse, func(m *mockdeviceConfig) {
50+ m.EXPECT().IsTouchpad(testMouse).Return(false)
51+ m.EXPECT().IsPointer(testMouse).Return(true)
52+ m.EXPECT().SetNaturalScroll(testMouse, false).Return(nil).Once()
53+ m.EXPECT().SetAccel(testMouse, -0.25, ports.AccelAdaptive).Return(nil).Once()
54+ m.EXPECT().SetLeftHanded(testMouse, true).Return(nil).Once()
55+ }},
56+ {"keyboard", testKeyboard, func(m *mockdeviceConfig) {
57+ m.EXPECT().IsTouchpad(testKeyboard).Return(false)
58+ m.EXPECT().IsPointer(testKeyboard).Return(false)
59+ }},
60+ } {
61+ t.Run(tc.name, func(t *testing.T) {
62+ in, dev, log, buf := newState(t)
63+ tc.expect(dev)
64+ in.configure(tc.dev, log)
65+ if buf.Len() != 0 {
66+ t.Fatalf("logged: %s", buf)
67+ }
68+ })
69+ }
70+}
71+
72+func TestConfigurable(t *testing.T) {
73+ for _, tc := range []struct {
74+ name string
75+ touchpad, ptr bool
76+ want, askedPtr bool
77+ }{
78+ {"touchpad", true, false, true, false},
79+ {"mouse", false, true, true, true},
80+ {"keyboard", false, false, false, true},
81+ } {
82+ t.Run(tc.name, func(t *testing.T) {
83+ in, dev, _, _ := newState(t)
84+ dev.EXPECT().IsTouchpad(testPad).Return(tc.touchpad)
85+ if tc.askedPtr {
86+ dev.EXPECT().IsPointer(testPad).Return(tc.ptr)
87+ }
88+ if got := in.configurable(testPad); got != tc.want {
89+ t.Fatalf("configurable = %v, want %v", got, tc.want)
90+ }
91+ })
92+ }
93+}
94+
95+func TestConfigureRejectedSettingLogsAndContinues(t *testing.T) {
96+ in, dev, log, buf := newState(t)
97+ dev.EXPECT().IsTouchpad(testPad).Return(true)
98+ dev.EXPECT().SetNaturalScroll(testPad, true).Return(errRejected).Once()
99+ dev.EXPECT().Name(testPad).Return("Synaptics").Once()
100+ dev.EXPECT().SetAccel(testPad, 0.5, ports.AccelFlat).Return(nil).Once()
101+ dev.EXPECT().SetLeftHanded(testPad, false).Return(nil).Once()
102+ dev.EXPECT().SetTap(testPad, true).Return(nil).Once()
103+ in.configure(testPad, log)
104+ out := buf.String()
105+ for _, want := range []string{`"device":"Synaptics"`, `"setting":`, `"component":"input"`, `"level":"warn"`} {
106+ if !strings.Contains(out, want) {
107+ t.Errorf("log %s lacks %s", out, want)
108+ }
109+ }
110+ if n := strings.Count(out, "\n"); n != 1 {
111+ t.Errorf("%d log lines, want 1: %s", n, out)
112+ }
113+}
@@ -0,0 +1,113 @@
1+package libinput
2+
3+import (
4+ "bytes"
5+ "strings"
6+ "testing"
7+
8+ "github.com/bnema/neferwl/internal/ports"
9+ "github.com/bnema/zerowrap"
10+)
11+
12+const (
13+ testPad uintptr = 1
14+ testMouse uintptr = 2
15+ testKeyboard uintptr = 3
16+)
17+
18+// Every shared field differs between the two, so a mixed-up route fails.
19+var (
20+ padCfg = ports.TouchpadConfig{PointerConfig: ports.PointerConfig{NaturalScroll: true, AccelSpeed: 0.5, AccelProfile: ports.AccelFlat}, Tap: true}
21+ mouseCfg = ports.PointerConfig{AccelSpeed: -0.25, AccelProfile: ports.AccelAdaptive, LeftHanded: true}
22+)
23+
24+// newState returns an input state over a device config mock, and a logger
25+// writing to the returned buffer.
26+func newState(t *testing.T) (*inputState, *mockdeviceConfig, zerowrap.Logger, *bytes.Buffer) {
27+ t.Helper()
28+ dev := newMockdeviceConfig(t)
29+ buf := &bytes.Buffer{}
30+ log := zerowrap.New(zerowrap.Config{Level: "debug", Format: "json", Output: buf}).WithField("component", "input")
31+ return &inputState{cfg: ports.InputDevicesConfig{Touchpad: padCfg, Mouse: mouseCfg}, dev: dev}, dev, log, buf
32+}
33+
34+func TestConfigure(t *testing.T) {
35+ for _, tc := range []struct {
36+ name string
37+ dev uintptr
38+ expect func(m *mockdeviceConfig)
39+ }{
40+ {"touchpad", testPad, func(m *mockdeviceConfig) {
41+ m.EXPECT().IsTouchpad(testPad).Return(true)
42+ m.EXPECT().SetNaturalScroll(testPad, true).Return(nil).Once()
43+ m.EXPECT().SetAccel(testPad, 0.5, ports.AccelFlat).Return(nil).Once()
44+ m.EXPECT().SetLeftHanded(testPad, false).Return(nil).Once()
45+ m.EXPECT().SetTap(testPad, true).Return(nil).Once()
46+ }},
47+ // The touchpad settings must not reach the mouse, and a mouse is
48+ // never given tapping: SetTap has no expectation.
49+ {"mouse", testMouse, func(m *mockdeviceConfig) {
50+ m.EXPECT().IsTouchpad(testMouse).Return(false)
51+ m.EXPECT().IsPointer(testMouse).Return(true)
52+ m.EXPECT().SetNaturalScroll(testMouse, false).Return(nil).Once()
53+ m.EXPECT().SetAccel(testMouse, -0.25, ports.AccelAdaptive).Return(nil).Once()
54+ m.EXPECT().SetLeftHanded(testMouse, true).Return(nil).Once()
55+ }},
56+ {"keyboard", testKeyboard, func(m *mockdeviceConfig) {
57+ m.EXPECT().IsTouchpad(testKeyboard).Return(false)
58+ m.EXPECT().IsPointer(testKeyboard).Return(false)
59+ }},
60+ } {
61+ t.Run(tc.name, func(t *testing.T) {
62+ in, dev, log, buf := newState(t)
63+ tc.expect(dev)
64+ in.configure(tc.dev, log)
65+ if buf.Len() != 0 {
66+ t.Fatalf("logged: %s", buf)
67+ }
68+ })
69+ }
70+}
71+
72+func TestConfigurable(t *testing.T) {
73+ for _, tc := range []struct {
74+ name string
75+ touchpad, ptr bool
76+ want, askedPtr bool
77+ }{
78+ {"touchpad", true, false, true, false},
79+ {"mouse", false, true, true, true},
80+ {"keyboard", false, false, false, true},
81+ } {
82+ t.Run(tc.name, func(t *testing.T) {
83+ in, dev, _, _ := newState(t)
84+ dev.EXPECT().IsTouchpad(testPad).Return(tc.touchpad)
85+ if tc.askedPtr {
86+ dev.EXPECT().IsPointer(testPad).Return(tc.ptr)
87+ }
88+ if got := in.configurable(testPad); got != tc.want {
89+ t.Fatalf("configurable = %v, want %v", got, tc.want)
90+ }
91+ })
92+ }
93+}
94+
95+func TestConfigureRejectedSettingLogsAndContinues(t *testing.T) {
96+ in, dev, log, buf := newState(t)
97+ dev.EXPECT().IsTouchpad(testPad).Return(true)
98+ dev.EXPECT().SetNaturalScroll(testPad, true).Return(errRejected).Once()
99+ dev.EXPECT().Name(testPad).Return("Synaptics").Once()
100+ dev.EXPECT().SetAccel(testPad, 0.5, ports.AccelFlat).Return(nil).Once()
101+ dev.EXPECT().SetLeftHanded(testPad, false).Return(nil).Once()
102+ dev.EXPECT().SetTap(testPad, true).Return(nil).Once()
103+ in.configure(testPad, log)
104+ out := buf.String()
105+ for _, want := range []string{`"device":"Synaptics"`, `"setting":`, `"component":"input"`, `"level":"warn"`} {
106+ if !strings.Contains(out, want) {
107+ t.Errorf("log %s lacks %s", out, want)
108+ }
109+ }
110+ if n := strings.Count(out, "\n"); n != 1 {
111+ t.Errorf("%d log lines, want 1: %s", n, out)
112+ }
113+}
A internal/adapters/libinput/device_config_mock_test.go +432 -0
@@ -0,0 +1,432 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package libinput
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockdeviceConfig creates a new instance of mockdeviceConfig. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockdeviceConfig(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockdeviceConfig {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockdeviceConfig{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockdeviceConfig is an autogenerated mock type for the deviceConfig type
35+type mockdeviceConfig struct {
36+ mock.Mock
37+}
38+
39+type mockdeviceConfig_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockdeviceConfig) EXPECT() *mockdeviceConfig_Expecter {
44+ return &mockdeviceConfig_Expecter{mock: &_m.Mock}
45+}
46+
47+// IsPointer provides a mock function for the type mockdeviceConfig
48+func (_mock *mockdeviceConfig) IsPointer(dev uintptr) bool {
49+ ret := _mock.Called(dev)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for IsPointer")
53+ }
54+
55+ var r0 bool
56+ if returnFunc, ok := ret.Get(0).(func(uintptr) bool); ok {
57+ r0 = returnFunc(dev)
58+ } else {
59+ r0 = ret.Get(0).(bool)
60+ }
61+ return r0
62+}
63+
64+// mockdeviceConfig_IsPointer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsPointer'
65+type mockdeviceConfig_IsPointer_Call struct {
66+ *mock.Call
67+}
68+
69+// IsPointer is a helper method to define mock.On call
70+// - dev uintptr
71+func (_e *mockdeviceConfig_Expecter) IsPointer(dev any) *mockdeviceConfig_IsPointer_Call {
72+ return &mockdeviceConfig_IsPointer_Call{Call: _e.mock.On("IsPointer", dev)}
73+}
74+
75+func (_c *mockdeviceConfig_IsPointer_Call) Run(run func(dev uintptr)) *mockdeviceConfig_IsPointer_Call {
76+ _c.Call.Run(func(args mock.Arguments) {
77+ var arg0 uintptr
78+ if args[0] != nil {
79+ arg0 = args[0].(uintptr)
80+ }
81+ run(
82+ arg0,
83+ )
84+ })
85+ return _c
86+}
87+
88+func (_c *mockdeviceConfig_IsPointer_Call) Return(b bool) *mockdeviceConfig_IsPointer_Call {
89+ _c.Call.Return(b)
90+ return _c
91+}
92+
93+func (_c *mockdeviceConfig_IsPointer_Call) RunAndReturn(run func(dev uintptr) bool) *mockdeviceConfig_IsPointer_Call {
94+ _c.Call.Return(run)
95+ return _c
96+}
97+
98+// IsTouchpad provides a mock function for the type mockdeviceConfig
99+func (_mock *mockdeviceConfig) IsTouchpad(dev uintptr) bool {
100+ ret := _mock.Called(dev)
101+
102+ if len(ret) == 0 {
103+ panic("no return value specified for IsTouchpad")
104+ }
105+
106+ var r0 bool
107+ if returnFunc, ok := ret.Get(0).(func(uintptr) bool); ok {
108+ r0 = returnFunc(dev)
109+ } else {
110+ r0 = ret.Get(0).(bool)
111+ }
112+ return r0
113+}
114+
115+// mockdeviceConfig_IsTouchpad_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsTouchpad'
116+type mockdeviceConfig_IsTouchpad_Call struct {
117+ *mock.Call
118+}
119+
120+// IsTouchpad is a helper method to define mock.On call
121+// - dev uintptr
122+func (_e *mockdeviceConfig_Expecter) IsTouchpad(dev any) *mockdeviceConfig_IsTouchpad_Call {
123+ return &mockdeviceConfig_IsTouchpad_Call{Call: _e.mock.On("IsTouchpad", dev)}
124+}
125+
126+func (_c *mockdeviceConfig_IsTouchpad_Call) Run(run func(dev uintptr)) *mockdeviceConfig_IsTouchpad_Call {
127+ _c.Call.Run(func(args mock.Arguments) {
128+ var arg0 uintptr
129+ if args[0] != nil {
130+ arg0 = args[0].(uintptr)
131+ }
132+ run(
133+ arg0,
134+ )
135+ })
136+ return _c
137+}
138+
139+func (_c *mockdeviceConfig_IsTouchpad_Call) Return(b bool) *mockdeviceConfig_IsTouchpad_Call {
140+ _c.Call.Return(b)
141+ return _c
142+}
143+
144+func (_c *mockdeviceConfig_IsTouchpad_Call) RunAndReturn(run func(dev uintptr) bool) *mockdeviceConfig_IsTouchpad_Call {
145+ _c.Call.Return(run)
146+ return _c
147+}
148+
149+// Name provides a mock function for the type mockdeviceConfig
150+func (_mock *mockdeviceConfig) Name(dev uintptr) string {
151+ ret := _mock.Called(dev)
152+
153+ if len(ret) == 0 {
154+ panic("no return value specified for Name")
155+ }
156+
157+ var r0 string
158+ if returnFunc, ok := ret.Get(0).(func(uintptr) string); ok {
159+ r0 = returnFunc(dev)
160+ } else {
161+ r0 = ret.Get(0).(string)
162+ }
163+ return r0
164+}
165+
166+// mockdeviceConfig_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
167+type mockdeviceConfig_Name_Call struct {
168+ *mock.Call
169+}
170+
171+// Name is a helper method to define mock.On call
172+// - dev uintptr
173+func (_e *mockdeviceConfig_Expecter) Name(dev any) *mockdeviceConfig_Name_Call {
174+ return &mockdeviceConfig_Name_Call{Call: _e.mock.On("Name", dev)}
175+}
176+
177+func (_c *mockdeviceConfig_Name_Call) Run(run func(dev uintptr)) *mockdeviceConfig_Name_Call {
178+ _c.Call.Run(func(args mock.Arguments) {
179+ var arg0 uintptr
180+ if args[0] != nil {
181+ arg0 = args[0].(uintptr)
182+ }
183+ run(
184+ arg0,
185+ )
186+ })
187+ return _c
188+}
189+
190+func (_c *mockdeviceConfig_Name_Call) Return(s string) *mockdeviceConfig_Name_Call {
191+ _c.Call.Return(s)
192+ return _c
193+}
194+
195+func (_c *mockdeviceConfig_Name_Call) RunAndReturn(run func(dev uintptr) string) *mockdeviceConfig_Name_Call {
196+ _c.Call.Return(run)
197+ return _c
198+}
199+
200+// SetAccel provides a mock function for the type mockdeviceConfig
201+func (_mock *mockdeviceConfig) SetAccel(dev uintptr, speed float64, profile string) error {
202+ ret := _mock.Called(dev, speed, profile)
203+
204+ if len(ret) == 0 {
205+ panic("no return value specified for SetAccel")
206+ }
207+
208+ var r0 error
209+ if returnFunc, ok := ret.Get(0).(func(uintptr, float64, string) error); ok {
210+ r0 = returnFunc(dev, speed, profile)
211+ } else {
212+ r0 = ret.Error(0)
213+ }
214+ return r0
215+}
216+
217+// mockdeviceConfig_SetAccel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAccel'
218+type mockdeviceConfig_SetAccel_Call struct {
219+ *mock.Call
220+}
221+
222+// SetAccel is a helper method to define mock.On call
223+// - dev uintptr
224+// - speed float64
225+// - profile string
226+func (_e *mockdeviceConfig_Expecter) SetAccel(dev any, speed any, profile any) *mockdeviceConfig_SetAccel_Call {
227+ return &mockdeviceConfig_SetAccel_Call{Call: _e.mock.On("SetAccel", dev, speed, profile)}
228+}
229+
230+func (_c *mockdeviceConfig_SetAccel_Call) Run(run func(dev uintptr, speed float64, profile string)) *mockdeviceConfig_SetAccel_Call {
231+ _c.Call.Run(func(args mock.Arguments) {
232+ var arg0 uintptr
233+ if args[0] != nil {
234+ arg0 = args[0].(uintptr)
235+ }
236+ var arg1 float64
237+ if args[1] != nil {
238+ arg1 = args[1].(float64)
239+ }
240+ var arg2 string
241+ if args[2] != nil {
242+ arg2 = args[2].(string)
243+ }
244+ run(
245+ arg0,
246+ arg1,
247+ arg2,
248+ )
249+ })
250+ return _c
251+}
252+
253+func (_c *mockdeviceConfig_SetAccel_Call) Return(err error) *mockdeviceConfig_SetAccel_Call {
254+ _c.Call.Return(err)
255+ return _c
256+}
257+
258+func (_c *mockdeviceConfig_SetAccel_Call) RunAndReturn(run func(dev uintptr, speed float64, profile string) error) *mockdeviceConfig_SetAccel_Call {
259+ _c.Call.Return(run)
260+ return _c
261+}
262+
263+// SetLeftHanded provides a mock function for the type mockdeviceConfig
264+func (_mock *mockdeviceConfig) SetLeftHanded(dev uintptr, on bool) error {
265+ ret := _mock.Called(dev, on)
266+
267+ if len(ret) == 0 {
268+ panic("no return value specified for SetLeftHanded")
269+ }
270+
271+ var r0 error
272+ if returnFunc, ok := ret.Get(0).(func(uintptr, bool) error); ok {
273+ r0 = returnFunc(dev, on)
274+ } else {
275+ r0 = ret.Error(0)
276+ }
277+ return r0
278+}
279+
280+// mockdeviceConfig_SetLeftHanded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetLeftHanded'
281+type mockdeviceConfig_SetLeftHanded_Call struct {
282+ *mock.Call
283+}
284+
285+// SetLeftHanded is a helper method to define mock.On call
286+// - dev uintptr
287+// - on bool
288+func (_e *mockdeviceConfig_Expecter) SetLeftHanded(dev any, on any) *mockdeviceConfig_SetLeftHanded_Call {
289+ return &mockdeviceConfig_SetLeftHanded_Call{Call: _e.mock.On("SetLeftHanded", dev, on)}
290+}
291+
292+func (_c *mockdeviceConfig_SetLeftHanded_Call) Run(run func(dev uintptr, on bool)) *mockdeviceConfig_SetLeftHanded_Call {
293+ _c.Call.Run(func(args mock.Arguments) {
294+ var arg0 uintptr
295+ if args[0] != nil {
296+ arg0 = args[0].(uintptr)
297+ }
298+ var arg1 bool
299+ if args[1] != nil {
300+ arg1 = args[1].(bool)
301+ }
302+ run(
303+ arg0,
304+ arg1,
305+ )
306+ })
307+ return _c
308+}
309+
310+func (_c *mockdeviceConfig_SetLeftHanded_Call) Return(err error) *mockdeviceConfig_SetLeftHanded_Call {
311+ _c.Call.Return(err)
312+ return _c
313+}
314+
315+func (_c *mockdeviceConfig_SetLeftHanded_Call) RunAndReturn(run func(dev uintptr, on bool) error) *mockdeviceConfig_SetLeftHanded_Call {
316+ _c.Call.Return(run)
317+ return _c
318+}
319+
320+// SetNaturalScroll provides a mock function for the type mockdeviceConfig
321+func (_mock *mockdeviceConfig) SetNaturalScroll(dev uintptr, on bool) error {
322+ ret := _mock.Called(dev, on)
323+
324+ if len(ret) == 0 {
325+ panic("no return value specified for SetNaturalScroll")
326+ }
327+
328+ var r0 error
329+ if returnFunc, ok := ret.Get(0).(func(uintptr, bool) error); ok {
330+ r0 = returnFunc(dev, on)
331+ } else {
332+ r0 = ret.Error(0)
333+ }
334+ return r0
335+}
336+
337+// mockdeviceConfig_SetNaturalScroll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetNaturalScroll'
338+type mockdeviceConfig_SetNaturalScroll_Call struct {
339+ *mock.Call
340+}
341+
342+// SetNaturalScroll is a helper method to define mock.On call
343+// - dev uintptr
344+// - on bool
345+func (_e *mockdeviceConfig_Expecter) SetNaturalScroll(dev any, on any) *mockdeviceConfig_SetNaturalScroll_Call {
346+ return &mockdeviceConfig_SetNaturalScroll_Call{Call: _e.mock.On("SetNaturalScroll", dev, on)}
347+}
348+
349+func (_c *mockdeviceConfig_SetNaturalScroll_Call) Run(run func(dev uintptr, on bool)) *mockdeviceConfig_SetNaturalScroll_Call {
350+ _c.Call.Run(func(args mock.Arguments) {
351+ var arg0 uintptr
352+ if args[0] != nil {
353+ arg0 = args[0].(uintptr)
354+ }
355+ var arg1 bool
356+ if args[1] != nil {
357+ arg1 = args[1].(bool)
358+ }
359+ run(
360+ arg0,
361+ arg1,
362+ )
363+ })
364+ return _c
365+}
366+
367+func (_c *mockdeviceConfig_SetNaturalScroll_Call) Return(err error) *mockdeviceConfig_SetNaturalScroll_Call {
368+ _c.Call.Return(err)
369+ return _c
370+}
371+
372+func (_c *mockdeviceConfig_SetNaturalScroll_Call) RunAndReturn(run func(dev uintptr, on bool) error) *mockdeviceConfig_SetNaturalScroll_Call {
373+ _c.Call.Return(run)
374+ return _c
375+}
376+
377+// SetTap provides a mock function for the type mockdeviceConfig
378+func (_mock *mockdeviceConfig) SetTap(dev uintptr, on bool) error {
379+ ret := _mock.Called(dev, on)
380+
381+ if len(ret) == 0 {
382+ panic("no return value specified for SetTap")
383+ }
384+
385+ var r0 error
386+ if returnFunc, ok := ret.Get(0).(func(uintptr, bool) error); ok {
387+ r0 = returnFunc(dev, on)
388+ } else {
389+ r0 = ret.Error(0)
390+ }
391+ return r0
392+}
393+
394+// mockdeviceConfig_SetTap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTap'
395+type mockdeviceConfig_SetTap_Call struct {
396+ *mock.Call
397+}
398+
399+// SetTap is a helper method to define mock.On call
400+// - dev uintptr
401+// - on bool
402+func (_e *mockdeviceConfig_Expecter) SetTap(dev any, on any) *mockdeviceConfig_SetTap_Call {
403+ return &mockdeviceConfig_SetTap_Call{Call: _e.mock.On("SetTap", dev, on)}
404+}
405+
406+func (_c *mockdeviceConfig_SetTap_Call) Run(run func(dev uintptr, on bool)) *mockdeviceConfig_SetTap_Call {
407+ _c.Call.Run(func(args mock.Arguments) {
408+ var arg0 uintptr
409+ if args[0] != nil {
410+ arg0 = args[0].(uintptr)
411+ }
412+ var arg1 bool
413+ if args[1] != nil {
414+ arg1 = args[1].(bool)
415+ }
416+ run(
417+ arg0,
418+ arg1,
419+ )
420+ })
421+ return _c
422+}
423+
424+func (_c *mockdeviceConfig_SetTap_Call) Return(err error) *mockdeviceConfig_SetTap_Call {
425+ _c.Call.Return(err)
426+ return _c
427+}
428+
429+func (_c *mockdeviceConfig_SetTap_Call) RunAndReturn(run func(dev uintptr, on bool) error) *mockdeviceConfig_SetTap_Call {
430+ _c.Call.Return(run)
431+ return _c
432+}
@@ -0,0 +1,432 @@
1+// Code generated by mockery; DO NOT EDIT.
2+// github.com/vektra/mockery
3+// template: testify
4+
5+package libinput
6+
7+import (
8+ mock "github.com/stretchr/testify/mock"
9+)
10+
11+// newMockdeviceConfig creates a new instance of mockdeviceConfig. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
12+// The first argument is typically a *testing.T value.
13+func newMockdeviceConfig(t interface {
14+ mock.TestingT
15+ Cleanup(func())
16+}) *mockdeviceConfig {
17+ if helper, ok := t.(interface{ Helper() }); ok {
18+ helper.Helper()
19+ }
20+
21+ mock := &mockdeviceConfig{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() {
25+ if helper, ok := t.(interface{ Helper() }); ok {
26+ helper.Helper()
27+ }
28+ mock.AssertExpectations(t)
29+ })
30+
31+ return mock
32+}
33+
34+// mockdeviceConfig is an autogenerated mock type for the deviceConfig type
35+type mockdeviceConfig struct {
36+ mock.Mock
37+}
38+
39+type mockdeviceConfig_Expecter struct {
40+ mock *mock.Mock
41+}
42+
43+func (_m *mockdeviceConfig) EXPECT() *mockdeviceConfig_Expecter {
44+ return &mockdeviceConfig_Expecter{mock: &_m.Mock}
45+}
46+
47+// IsPointer provides a mock function for the type mockdeviceConfig
48+func (_mock *mockdeviceConfig) IsPointer(dev uintptr) bool {
49+ ret := _mock.Called(dev)
50+
51+ if len(ret) == 0 {
52+ panic("no return value specified for IsPointer")
53+ }
54+
55+ var r0 bool
56+ if returnFunc, ok := ret.Get(0).(func(uintptr) bool); ok {
57+ r0 = returnFunc(dev)
58+ } else {
59+ r0 = ret.Get(0).(bool)
60+ }
61+ return r0
62+}
63+
64+// mockdeviceConfig_IsPointer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsPointer'
65+type mockdeviceConfig_IsPointer_Call struct {
66+ *mock.Call
67+}
68+
69+// IsPointer is a helper method to define mock.On call
70+// - dev uintptr
71+func (_e *mockdeviceConfig_Expecter) IsPointer(dev any) *mockdeviceConfig_IsPointer_Call {
72+ return &mockdeviceConfig_IsPointer_Call{Call: _e.mock.On("IsPointer", dev)}
73+}
74+
75+func (_c *mockdeviceConfig_IsPointer_Call) Run(run func(dev uintptr)) *mockdeviceConfig_IsPointer_Call {
76+ _c.Call.Run(func(args mock.Arguments) {
77+ var arg0 uintptr
78+ if args[0] != nil {
79+ arg0 = args[0].(uintptr)
80+ }
81+ run(
82+ arg0,
83+ )
84+ })
85+ return _c
86+}
87+
88+func (_c *mockdeviceConfig_IsPointer_Call) Return(b bool) *mockdeviceConfig_IsPointer_Call {
89+ _c.Call.Return(b)
90+ return _c
91+}
92+
93+func (_c *mockdeviceConfig_IsPointer_Call) RunAndReturn(run func(dev uintptr) bool) *mockdeviceConfig_IsPointer_Call {
94+ _c.Call.Return(run)
95+ return _c
96+}
97+
98+// IsTouchpad provides a mock function for the type mockdeviceConfig
99+func (_mock *mockdeviceConfig) IsTouchpad(dev uintptr) bool {
100+ ret := _mock.Called(dev)
101+
102+ if len(ret) == 0 {
103+ panic("no return value specified for IsTouchpad")
104+ }
105+
106+ var r0 bool
107+ if returnFunc, ok := ret.Get(0).(func(uintptr) bool); ok {
108+ r0 = returnFunc(dev)
109+ } else {
110+ r0 = ret.Get(0).(bool)
111+ }
112+ return r0
113+}
114+
115+// mockdeviceConfig_IsTouchpad_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsTouchpad'
116+type mockdeviceConfig_IsTouchpad_Call struct {
117+ *mock.Call
118+}
119+
120+// IsTouchpad is a helper method to define mock.On call
121+// - dev uintptr
122+func (_e *mockdeviceConfig_Expecter) IsTouchpad(dev any) *mockdeviceConfig_IsTouchpad_Call {
123+ return &mockdeviceConfig_IsTouchpad_Call{Call: _e.mock.On("IsTouchpad", dev)}
124+}
125+
126+func (_c *mockdeviceConfig_IsTouchpad_Call) Run(run func(dev uintptr)) *mockdeviceConfig_IsTouchpad_Call {
127+ _c.Call.Run(func(args mock.Arguments) {
128+ var arg0 uintptr
129+ if args[0] != nil {
130+ arg0 = args[0].(uintptr)
131+ }
132+ run(
133+ arg0,
134+ )
135+ })
136+ return _c
137+}
138+
139+func (_c *mockdeviceConfig_IsTouchpad_Call) Return(b bool) *mockdeviceConfig_IsTouchpad_Call {
140+ _c.Call.Return(b)
141+ return _c
142+}
143+
144+func (_c *mockdeviceConfig_IsTouchpad_Call) RunAndReturn(run func(dev uintptr) bool) *mockdeviceConfig_IsTouchpad_Call {
145+ _c.Call.Return(run)
146+ return _c
147+}
148+
149+// Name provides a mock function for the type mockdeviceConfig
150+func (_mock *mockdeviceConfig) Name(dev uintptr) string {
151+ ret := _mock.Called(dev)
152+
153+ if len(ret) == 0 {
154+ panic("no return value specified for Name")
155+ }
156+
157+ var r0 string
158+ if returnFunc, ok := ret.Get(0).(func(uintptr) string); ok {
159+ r0 = returnFunc(dev)
160+ } else {
161+ r0 = ret.Get(0).(string)
162+ }
163+ return r0
164+}
165+
166+// mockdeviceConfig_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name'
167+type mockdeviceConfig_Name_Call struct {
168+ *mock.Call
169+}
170+
171+// Name is a helper method to define mock.On call
172+// - dev uintptr
173+func (_e *mockdeviceConfig_Expecter) Name(dev any) *mockdeviceConfig_Name_Call {
174+ return &mockdeviceConfig_Name_Call{Call: _e.mock.On("Name", dev)}
175+}
176+
177+func (_c *mockdeviceConfig_Name_Call) Run(run func(dev uintptr)) *mockdeviceConfig_Name_Call {
178+ _c.Call.Run(func(args mock.Arguments) {
179+ var arg0 uintptr
180+ if args[0] != nil {
181+ arg0 = args[0].(uintptr)
182+ }
183+ run(
184+ arg0,
185+ )
186+ })
187+ return _c
188+}
189+
190+func (_c *mockdeviceConfig_Name_Call) Return(s string) *mockdeviceConfig_Name_Call {
191+ _c.Call.Return(s)
192+ return _c
193+}
194+
195+func (_c *mockdeviceConfig_Name_Call) RunAndReturn(run func(dev uintptr) string) *mockdeviceConfig_Name_Call {
196+ _c.Call.Return(run)
197+ return _c
198+}
199+
200+// SetAccel provides a mock function for the type mockdeviceConfig
201+func (_mock *mockdeviceConfig) SetAccel(dev uintptr, speed float64, profile string) error {
202+ ret := _mock.Called(dev, speed, profile)
203+
204+ if len(ret) == 0 {
205+ panic("no return value specified for SetAccel")
206+ }
207+
208+ var r0 error
209+ if returnFunc, ok := ret.Get(0).(func(uintptr, float64, string) error); ok {
210+ r0 = returnFunc(dev, speed, profile)
211+ } else {
212+ r0 = ret.Error(0)
213+ }
214+ return r0
215+}
216+
217+// mockdeviceConfig_SetAccel_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetAccel'
218+type mockdeviceConfig_SetAccel_Call struct {
219+ *mock.Call
220+}
221+
222+// SetAccel is a helper method to define mock.On call
223+// - dev uintptr
224+// - speed float64
225+// - profile string
226+func (_e *mockdeviceConfig_Expecter) SetAccel(dev any, speed any, profile any) *mockdeviceConfig_SetAccel_Call {
227+ return &mockdeviceConfig_SetAccel_Call{Call: _e.mock.On("SetAccel", dev, speed, profile)}
228+}
229+
230+func (_c *mockdeviceConfig_SetAccel_Call) Run(run func(dev uintptr, speed float64, profile string)) *mockdeviceConfig_SetAccel_Call {
231+ _c.Call.Run(func(args mock.Arguments) {
232+ var arg0 uintptr
233+ if args[0] != nil {
234+ arg0 = args[0].(uintptr)
235+ }
236+ var arg1 float64
237+ if args[1] != nil {
238+ arg1 = args[1].(float64)
239+ }
240+ var arg2 string
241+ if args[2] != nil {
242+ arg2 = args[2].(string)
243+ }
244+ run(
245+ arg0,
246+ arg1,
247+ arg2,
248+ )
249+ })
250+ return _c
251+}
252+
253+func (_c *mockdeviceConfig_SetAccel_Call) Return(err error) *mockdeviceConfig_SetAccel_Call {
254+ _c.Call.Return(err)
255+ return _c
256+}
257+
258+func (_c *mockdeviceConfig_SetAccel_Call) RunAndReturn(run func(dev uintptr, speed float64, profile string) error) *mockdeviceConfig_SetAccel_Call {
259+ _c.Call.Return(run)
260+ return _c
261+}
262+
263+// SetLeftHanded provides a mock function for the type mockdeviceConfig
264+func (_mock *mockdeviceConfig) SetLeftHanded(dev uintptr, on bool) error {
265+ ret := _mock.Called(dev, on)
266+
267+ if len(ret) == 0 {
268+ panic("no return value specified for SetLeftHanded")
269+ }
270+
271+ var r0 error
272+ if returnFunc, ok := ret.Get(0).(func(uintptr, bool) error); ok {
273+ r0 = returnFunc(dev, on)
274+ } else {
275+ r0 = ret.Error(0)
276+ }
277+ return r0
278+}
279+
280+// mockdeviceConfig_SetLeftHanded_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetLeftHanded'
281+type mockdeviceConfig_SetLeftHanded_Call struct {
282+ *mock.Call
283+}
284+
285+// SetLeftHanded is a helper method to define mock.On call
286+// - dev uintptr
287+// - on bool
288+func (_e *mockdeviceConfig_Expecter) SetLeftHanded(dev any, on any) *mockdeviceConfig_SetLeftHanded_Call {
289+ return &mockdeviceConfig_SetLeftHanded_Call{Call: _e.mock.On("SetLeftHanded", dev, on)}
290+}
291+
292+func (_c *mockdeviceConfig_SetLeftHanded_Call) Run(run func(dev uintptr, on bool)) *mockdeviceConfig_SetLeftHanded_Call {
293+ _c.Call.Run(func(args mock.Arguments) {
294+ var arg0 uintptr
295+ if args[0] != nil {
296+ arg0 = args[0].(uintptr)
297+ }
298+ var arg1 bool
299+ if args[1] != nil {
300+ arg1 = args[1].(bool)
301+ }
302+ run(
303+ arg0,
304+ arg1,
305+ )
306+ })
307+ return _c
308+}
309+
310+func (_c *mockdeviceConfig_SetLeftHanded_Call) Return(err error) *mockdeviceConfig_SetLeftHanded_Call {
311+ _c.Call.Return(err)
312+ return _c
313+}
314+
315+func (_c *mockdeviceConfig_SetLeftHanded_Call) RunAndReturn(run func(dev uintptr, on bool) error) *mockdeviceConfig_SetLeftHanded_Call {
316+ _c.Call.Return(run)
317+ return _c
318+}
319+
320+// SetNaturalScroll provides a mock function for the type mockdeviceConfig
321+func (_mock *mockdeviceConfig) SetNaturalScroll(dev uintptr, on bool) error {
322+ ret := _mock.Called(dev, on)
323+
324+ if len(ret) == 0 {
325+ panic("no return value specified for SetNaturalScroll")
326+ }
327+
328+ var r0 error
329+ if returnFunc, ok := ret.Get(0).(func(uintptr, bool) error); ok {
330+ r0 = returnFunc(dev, on)
331+ } else {
332+ r0 = ret.Error(0)
333+ }
334+ return r0
335+}
336+
337+// mockdeviceConfig_SetNaturalScroll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetNaturalScroll'
338+type mockdeviceConfig_SetNaturalScroll_Call struct {
339+ *mock.Call
340+}
341+
342+// SetNaturalScroll is a helper method to define mock.On call
343+// - dev uintptr
344+// - on bool
345+func (_e *mockdeviceConfig_Expecter) SetNaturalScroll(dev any, on any) *mockdeviceConfig_SetNaturalScroll_Call {
346+ return &mockdeviceConfig_SetNaturalScroll_Call{Call: _e.mock.On("SetNaturalScroll", dev, on)}
347+}
348+
349+func (_c *mockdeviceConfig_SetNaturalScroll_Call) Run(run func(dev uintptr, on bool)) *mockdeviceConfig_SetNaturalScroll_Call {
350+ _c.Call.Run(func(args mock.Arguments) {
351+ var arg0 uintptr
352+ if args[0] != nil {
353+ arg0 = args[0].(uintptr)
354+ }
355+ var arg1 bool
356+ if args[1] != nil {
357+ arg1 = args[1].(bool)
358+ }
359+ run(
360+ arg0,
361+ arg1,
362+ )
363+ })
364+ return _c
365+}
366+
367+func (_c *mockdeviceConfig_SetNaturalScroll_Call) Return(err error) *mockdeviceConfig_SetNaturalScroll_Call {
368+ _c.Call.Return(err)
369+ return _c
370+}
371+
372+func (_c *mockdeviceConfig_SetNaturalScroll_Call) RunAndReturn(run func(dev uintptr, on bool) error) *mockdeviceConfig_SetNaturalScroll_Call {
373+ _c.Call.Return(run)
374+ return _c
375+}
376+
377+// SetTap provides a mock function for the type mockdeviceConfig
378+func (_mock *mockdeviceConfig) SetTap(dev uintptr, on bool) error {
379+ ret := _mock.Called(dev, on)
380+
381+ if len(ret) == 0 {
382+ panic("no return value specified for SetTap")
383+ }
384+
385+ var r0 error
386+ if returnFunc, ok := ret.Get(0).(func(uintptr, bool) error); ok {
387+ r0 = returnFunc(dev, on)
388+ } else {
389+ r0 = ret.Error(0)
390+ }
391+ return r0
392+}
393+
394+// mockdeviceConfig_SetTap_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetTap'
395+type mockdeviceConfig_SetTap_Call struct {
396+ *mock.Call
397+}
398+
399+// SetTap is a helper method to define mock.On call
400+// - dev uintptr
401+// - on bool
402+func (_e *mockdeviceConfig_Expecter) SetTap(dev any, on any) *mockdeviceConfig_SetTap_Call {
403+ return &mockdeviceConfig_SetTap_Call{Call: _e.mock.On("SetTap", dev, on)}
404+}
405+
406+func (_c *mockdeviceConfig_SetTap_Call) Run(run func(dev uintptr, on bool)) *mockdeviceConfig_SetTap_Call {
407+ _c.Call.Run(func(args mock.Arguments) {
408+ var arg0 uintptr
409+ if args[0] != nil {
410+ arg0 = args[0].(uintptr)
411+ }
412+ var arg1 bool
413+ if args[1] != nil {
414+ arg1 = args[1].(bool)
415+ }
416+ run(
417+ arg0,
418+ arg1,
419+ )
420+ })
421+ return _c
422+}
423+
424+func (_c *mockdeviceConfig_SetTap_Call) Return(err error) *mockdeviceConfig_SetTap_Call {
425+ _c.Call.Return(err)
426+ return _c
427+}
428+
429+func (_c *mockdeviceConfig_SetTap_Call) RunAndReturn(run func(dev uintptr, on bool) error) *mockdeviceConfig_SetTap_Call {
430+ _c.Call.Return(run)
431+ return _c
432+}
M internal/adapters/libinput/libinput.go +186 -95
@@ -39,57 +39,59 @@ var (
3939 loadOnce sync.Once
4040 loadErr error
4141
42- udevNew func() uintptr
43- udevUnref func(uintptr) uintptr
44- createContext func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr
45- assignSeat func(li uintptr, seat string) int32
46- unref func(li uintptr) uintptr
47- getFD func(li uintptr) int32
48- dispatch func(li uintptr) int32
49- getEvent func(li uintptr) uintptr
50- eventType func(ev uintptr) int32
51- eventDestroy func(ev uintptr)
52- eventDevice func(ev uintptr) uintptr
53- deviceName func(dev uintptr) string
54- suspend func(li uintptr)
55- resume func(li uintptr) int32
56- keyboardEvent func(ev uintptr) uintptr
57- keyboardKey func(kev uintptr) uint32
58- keyboardState func(kev uintptr) int32
59- keyboardUsec func(kev uintptr) uint64
60- pointerEvent func(ev uintptr) uintptr
61- pointerDX func(pev uintptr) float64
62- pointerDY func(pev uintptr) float64
63- pointerRawDX func(pev uintptr) float64
64- pointerRawDY func(pev uintptr) float64
65- pointerUsec func(pev uintptr) uint64
66- pointerAbsX func(pev uintptr, width uint32) float64
67- pointerAbsY func(pev uintptr, height uint32) float64
68- pointerButton func(pev uintptr) uint32
69- pointerBtnState func(pev uintptr) int32
70- pointerHasAxis func(pev uintptr, axis uint32) int32
71- scrollValue func(pev uintptr, axis uint32) float64
72- scrollV120 func(pev uintptr, axis uint32) float64
73- gestureEvent func(ev uintptr) uintptr
74- gestureFingers func(gev uintptr) int32
75- gestureCanceled func(gev uintptr) int32
76- gestureDX func(gev uintptr) float64
77- gestureDY func(gev uintptr) float64
78- gestureUsec func(gev uintptr) uint64
79- deviceRef func(dev uintptr) uintptr
80- deviceUnref func(dev uintptr) uintptr
81- hasNatural func(dev uintptr) int32
82- setNatural func(dev uintptr, on int32) int32
83- tapFingers func(dev uintptr) int32
84- setTap func(dev uintptr, on int32) int32
85- setTapMap func(dev uintptr, m int32) int32
86- hasCapability func(dev uintptr, capability int32) int32
87- accelAvailable func(dev uintptr) int32
88- setAccelSpeed func(dev uintptr, speed float64) int32
89- accelProfiles func(dev uintptr) uint32
90- setAccelProfile func(dev uintptr, profile uint32) int32
91- iface [2]uintptr
92- active ports.Seat
42+ udevNew func() uintptr
43+ udevUnref func(uintptr) uintptr
44+ createContext func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr
45+ assignSeat func(li uintptr, seat string) int32
46+ unref func(li uintptr) uintptr
47+ getFD func(li uintptr) int32
48+ dispatch func(li uintptr) int32
49+ getEvent func(li uintptr) uintptr
50+ eventType func(ev uintptr) int32
51+ eventDestroy func(ev uintptr)
52+ eventDevice func(ev uintptr) uintptr
53+ deviceName func(dev uintptr) string
54+ suspend func(li uintptr)
55+ resume func(li uintptr) int32
56+ keyboardEvent func(ev uintptr) uintptr
57+ keyboardKey func(kev uintptr) uint32
58+ keyboardState func(kev uintptr) int32
59+ keyboardUsec func(kev uintptr) uint64
60+ pointerEvent func(ev uintptr) uintptr
61+ pointerDX func(pev uintptr) float64
62+ pointerDY func(pev uintptr) float64
63+ pointerRawDX func(pev uintptr) float64
64+ pointerRawDY func(pev uintptr) float64
65+ pointerUsec func(pev uintptr) uint64
66+ pointerAbsX func(pev uintptr, width uint32) float64
67+ pointerAbsY func(pev uintptr, height uint32) float64
68+ pointerButton func(pev uintptr) uint32
69+ pointerBtnState func(pev uintptr) int32
70+ pointerHasAxis func(pev uintptr, axis uint32) int32
71+ scrollValue func(pev uintptr, axis uint32) float64
72+ scrollV120 func(pev uintptr, axis uint32) float64
73+ gestureEvent func(ev uintptr) uintptr
74+ gestureFingers func(gev uintptr) int32
75+ gestureCanceled func(gev uintptr) int32
76+ gestureDX func(gev uintptr) float64
77+ gestureDY func(gev uintptr) float64
78+ gestureUsec func(gev uintptr) uint64
79+ deviceRef func(dev uintptr) uintptr
80+ deviceUnref func(dev uintptr) uintptr
81+ hasNatural func(dev uintptr) int32
82+ setNatural func(dev uintptr, on int32) int32
83+ tapFingers func(dev uintptr) int32
84+ setTap func(dev uintptr, on int32) int32
85+ setTapMap func(dev uintptr, m int32) int32
86+ hasCapability func(dev uintptr, capability int32) int32
87+ accelAvailable func(dev uintptr) int32
88+ setAccelSpeed func(dev uintptr, speed float64) int32
89+ accelProfiles func(dev uintptr) uint32
90+ leftHandedAvailable func(dev uintptr) int32
91+ setLeftHanded func(dev uintptr, on int32) int32
92+ setAccelProfile func(dev uintptr, profile uint32) int32
93+ iface [2]uintptr
94+ active ports.Seat
9395 )
9496
9597 func load() error {
@@ -159,6 +161,8 @@ func load() error {
159161 reg(&setAccelSpeed, "device_config_accel_set_speed")
160162 reg(&accelProfiles, "device_config_accel_get_profiles")
161163 reg(&setAccelProfile, "device_config_accel_set_profile")
164+ reg(&leftHandedAvailable, "device_config_left_handed_is_available")
165+ reg(&setLeftHanded, "device_config_left_handed_set")
162166 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
163167 fd, err := active.OpenDevice(goString(path))
164168 if err != nil {
@@ -199,10 +203,12 @@ type Options struct {
199203 // Constraints holds the pointer lock or confinement of the focused
200204 // window, in global logical coordinates.
201205 Constraints <-chan ports.PointerConstraint
202- // Touchpad is the initial touchpad config; Touchpads replaces it live.
203- Touchpad ports.TouchpadConfig
204- Touchpads <-chan ports.TouchpadConfig
205- Active <-chan bool
206+ // DeviceConfig is the initial config: Touchpad for touchpads, Mouse for
207+ // the other pointers (mice, trackballs, trackpoints); other devices are
208+ // left alone. DeviceConfigs replaces it live, on the devices already added.
209+ DeviceConfig ports.InputDevicesConfig
210+ DeviceConfigs <-chan ports.InputDevicesConfig
211+ Active <-chan bool
206212 // MoveCursor, when set, places the hardware cursor as soon as motion is
207213 // read, before core sees the event: the output under the pointer and
208214 // the physical position on it. motion is false when the pointer is only
@@ -246,7 +252,7 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
246252 }
247253 p := newPointer(opts.Layout)
248254 p.moved(opts.MoveCursor, false)
249- in := &inputState{touchpad: opts.Touchpad, devices: map[uintptr]bool{}}
255+ in := &inputState{cfg: opts.DeviceConfig, dev: libinputDevices{}, devices: map[uintptr]bool{}}
250256 defer in.release()
251257 fd := getFD(li)
252258 fwd := newForwarder(opts.Log)
@@ -283,12 +289,14 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
283289 case c := <-opts.Constraints:
284290 p.constrain(c)
285291 p.moved(opts.MoveCursor, false)
286- case t := <-opts.Touchpads:
287- in.touchpad = t
292+ case c := <-opts.DeviceConfigs:
293+ in.cfg = c
288294 for dev := range in.devices {
289295 in.configure(dev, opts.Log)
290296 }
291- opts.Log.Info().Bool("natural_scroll", t.NaturalScroll).Bool("tap", t.Tap).Float64("accel_speed", t.AccelSpeed).Str("accel_profile", t.AccelProfile).Float64("scroll_factor", t.ScrollFactor).Msg("touchpad reloaded")
297+ t, m := c.Touchpad, c.Mouse
298+ opts.Log.Info().Bool("natural_scroll", t.NaturalScroll).Bool("tap", t.Tap).Float64("accel_speed", t.AccelSpeed).Str("accel_profile", t.AccelProfile).Bool("left_handed", t.LeftHanded).Float64("scroll_factor", t.ScrollFactor).
299+ Bool("mouse_natural_scroll", m.NaturalScroll).Float64("mouse_accel_speed", m.AccelSpeed).Str("mouse_accel_profile", m.AccelProfile).Bool("mouse_left_handed", m.LeftHanded).Msg("input devices reloaded")
292300 default:
293301 }
294302 fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}
@@ -316,51 +324,134 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
316324 return nil
317325 }
318326
319-// inputState is the device state Run owns: touchpad config, the devices
327+// inputState is the device state Run owns: the device config, the devices
320328 // it configures (referenced until removed) and the swipe in progress on
321-// each touchpad.
329+// each touchpad. dev applies the config to libinput; it is a zero-size
330+// type, so storing it in the interface allocates nothing.
322331 type inputState struct {
323- touchpad ports.TouchpadConfig
324- devices map[uintptr]bool
325- swipes swipes
332+ cfg ports.InputDevicesConfig
333+ dev deviceConfig
334+ devices map[uintptr]bool
335+ swipes swipes
326336 }
327337
328-// configurable reports whether the touchpad config applies to dev: it has
329-// natural scroll or tapping, or is a touchpad.
330-func configurable(dev uintptr) bool {
331- return hasNatural(dev) != 0 || tapFingers(dev) > 0 || touchpad(dev)
338+// deviceConfig is libinput's device configuration side: what a device is
339+// and the settings it takes. libinputDevices is the real one; tests use its
340+// Mockery mock. It serves device-added and config reload only, never the
341+// event path. A setting the device lacks is skipped without error; one it
342+// refuses returns a sentinel error.
343+type deviceConfig interface {
344+ // IsTouchpad reports the gesture capability libinput gives touchpads.
345+ IsTouchpad(dev uintptr) bool
346+ // IsPointer reports the pointer capability: mice, trackballs,
347+ // trackpoints and touchpads.
348+ IsPointer(dev uintptr) bool
349+ // Name is the device name, for logs.
350+ Name(dev uintptr) string
351+ SetNaturalScroll(dev uintptr, on bool) error
352+ // SetAccel sets the pointer speed and, when the device offers it, the
353+ // profile (ports.AccelFlat or ports.AccelAdaptive).
354+ SetAccel(dev uintptr, speed float64, profile string) error
355+ SetLeftHanded(dev uintptr, on bool) error
356+ // SetTap enables tap to click, with one finger left, two right, three
357+ // middle. Devices that cannot tap skip it.
358+ SetTap(dev uintptr, on bool) error
332359 }
333360
334-// capGesture is LIBINPUT_DEVICE_CAP_GESTURE: libinput gives it to
335-// touchpads, with or without tapping.
336-const capGesture = 5
361+// errRejected is a setting the device refuses (libinput status not success).
362+var errRejected = errors.New("rejected by libinput")
337363
338-func touchpad(dev uintptr) bool { return hasCapability(dev, capGesture) != 0 }
364+// libinputDevices implements deviceConfig over the libinput functions.
365+type libinputDevices struct{}
339366
340-// tapButtonMap is LIBINPUT_CONFIG_TAP_MAP_LRM: one finger taps left, two
341-// right, three middle.
342-const tapButtonMap = 0
367+const (
368+ // capPointer is LIBINPUT_DEVICE_CAP_POINTER.
369+ capPointer = 1
370+ // capGesture is LIBINPUT_DEVICE_CAP_GESTURE: libinput gives it to
371+ // touchpads, with or without tapping.
372+ capGesture = 5
373+ // tapButtonMap is LIBINPUT_CONFIG_TAP_MAP_LRM.
374+ tapButtonMap = 0
375+)
343376
344-func (s *inputState) configure(dev uintptr, log zerowrap.Logger) {
345- if hasNatural(dev) != 0 && setNatural(dev, flag(s.touchpad.NaturalScroll)) != 0 {
346- log.Warn().Str("device", deviceName(dev)).Msg("natural scroll rejected")
377+func (libinputDevices) IsTouchpad(dev uintptr) bool { return hasCapability(dev, capGesture) != 0 }
378+func (libinputDevices) IsPointer(dev uintptr) bool { return hasCapability(dev, capPointer) != 0 }
379+func (libinputDevices) Name(dev uintptr) string { return deviceName(dev) }
380+
381+func (libinputDevices) SetNaturalScroll(dev uintptr, on bool) error {
382+ if hasNatural(dev) != 0 && setNatural(dev, flag(on)) != 0 {
383+ return errRejected
347384 }
348- if tapFingers(dev) > 0 {
349- if setTap(dev, flag(s.touchpad.Tap)) != 0 {
350- log.Warn().Str("device", deviceName(dev)).Msg("tap to click rejected")
351- }
352- if setTapMap(dev, tapButtonMap) != 0 {
353- log.Warn().Str("device", deviceName(dev)).Msg("tap button map rejected")
354- }
385+ return nil
386+}
387+
388+func (libinputDevices) SetAccel(dev uintptr, speed float64, profile string) error {
389+ if accelAvailable(dev) == 0 {
390+ return nil
355391 }
356- // Pointer speed is a touchpad setting: mice keep libinput's own.
357- if touchpad(dev) && accelAvailable(dev) != 0 {
358- if setAccelSpeed(dev, s.touchpad.AccelSpeed) != 0 {
359- log.Warn().Str("device", deviceName(dev)).Msg("accel speed rejected")
360- }
361- if p := accelProfile(s.touchpad.AccelProfile); accelProfiles(dev)&p != 0 && setAccelProfile(dev, p) != 0 {
362- log.Warn().Str("device", deviceName(dev)).Msg("accel profile rejected")
363- }
392+ var err error
393+ if setAccelSpeed(dev, speed) != 0 {
394+ err = errRejected
395+ }
396+ if p := accelProfile(profile); accelProfiles(dev)&p != 0 && setAccelProfile(dev, p) != 0 {
397+ err = errRejected
398+ }
399+ return err
400+}
401+
402+func (libinputDevices) SetLeftHanded(dev uintptr, on bool) error {
403+ if leftHandedAvailable(dev) != 0 && setLeftHanded(dev, flag(on)) != 0 {
404+ return errRejected
405+ }
406+ return nil
407+}
408+
409+func (libinputDevices) SetTap(dev uintptr, on bool) error {
410+ if tapFingers(dev) <= 0 {
411+ return nil
412+ }
413+ var err error
414+ if setTap(dev, flag(on)) != 0 {
415+ err = errRejected
416+ }
417+ if setTapMap(dev, tapButtonMap) != 0 {
418+ err = errRejected
419+ }
420+ return err
421+}
422+
423+// configurable reports whether the device config applies to dev: a
424+// touchpad or another pointer. Keyboards, switches and the like are left
425+// alone and not referenced.
426+func (s *inputState) configurable(dev uintptr) bool {
427+ return s.dev.IsTouchpad(dev) || s.dev.IsPointer(dev)
428+}
429+
430+// configure applies the config to dev by type: touchpads take the touchpad
431+// settings and tapping, other pointers (mice, trackballs, trackpoints) the
432+// mouse settings, other devices nothing. A refused setting is logged and
433+// the rest still apply. The success path logs and allocates nothing.
434+func (s *inputState) configure(dev uintptr, log zerowrap.Logger) {
435+ switch {
436+ case s.dev.IsTouchpad(dev):
437+ s.applyPointer(dev, s.cfg.Touchpad.PointerConfig, log)
438+ s.check(dev, "tap to click", s.dev.SetTap(dev, s.cfg.Touchpad.Tap), log)
439+ case s.dev.IsPointer(dev):
440+ s.applyPointer(dev, s.cfg.Mouse, log)
441+ }
442+}
443+
444+// applyPointer applies the settings touchpads and mice share.
445+func (s *inputState) applyPointer(dev uintptr, c ports.PointerConfig, log zerowrap.Logger) {
446+ s.check(dev, "natural scroll", s.dev.SetNaturalScroll(dev, c.NaturalScroll), log)
447+ s.check(dev, "accel", s.dev.SetAccel(dev, c.AccelSpeed, c.AccelProfile), log)
448+ s.check(dev, "left handed", s.dev.SetLeftHanded(dev, c.LeftHanded), log)
449+}
450+
451+// check logs a refused setting; the device name is only read then.
452+func (s *inputState) check(dev uintptr, setting string, err error, log zerowrap.Logger) {
453+ if err != nil {
454+ log.Warn().Err(err).Str("device", s.dev.Name(dev)).Str("setting", setting).Msg("input device setting rejected")
364455 }
365456 }
366457
@@ -453,7 +544,7 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
453544 case evDeviceAdded:
454545 dev := eventDevice(ev)
455546 log.Info().Str("device", deviceName(dev)).Msg("input device added")
456- if configurable(dev) && !in.devices[dev] {
547+ if in.configurable(dev) && !in.devices[dev] {
457548 in.devices[deviceRef(dev)] = true
458549 in.configure(dev, log)
459550 }
@@ -519,7 +610,7 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
519610 s.Set, s.Value = true, scrollValue(pe, uint32(i))
520611 if source == ports.AxisFinger {
521612 // Two-finger scroll is the touchpad's only scroll source.
522- s.Value *= in.touchpad.ScrollFactor
613+ s.Value *= in.cfg.Touchpad.ScrollFactor
523614 }
524615 if source == ports.AxisWheel {
525616 s.V120 = int32(scrollV120(pe, uint32(i)))
@@ -39,57 +39,59 @@ var (
39 loadOnce sync.Once39 loadOnce sync.Once
40 loadErr error40 loadErr error
41 41
42- udevNew func() uintptr42+ udevNew func() uintptr
43- udevUnref func(uintptr) uintptr43+ udevUnref func(uintptr) uintptr
44- createContext func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr44+ createContext func(iface unsafe.Pointer, data uintptr, udev uintptr) uintptr
45- assignSeat func(li uintptr, seat string) int3245+ assignSeat func(li uintptr, seat string) int32
46- unref func(li uintptr) uintptr46+ unref func(li uintptr) uintptr
47- getFD func(li uintptr) int3247+ getFD func(li uintptr) int32
48- dispatch func(li uintptr) int3248+ dispatch func(li uintptr) int32
49- getEvent func(li uintptr) uintptr49+ getEvent func(li uintptr) uintptr
50- eventType func(ev uintptr) int3250+ eventType func(ev uintptr) int32
51- eventDestroy func(ev uintptr)51+ eventDestroy func(ev uintptr)
52- eventDevice func(ev uintptr) uintptr52+ eventDevice func(ev uintptr) uintptr
53- deviceName func(dev uintptr) string53+ deviceName func(dev uintptr) string
54- suspend func(li uintptr)54+ suspend func(li uintptr)
55- resume func(li uintptr) int3255+ resume func(li uintptr) int32
56- keyboardEvent func(ev uintptr) uintptr56+ keyboardEvent func(ev uintptr) uintptr
57- keyboardKey func(kev uintptr) uint3257+ keyboardKey func(kev uintptr) uint32
58- keyboardState func(kev uintptr) int3258+ keyboardState func(kev uintptr) int32
59- keyboardUsec func(kev uintptr) uint6459+ keyboardUsec func(kev uintptr) uint64
60- pointerEvent func(ev uintptr) uintptr60+ pointerEvent func(ev uintptr) uintptr
61- pointerDX func(pev uintptr) float6461+ pointerDX func(pev uintptr) float64
62- pointerDY func(pev uintptr) float6462+ pointerDY func(pev uintptr) float64
63- pointerRawDX func(pev uintptr) float6463+ pointerRawDX func(pev uintptr) float64
64- pointerRawDY func(pev uintptr) float6464+ pointerRawDY func(pev uintptr) float64
65- pointerUsec func(pev uintptr) uint6465+ pointerUsec func(pev uintptr) uint64
66- pointerAbsX func(pev uintptr, width uint32) float6466+ pointerAbsX func(pev uintptr, width uint32) float64
67- pointerAbsY func(pev uintptr, height uint32) float6467+ pointerAbsY func(pev uintptr, height uint32) float64
68- pointerButton func(pev uintptr) uint3268+ pointerButton func(pev uintptr) uint32
69- pointerBtnState func(pev uintptr) int3269+ pointerBtnState func(pev uintptr) int32
70- pointerHasAxis func(pev uintptr, axis uint32) int3270+ pointerHasAxis func(pev uintptr, axis uint32) int32
71- scrollValue func(pev uintptr, axis uint32) float6471+ scrollValue func(pev uintptr, axis uint32) float64
72- scrollV120 func(pev uintptr, axis uint32) float6472+ scrollV120 func(pev uintptr, axis uint32) float64
73- gestureEvent func(ev uintptr) uintptr73+ gestureEvent func(ev uintptr) uintptr
74- gestureFingers func(gev uintptr) int3274+ gestureFingers func(gev uintptr) int32
75- gestureCanceled func(gev uintptr) int3275+ gestureCanceled func(gev uintptr) int32
76- gestureDX func(gev uintptr) float6476+ gestureDX func(gev uintptr) float64
77- gestureDY func(gev uintptr) float6477+ gestureDY func(gev uintptr) float64
78- gestureUsec func(gev uintptr) uint6478+ gestureUsec func(gev uintptr) uint64
79- deviceRef func(dev uintptr) uintptr79+ deviceRef func(dev uintptr) uintptr
80- deviceUnref func(dev uintptr) uintptr80+ deviceUnref func(dev uintptr) uintptr
81- hasNatural func(dev uintptr) int3281+ hasNatural func(dev uintptr) int32
82- setNatural func(dev uintptr, on int32) int3282+ setNatural func(dev uintptr, on int32) int32
83- tapFingers func(dev uintptr) int3283+ tapFingers func(dev uintptr) int32
84- setTap func(dev uintptr, on int32) int3284+ setTap func(dev uintptr, on int32) int32
85- setTapMap func(dev uintptr, m int32) int3285+ setTapMap func(dev uintptr, m int32) int32
86- hasCapability func(dev uintptr, capability int32) int3286+ hasCapability func(dev uintptr, capability int32) int32
87- accelAvailable func(dev uintptr) int3287+ accelAvailable func(dev uintptr) int32
88- setAccelSpeed func(dev uintptr, speed float64) int3288+ setAccelSpeed func(dev uintptr, speed float64) int32
89- accelProfiles func(dev uintptr) uint3289+ accelProfiles func(dev uintptr) uint32
90- setAccelProfile func(dev uintptr, profile uint32) int3290+ leftHandedAvailable func(dev uintptr) int32
91- iface [2]uintptr91+ setLeftHanded func(dev uintptr, on int32) int32
92- active ports.Seat92+ setAccelProfile func(dev uintptr, profile uint32) int32
93+ iface [2]uintptr
94+ active ports.Seat
93 )95 )
94 96
95 func load() error {97 func load() error {
@@ -159,6 +161,8 @@ func load() error {
159 reg(&setAccelSpeed, "device_config_accel_set_speed")161 reg(&setAccelSpeed, "device_config_accel_set_speed")
160 reg(&accelProfiles, "device_config_accel_get_profiles")162 reg(&accelProfiles, "device_config_accel_get_profiles")
161 reg(&setAccelProfile, "device_config_accel_set_profile")163 reg(&setAccelProfile, "device_config_accel_set_profile")
164+ reg(&leftHandedAvailable, "device_config_left_handed_is_available")
165+ reg(&setLeftHanded, "device_config_left_handed_set")
162 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {166 iface[0] = purego.NewCallback(func(path *byte, _, _ uintptr) uintptr {
163 fd, err := active.OpenDevice(goString(path))167 fd, err := active.OpenDevice(goString(path))
164 if err != nil {168 if err != nil {
@@ -199,10 +203,12 @@ type Options struct {
199 // Constraints holds the pointer lock or confinement of the focused203 // Constraints holds the pointer lock or confinement of the focused
200 // window, in global logical coordinates.204 // window, in global logical coordinates.
201 Constraints <-chan ports.PointerConstraint205 Constraints <-chan ports.PointerConstraint
202- // Touchpad is the initial touchpad config; Touchpads replaces it live.206+ // DeviceConfig is the initial config: Touchpad for touchpads, Mouse for
203- Touchpad ports.TouchpadConfig207+ // the other pointers (mice, trackballs, trackpoints); other devices are
204- Touchpads <-chan ports.TouchpadConfig208+ // left alone. DeviceConfigs replaces it live, on the devices already added.
205- Active <-chan bool209+ DeviceConfig ports.InputDevicesConfig
210+ DeviceConfigs <-chan ports.InputDevicesConfig
211+ Active <-chan bool
206 // MoveCursor, when set, places the hardware cursor as soon as motion is212 // MoveCursor, when set, places the hardware cursor as soon as motion is
207 // read, before core sees the event: the output under the pointer and213 // read, before core sees the event: the output under the pointer and
208 // the physical position on it. motion is false when the pointer is only214 // the physical position on it. motion is false when the pointer is only
@@ -246,7 +252,7 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
246 }252 }
247 p := newPointer(opts.Layout)253 p := newPointer(opts.Layout)
248 p.moved(opts.MoveCursor, false)254 p.moved(opts.MoveCursor, false)
249- in := &inputState{touchpad: opts.Touchpad, devices: map[uintptr]bool{}}255+ in := &inputState{cfg: opts.DeviceConfig, dev: libinputDevices{}, devices: map[uintptr]bool{}}
250 defer in.release()256 defer in.release()
251 fd := getFD(li)257 fd := getFD(li)
252 fwd := newForwarder(opts.Log)258 fwd := newForwarder(opts.Log)
@@ -283,12 +289,14 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
283 case c := <-opts.Constraints:289 case c := <-opts.Constraints:
284 p.constrain(c)290 p.constrain(c)
285 p.moved(opts.MoveCursor, false)291 p.moved(opts.MoveCursor, false)
286- case t := <-opts.Touchpads:292+ case c := <-opts.DeviceConfigs:
287- in.touchpad = t293+ in.cfg = c
288 for dev := range in.devices {294 for dev := range in.devices {
289 in.configure(dev, opts.Log)295 in.configure(dev, opts.Log)
290 }296 }
291- opts.Log.Info().Bool("natural_scroll", t.NaturalScroll).Bool("tap", t.Tap).Float64("accel_speed", t.AccelSpeed).Str("accel_profile", t.AccelProfile).Float64("scroll_factor", t.ScrollFactor).Msg("touchpad reloaded")297+ t, m := c.Touchpad, c.Mouse
298+ opts.Log.Info().Bool("natural_scroll", t.NaturalScroll).Bool("tap", t.Tap).Float64("accel_speed", t.AccelSpeed).Str("accel_profile", t.AccelProfile).Bool("left_handed", t.LeftHanded).Float64("scroll_factor", t.ScrollFactor).
299+ Bool("mouse_natural_scroll", m.NaturalScroll).Float64("mouse_accel_speed", m.AccelSpeed).Str("mouse_accel_profile", m.AccelProfile).Bool("mouse_left_handed", m.LeftHanded).Msg("input devices reloaded")
292 default:300 default:
293 }301 }
294 fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}302 fds := []unix.PollFd{{Fd: fd, Events: unix.POLLIN}}
@@ -316,51 +324,134 @@ func Run(ctx context.Context, opts Options, input chan<- ports.InputEvent) error
316 return nil324 return nil
317 }325 }
318 326
319-// inputState is the device state Run owns: touchpad config, the devices327+// inputState is the device state Run owns: the device config, the devices
320 // it configures (referenced until removed) and the swipe in progress on328 // it configures (referenced until removed) and the swipe in progress on
321-// each touchpad.329+// each touchpad. dev applies the config to libinput; it is a zero-size
330+// type, so storing it in the interface allocates nothing.
322 type inputState struct {331 type inputState struct {
323- touchpad ports.TouchpadConfig332+ cfg ports.InputDevicesConfig
324- devices map[uintptr]bool333+ dev deviceConfig
325- swipes swipes334+ devices map[uintptr]bool
335+ swipes swipes
326 }336 }
327 337
328-// configurable reports whether the touchpad config applies to dev: it has338+// deviceConfig is libinput's device configuration side: what a device is
329-// natural scroll or tapping, or is a touchpad.339+// and the settings it takes. libinputDevices is the real one; tests use its
330-func configurable(dev uintptr) bool {340+// Mockery mock. It serves device-added and config reload only, never the
331- return hasNatural(dev) != 0 || tapFingers(dev) > 0 || touchpad(dev)341+// event path. A setting the device lacks is skipped without error; one it
342+// refuses returns a sentinel error.
343+type deviceConfig interface {
344+ // IsTouchpad reports the gesture capability libinput gives touchpads.
345+ IsTouchpad(dev uintptr) bool
346+ // IsPointer reports the pointer capability: mice, trackballs,
347+ // trackpoints and touchpads.
348+ IsPointer(dev uintptr) bool
349+ // Name is the device name, for logs.
350+ Name(dev uintptr) string
351+ SetNaturalScroll(dev uintptr, on bool) error
352+ // SetAccel sets the pointer speed and, when the device offers it, the
353+ // profile (ports.AccelFlat or ports.AccelAdaptive).
354+ SetAccel(dev uintptr, speed float64, profile string) error
355+ SetLeftHanded(dev uintptr, on bool) error
356+ // SetTap enables tap to click, with one finger left, two right, three
357+ // middle. Devices that cannot tap skip it.
358+ SetTap(dev uintptr, on bool) error
332 }359 }
333 360
334-// capGesture is LIBINPUT_DEVICE_CAP_GESTURE: libinput gives it to361+// errRejected is a setting the device refuses (libinput status not success).
335-// touchpads, with or without tapping.362+var errRejected = errors.New("rejected by libinput")
336-const capGesture = 5
337 363
338-func touchpad(dev uintptr) bool { return hasCapability(dev, capGesture) != 0 }364+// libinputDevices implements deviceConfig over the libinput functions.
365+type libinputDevices struct{}
339 366
340-// tapButtonMap is LIBINPUT_CONFIG_TAP_MAP_LRM: one finger taps left, two367+const (
341-// right, three middle.368+ // capPointer is LIBINPUT_DEVICE_CAP_POINTER.
342-const tapButtonMap = 0369+ capPointer = 1
370+ // capGesture is LIBINPUT_DEVICE_CAP_GESTURE: libinput gives it to
371+ // touchpads, with or without tapping.
372+ capGesture = 5
373+ // tapButtonMap is LIBINPUT_CONFIG_TAP_MAP_LRM.
374+ tapButtonMap = 0
375+)
343 376
344-func (s *inputState) configure(dev uintptr, log zerowrap.Logger) {377+func (libinputDevices) IsTouchpad(dev uintptr) bool { return hasCapability(dev, capGesture) != 0 }
345- if hasNatural(dev) != 0 && setNatural(dev, flag(s.touchpad.NaturalScroll)) != 0 {378+func (libinputDevices) IsPointer(dev uintptr) bool { return hasCapability(dev, capPointer) != 0 }
346- log.Warn().Str("device", deviceName(dev)).Msg("natural scroll rejected")379+func (libinputDevices) Name(dev uintptr) string { return deviceName(dev) }
380+
381+func (libinputDevices) SetNaturalScroll(dev uintptr, on bool) error {
382+ if hasNatural(dev) != 0 && setNatural(dev, flag(on)) != 0 {
383+ return errRejected
347 }384 }
348- if tapFingers(dev) > 0 {385+ return nil
349- if setTap(dev, flag(s.touchpad.Tap)) != 0 {386+}
350- log.Warn().Str("device", deviceName(dev)).Msg("tap to click rejected")387+
351- }388+func (libinputDevices) SetAccel(dev uintptr, speed float64, profile string) error {
352- if setTapMap(dev, tapButtonMap) != 0 {389+ if accelAvailable(dev) == 0 {
353- log.Warn().Str("device", deviceName(dev)).Msg("tap button map rejected")390+ return nil
354- }
355 }391 }
356- // Pointer speed is a touchpad setting: mice keep libinput's own.392+ var err error
357- if touchpad(dev) && accelAvailable(dev) != 0 {393+ if setAccelSpeed(dev, speed) != 0 {
358- if setAccelSpeed(dev, s.touchpad.AccelSpeed) != 0 {394+ err = errRejected
359- log.Warn().Str("device", deviceName(dev)).Msg("accel speed rejected")395+ }
360- }396+ if p := accelProfile(profile); accelProfiles(dev)&p != 0 && setAccelProfile(dev, p) != 0 {
361- if p := accelProfile(s.touchpad.AccelProfile); accelProfiles(dev)&p != 0 && setAccelProfile(dev, p) != 0 {397+ err = errRejected
362- log.Warn().Str("device", deviceName(dev)).Msg("accel profile rejected")398+ }
363- }399+ return err
400+}
401+
402+func (libinputDevices) SetLeftHanded(dev uintptr, on bool) error {
403+ if leftHandedAvailable(dev) != 0 && setLeftHanded(dev, flag(on)) != 0 {
404+ return errRejected
405+ }
406+ return nil
407+}
408+
409+func (libinputDevices) SetTap(dev uintptr, on bool) error {
410+ if tapFingers(dev) <= 0 {
411+ return nil
412+ }
413+ var err error
414+ if setTap(dev, flag(on)) != 0 {
415+ err = errRejected
416+ }
417+ if setTapMap(dev, tapButtonMap) != 0 {
418+ err = errRejected
419+ }
420+ return err
421+}
422+
423+// configurable reports whether the device config applies to dev: a
424+// touchpad or another pointer. Keyboards, switches and the like are left
425+// alone and not referenced.
426+func (s *inputState) configurable(dev uintptr) bool {
427+ return s.dev.IsTouchpad(dev) || s.dev.IsPointer(dev)
428+}
429+
430+// configure applies the config to dev by type: touchpads take the touchpad
431+// settings and tapping, other pointers (mice, trackballs, trackpoints) the
432+// mouse settings, other devices nothing. A refused setting is logged and
433+// the rest still apply. The success path logs and allocates nothing.
434+func (s *inputState) configure(dev uintptr, log zerowrap.Logger) {
435+ switch {
436+ case s.dev.IsTouchpad(dev):
437+ s.applyPointer(dev, s.cfg.Touchpad.PointerConfig, log)
438+ s.check(dev, "tap to click", s.dev.SetTap(dev, s.cfg.Touchpad.Tap), log)
439+ case s.dev.IsPointer(dev):
440+ s.applyPointer(dev, s.cfg.Mouse, log)
441+ }
442+}
443+
444+// applyPointer applies the settings touchpads and mice share.
445+func (s *inputState) applyPointer(dev uintptr, c ports.PointerConfig, log zerowrap.Logger) {
446+ s.check(dev, "natural scroll", s.dev.SetNaturalScroll(dev, c.NaturalScroll), log)
447+ s.check(dev, "accel", s.dev.SetAccel(dev, c.AccelSpeed, c.AccelProfile), log)
448+ s.check(dev, "left handed", s.dev.SetLeftHanded(dev, c.LeftHanded), log)
449+}
450+
451+// check logs a refused setting; the device name is only read then.
452+func (s *inputState) check(dev uintptr, setting string, err error, log zerowrap.Logger) {
453+ if err != nil {
454+ log.Warn().Err(err).Str("device", s.dev.Name(dev)).Str("setting", setting).Msg("input device setting rejected")
364 }455 }
365 }456 }
366 457
@@ -453,7 +544,7 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
453 case evDeviceAdded:544 case evDeviceAdded:
454 dev := eventDevice(ev)545 dev := eventDevice(ev)
455 log.Info().Str("device", deviceName(dev)).Msg("input device added")546 log.Info().Str("device", deviceName(dev)).Msg("input device added")
456- if configurable(dev) && !in.devices[dev] {547+ if in.configurable(dev) && !in.devices[dev] {
457 in.devices[deviceRef(dev)] = true548 in.devices[deviceRef(dev)] = true
458 in.configure(dev, log)549 in.configure(dev, log)
459 }550 }
@@ -519,7 +610,7 @@ func translateEvent(ev uintptr, opts Options, p *pointer, in *inputState, state
519 s.Set, s.Value = true, scrollValue(pe, uint32(i))610 s.Set, s.Value = true, scrollValue(pe, uint32(i))
520 if source == ports.AxisFinger {611 if source == ports.AxisFinger {
521 // Two-finger scroll is the touchpad's only scroll source.612 // Two-finger scroll is the touchpad's only scroll source.
522- s.Value *= in.touchpad.ScrollFactor613+ s.Value *= in.cfg.Touchpad.ScrollFactor
523 }614 }
524 if source == ports.AxisWheel {615 if source == ports.AxisWheel {
525 s.V120 = int32(scrollV120(pe, uint32(i)))616 s.V120 = int32(scrollV120(pe, uint32(i)))
M internal/app/app.go +30 -26
@@ -191,7 +191,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
191191 }
192192 watched := make(chan ports.ConfigChanged, 8)
193193 keymaps := make(chan *xkb.Keymap, 1)
194- touchpads := make(chan ports.TouchpadConfig, 1)
194+ deviceConfigs := make(chan ports.InputDevicesConfig, 1)
195195 go func() {
196196 defer workers.Done()
197197 done <- config.Watch(ctx, path, watched, logging.For(ctx, "config"))
@@ -209,7 +209,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
209209 workers.Add(1)
210210 go func() {
211211 defer workers.Done()
212- relayConfig(ctx, opts.Config, watched, filtered, keymaps, touchpads, curs, commands, logging.For(ctx, "config"))
212+ relayConfig(ctx, opts.Config, watched, filtered, keymaps, deviceConfigs, curs, commands, logging.For(ctx, "config"))
213213 }()
214214 script := make(chan string)
215215 go func() {
@@ -231,7 +231,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
231231 log.Warn().Str("component", "sched").Err(err).Msg("input scheduling")
232232 }
233233 }
234- return libinput.Run(ctx, libinput.Options{Security: security, Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Touchpad: opts.Config.Touchpad, Touchpads: touchpads, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion"), LogKeys: logging.Enabled(ctx, "input-keys")}, input)
234+ return libinput.Run(ctx, libinput.Options{Security: security, Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, DeviceConfig: opts.Config.InputDevicesConfig, DeviceConfigs: deviceConfigs, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion"), LogKeys: logging.Enabled(ctx, "input-keys")}, input)
235235 })
236236 return
237237 }
@@ -369,11 +369,12 @@ const screensaverRetry = 2 * time.Second
369369 // relayConfig forwards reloads to core. A layout change builds a new keymap, hands it
370370 // to the input goroutine and sends it to clients; a repeat-only change just updates
371371 // clients. A keymap that fails to build keeps the previous layout. A touchpad
372-// change goes to the input goroutine; only the newest one waits there. The
373-// cursor idle delay goes straight to the cursor router.
374-func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, touchpads chan ports.TouchpadConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) {
372+// or mouse change goes to the input goroutine as one InputDevicesConfig; only
373+// the newest one waits there. The cursor idle delay goes straight to the
374+// cursor router.
375+func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, deviceConfigs chan ports.InputDevicesConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) {
375376 kb := cur.Keyboard
376- tp := cur.Touchpad
377+ devCfg := cur.InputDevicesConfig
377378 for {
378379 var ev ports.ConfigChanged
379380 select {
@@ -408,13 +409,9 @@ func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigCh
408409 }
409410 }
410411 curs.setHideAfter(ev.Config.Cursor.HideAfter)
411- if ev.Config.Touchpad != tp {
412- tp = ev.Config.Touchpad
413- select {
414- case <-touchpads:
415- default:
416- }
417- touchpads <- tp // buffered and just drained: this goroutine is the only sender
412+ if ev.Config.InputDevicesConfig != devCfg {
413+ devCfg = ev.Config.InputDevicesConfig
414+ latest(deviceConfigs, devCfg) // this goroutine is the only sender
418415 }
419416 if layoutChanged || repeatChanged {
420417 select {
@@ -475,18 +472,7 @@ func consumeScenes(ctx context.Context, scenes <-chan []ports.Scene, configError
475472 default:
476473 }
477474 }
478- select {
479- case renderScenes <- set:
480- default:
481- select {
482- case <-renderScenes:
483- default:
484- }
485- select {
486- case renderScenes <- set:
487- default:
488- }
489- }
475+ latest(renderScenes, set) // this goroutine is the only sender
490476 for _, s := range set {
491477 ev := log.Debug().Str("output", s.Output).Uint64("seq", s.Seq).Int("out_w", s.OutputWidth).Int("out_h", s.OutputHeight).Float64("scale", s.Scale)
492478 rects := make([]string, 0, len(s.Windows))
@@ -532,3 +518,21 @@ func renderNode(dev uint64) string {
532518 }
533519 return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev))
534520 }
521+
522+// latest sends v without blocking and replaces a value the consumer has not
523+// read yet: the newest wins. Only one goroutine may send on ch; consumers only
524+// receive, so the retry after draining cannot find the buffer full again.
525+func latest[T any](ch chan T, v T) {
526+ select {
527+ case ch <- v:
528+ default:
529+ select {
530+ case <-ch:
531+ default:
532+ }
533+ select {
534+ case ch <- v:
535+ default:
536+ }
537+ }
538+}
@@ -191,7 +191,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
191 }191 }
192 watched := make(chan ports.ConfigChanged, 8)192 watched := make(chan ports.ConfigChanged, 8)
193 keymaps := make(chan *xkb.Keymap, 1)193 keymaps := make(chan *xkb.Keymap, 1)
194- touchpads := make(chan ports.TouchpadConfig, 1)194+ deviceConfigs := make(chan ports.InputDevicesConfig, 1)
195 go func() {195 go func() {
196 defer workers.Done()196 defer workers.Done()
197 done <- config.Watch(ctx, path, watched, logging.For(ctx, "config"))197 done <- config.Watch(ctx, path, watched, logging.For(ctx, "config"))
@@ -209,7 +209,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
209 workers.Add(1)209 workers.Add(1)
210 go func() {210 go func() {
211 defer workers.Done()211 defer workers.Done()
212- relayConfig(ctx, opts.Config, watched, filtered, keymaps, touchpads, curs, commands, logging.For(ctx, "config"))212+ relayConfig(ctx, opts.Config, watched, filtered, keymaps, deviceConfigs, curs, commands, logging.For(ctx, "config"))
213 }()213 }()
214 script := make(chan string)214 script := make(chan string)
215 go func() {215 go func() {
@@ -231,7 +231,7 @@ func run(ctx context.Context, opts Options, inject func(chan<- ports.InputEvent)
231 log.Warn().Str("component", "sched").Err(err).Msg("input scheduling")231 log.Warn().Str("component", "sched").Err(err).Msg("input scheduling")
232 }232 }
233 }233 }
234- return libinput.Run(ctx, libinput.Options{Security: security, Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, Touchpad: opts.Config.Touchpad, Touchpads: touchpads, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion"), LogKeys: logging.Enabled(ctx, "input-keys")}, input)234+ return libinput.Run(ctx, libinput.Options{Security: security, Seat: hw.seat, SeatName: hw.seat.Name(), Keymap: km, Keymaps: keymaps, Layout: layout, Layouts: layouts, Constraints: constraints, DeviceConfig: opts.Config.InputDevicesConfig, DeviceConfigs: deviceConfigs, Active: hw.seat.Subscribe(), MoveCursor: curs.move, Log: logging.For(ctx, "input"), LogMotion: logging.Enabled(ctx, "input-motion"), LogKeys: logging.Enabled(ctx, "input-keys")}, input)
235 })235 })
236 return236 return
237 }237 }
@@ -369,11 +369,12 @@ const screensaverRetry = 2 * time.Second
369 // relayConfig forwards reloads to core. A layout change builds a new keymap, hands it369 // relayConfig forwards reloads to core. A layout change builds a new keymap, hands it
370 // to the input goroutine and sends it to clients; a repeat-only change just updates370 // to the input goroutine and sends it to clients; a repeat-only change just updates
371 // clients. A keymap that fails to build keeps the previous layout. A touchpad371 // clients. A keymap that fails to build keeps the previous layout. A touchpad
372-// change goes to the input goroutine; only the newest one waits there. The372+// or mouse change goes to the input goroutine as one InputDevicesConfig; only
373-// cursor idle delay goes straight to the cursor router.373+// the newest one waits there. The cursor idle delay goes straight to the
374-func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, touchpads chan ports.TouchpadConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) {374+// cursor router.
375+func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigChanged, out chan<- ports.ConfigChanged, keymaps chan *xkb.Keymap, deviceConfigs chan ports.InputDevicesConfig, curs *cursors, commands chan<- ports.ClientCommand, log zerowrap.Logger) {
375 kb := cur.Keyboard376 kb := cur.Keyboard
376- tp := cur.Touchpad377+ devCfg := cur.InputDevicesConfig
377 for {378 for {
378 var ev ports.ConfigChanged379 var ev ports.ConfigChanged
379 select {380 select {
@@ -408,13 +409,9 @@ func relayConfig(ctx context.Context, cur ports.Config, in <-chan ports.ConfigCh
408 }409 }
409 }410 }
410 curs.setHideAfter(ev.Config.Cursor.HideAfter)411 curs.setHideAfter(ev.Config.Cursor.HideAfter)
411- if ev.Config.Touchpad != tp {412+ if ev.Config.InputDevicesConfig != devCfg {
412- tp = ev.Config.Touchpad413+ devCfg = ev.Config.InputDevicesConfig
413- select {414+ latest(deviceConfigs, devCfg) // this goroutine is the only sender
414- case <-touchpads:
415- default:
416- }
417- touchpads <- tp // buffered and just drained: this goroutine is the only sender
418 }415 }
419 if layoutChanged || repeatChanged {416 if layoutChanged || repeatChanged {
420 select {417 select {
@@ -475,18 +472,7 @@ func consumeScenes(ctx context.Context, scenes <-chan []ports.Scene, configError
475 default:472 default:
476 }473 }
477 }474 }
478- select {475+ latest(renderScenes, set) // this goroutine is the only sender
479- case renderScenes <- set:
480- default:
481- select {
482- case <-renderScenes:
483- default:
484- }
485- select {
486- case renderScenes <- set:
487- default:
488- }
489- }
490 for _, s := range set {476 for _, s := range set {
491 ev := log.Debug().Str("output", s.Output).Uint64("seq", s.Seq).Int("out_w", s.OutputWidth).Int("out_h", s.OutputHeight).Float64("scale", s.Scale)477 ev := log.Debug().Str("output", s.Output).Uint64("seq", s.Seq).Int("out_w", s.OutputWidth).Int("out_h", s.OutputHeight).Float64("scale", s.Scale)
492 rects := make([]string, 0, len(s.Windows))478 rects := make([]string, 0, len(s.Windows))
@@ -532,3 +518,21 @@ func renderNode(dev uint64) string {
532 }518 }
533 return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev))519 return fmt.Sprintf("/dev/dri/renderD%d", unix.Minor(dev))
534 }520 }
521+
522+// latest sends v without blocking and replaces a value the consumer has not
523+// read yet: the newest wins. Only one goroutine may send on ch; consumers only
524+// receive, so the retry after draining cannot find the buffer full again.
525+func latest[T any](ch chan T, v T) {
526+ select {
527+ case ch <- v:
528+ default:
529+ select {
530+ case <-ch:
531+ default:
532+ }
533+ select {
534+ case ch <- v:
535+ default:
536+ }
537+ }
538+}
M internal/app/app_test.go +29 -5
@@ -273,13 +273,13 @@ func TestRelayConfigKeyboard(t *testing.T) {
273273 out := make(chan ports.ConfigChanged, 1)
274274 keymaps := make(chan *xkb.Keymap, 1)
275275 commands := make(chan ports.ClientCommand, 1)
276- touchpads := make(chan ports.TouchpadConfig, 1)
277- go relayConfig(ctx, cur, in, out, keymaps, touchpads, newCursors(clock.System{}, 0), commands, logging.For(ctx, "config"))
276+ devices := make(chan ports.InputDevicesConfig, 1)
277+ go relayConfig(ctx, cur, in, out, keymaps, devices, newCursors(clock.System{}, 0), commands, logging.For(ctx, "config"))
278278
279279 next := config.Defaults()
280280 next.Background.Color = "#000000"
281281 in <- ports.ConfigChanged{Config: next}
282- if got := <-out; got.Config.Background.Color != "#000000" || len(keymaps) != 0 || len(commands) != 0 || len(touchpads) != 0 {
282+ if got := <-out; got.Config.Background.Color != "#000000" || len(keymaps) != 0 || len(commands) != 0 || len(devices) != 0 {
283283 t.Fatalf("non-keyboard change rebuilt keymap: %+v", got)
284284 }
285285
@@ -290,8 +290,32 @@ func TestRelayConfigKeyboard(t *testing.T) {
290290 next.Touchpad.NaturalScroll = false
291291 in <- ports.ConfigChanged{Config: next}
292292 <-out
293- if tp := <-touchpads; tp.NaturalScroll || len(touchpads) != 0 {
294- t.Fatalf("touchpad: %+v", tp)
293+ if d := <-devices; d.Touchpad.NaturalScroll || len(devices) != 0 {
294+ t.Fatalf("touchpad: %+v", d)
295+ }
296+
297+ // Touchpad and mouse changes travel together.
298+ next.Touchpad.Tap = false
299+ next.Mouse.LeftHanded = true
300+ next.Mouse.AccelSpeed = 0.25
301+ in <- ports.ConfigChanged{Config: next}
302+ <-out
303+ want := ports.InputDevicesConfig{Touchpad: next.Touchpad, Mouse: next.Mouse}
304+ if d := <-devices; d != want || len(devices) != 0 {
305+ t.Fatalf("devices: %+v, want %+v", d, want)
306+ }
307+ // A mouse-only change is relayed too, with the touchpad config beside it.
308+ // relayConfig sends to devices before out, so it is ready after <-out.
309+ next.Mouse.NaturalScroll = true
310+ in <- ports.ConfigChanged{Config: next}
311+ <-out
312+ select {
313+ case d := <-devices:
314+ if !d.Mouse.NaturalScroll || !d.Mouse.LeftHanded || d.Touchpad != want.Touchpad || len(devices) != 0 {
315+ t.Fatalf("mouse-only change: %+v", d)
316+ }
317+ default:
318+ t.Fatal("mouse-only change not relayed")
295319 }
296320
297321 next.Keyboard.Layout, next.Keyboard.RepeatRate = "fr", 40
@@ -273,13 +273,13 @@ func TestRelayConfigKeyboard(t *testing.T) {
273 out := make(chan ports.ConfigChanged, 1)273 out := make(chan ports.ConfigChanged, 1)
274 keymaps := make(chan *xkb.Keymap, 1)274 keymaps := make(chan *xkb.Keymap, 1)
275 commands := make(chan ports.ClientCommand, 1)275 commands := make(chan ports.ClientCommand, 1)
276- touchpads := make(chan ports.TouchpadConfig, 1)276+ devices := make(chan ports.InputDevicesConfig, 1)
277- go relayConfig(ctx, cur, in, out, keymaps, touchpads, newCursors(clock.System{}, 0), commands, logging.For(ctx, "config"))277+ go relayConfig(ctx, cur, in, out, keymaps, devices, newCursors(clock.System{}, 0), commands, logging.For(ctx, "config"))
278 278
279 next := config.Defaults()279 next := config.Defaults()
280 next.Background.Color = "#000000"280 next.Background.Color = "#000000"
281 in <- ports.ConfigChanged{Config: next}281 in <- ports.ConfigChanged{Config: next}
282- if got := <-out; got.Config.Background.Color != "#000000" || len(keymaps) != 0 || len(commands) != 0 || len(touchpads) != 0 {282+ if got := <-out; got.Config.Background.Color != "#000000" || len(keymaps) != 0 || len(commands) != 0 || len(devices) != 0 {
283 t.Fatalf("non-keyboard change rebuilt keymap: %+v", got)283 t.Fatalf("non-keyboard change rebuilt keymap: %+v", got)
284 }284 }
285 285
@@ -290,8 +290,32 @@ func TestRelayConfigKeyboard(t *testing.T) {
290 next.Touchpad.NaturalScroll = false290 next.Touchpad.NaturalScroll = false
291 in <- ports.ConfigChanged{Config: next}291 in <- ports.ConfigChanged{Config: next}
292 <-out292 <-out
293- if tp := <-touchpads; tp.NaturalScroll || len(touchpads) != 0 {293+ if d := <-devices; d.Touchpad.NaturalScroll || len(devices) != 0 {
294- t.Fatalf("touchpad: %+v", tp)294+ t.Fatalf("touchpad: %+v", d)
295+ }
296+
297+ // Touchpad and mouse changes travel together.
298+ next.Touchpad.Tap = false
299+ next.Mouse.LeftHanded = true
300+ next.Mouse.AccelSpeed = 0.25
301+ in <- ports.ConfigChanged{Config: next}
302+ <-out
303+ want := ports.InputDevicesConfig{Touchpad: next.Touchpad, Mouse: next.Mouse}
304+ if d := <-devices; d != want || len(devices) != 0 {
305+ t.Fatalf("devices: %+v, want %+v", d, want)
306+ }
307+ // A mouse-only change is relayed too, with the touchpad config beside it.
308+ // relayConfig sends to devices before out, so it is ready after <-out.
309+ next.Mouse.NaturalScroll = true
310+ in <- ports.ConfigChanged{Config: next}
311+ <-out
312+ select {
313+ case d := <-devices:
314+ if !d.Mouse.NaturalScroll || !d.Mouse.LeftHanded || d.Touchpad != want.Touchpad || len(devices) != 0 {
315+ t.Fatalf("mouse-only change: %+v", d)
316+ }
317+ default:
318+ t.Fatal("mouse-only change not relayed")
295 }319 }
296 320
297 next.Keyboard.Layout, next.Keyboard.RepeatRate = "fr", 40321 next.Keyboard.Layout, next.Keyboard.RepeatRate = "fr", 40
M internal/core/overview.go +3 -3
@@ -911,9 +911,9 @@ const (
911911 // in a stack is easy to pick.
912912 // Continuous scrolling steps once per overviewScrollStep and a wheel once
913913 // per notch (high-resolution wheels add up their fractions of a notch).
914-// Vertical steps select cards first, then rows; touchpad.natural-scroll
915-// flips both axes as libinput reports. The end of a scroll resets it
916-// (scrollStop).
914+// Vertical steps select cards first, then rows; natural scroll
915+// (touchpad.* or mouse.*) flips both axes as libinput reports. The end of
916+// a scroll resets it (scrollStop).
917917 func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
918918 if a.Vertical.Stop || a.Horizontal.Stop {
919919 return false
@@ -911,9 +911,9 @@ const (
911 // in a stack is easy to pick.911 // in a stack is easy to pick.
912 // Continuous scrolling steps once per overviewScrollStep and a wheel once912 // Continuous scrolling steps once per overviewScrollStep and a wheel once
913 // per notch (high-resolution wheels add up their fractions of a notch).913 // per notch (high-resolution wheels add up their fractions of a notch).
914-// Vertical steps select cards first, then rows; touchpad.natural-scroll914+// Vertical steps select cards first, then rows; natural scroll
915-// flips both axes as libinput reports. The end of a scroll resets it915+// (touchpad.* or mouse.*) flips both axes as libinput reports. The end of
916-// (scrollStop).916+// a scroll resets it (scrollStop).
917 func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {917 func (m *Monitor) overviewScroll(a ports.PointerAxis) (changed bool) {
918 if a.Vertical.Stop || a.Horizontal.Stop {918 if a.Vertical.Stop || a.Horizontal.Stop {
919 return false919 return false
M internal/ports/config.go +26 -10
@@ -110,7 +110,8 @@ type Config struct {
110110 LayoutRules
111111 Outputs []OutputLayout
112112 }
113- Touchpad TouchpadConfig
113+ // InputDevicesConfig holds Touchpad and Mouse, promoted.
114+ InputDevicesConfig
114115 // Cursor.HideAfter hides the pointer cursor after this long without
115116 // motion; the next motion shows it. 0 never hides it.
116117 Cursor struct{ HideAfter time.Duration }
@@ -146,20 +147,35 @@ type Config struct {
146147 }
147148 }
148149
149-// TouchpadConfig configures touchpads. NaturalScroll moves the content with
150-// the fingers, for two-finger scroll and three-finger swipes. Tap clicks on
151-// a tap: one finger left, two right, three middle. AccelSpeed (-1 to 1) and
152-// AccelProfile (AccelAdaptive or AccelFlat) set the pointer speed;
153-// ScrollFactor multiplies two-finger scroll.
154-type TouchpadConfig struct {
150+// PointerConfig holds the settings mice and touchpads share. NaturalScroll
151+// moves the content with the fingers or the wheel instead of against them.
152+// AccelSpeed (-1 to 1) and AccelProfile (AccelAdaptive or AccelFlat) set the
153+// pointer speed. LeftHanded swaps the left and right buttons.
154+type PointerConfig struct {
155155 NaturalScroll bool
156- Tap bool
157156 AccelSpeed float64
158157 AccelProfile string
159- ScrollFactor float64
158+ LeftHanded bool
159+}
160+
161+// TouchpadConfig configures touchpads: the shared pointer settings
162+// (NaturalScroll also applies to two-finger scroll and swipes), plus Tap,
163+// which clicks on a tap (one finger left, two right, three middle), and
164+// ScrollFactor, which multiplies two-finger scroll.
165+type TouchpadConfig struct {
166+ PointerConfig
167+ Tap bool
168+ ScrollFactor float64
169+}
170+
171+// InputDevicesConfig is the live configuration of the input devices, sent to
172+// the input adapter as one value. It is comparable.
173+type InputDevicesConfig struct {
174+ Touchpad TouchpadConfig
175+ Mouse PointerConfig
160176 }
161177
162-// Touchpad pointer acceleration profiles.
178+// Pointer acceleration profiles, for mice and touchpads.
163179 const (
164180 AccelAdaptive = "adaptive"
165181 AccelFlat = "flat"
@@ -110,7 +110,8 @@ type Config struct {
110 LayoutRules110 LayoutRules
111 Outputs []OutputLayout111 Outputs []OutputLayout
112 }112 }
113- Touchpad TouchpadConfig113+ // InputDevicesConfig holds Touchpad and Mouse, promoted.
114+ InputDevicesConfig
114 // Cursor.HideAfter hides the pointer cursor after this long without115 // Cursor.HideAfter hides the pointer cursor after this long without
115 // motion; the next motion shows it. 0 never hides it.116 // motion; the next motion shows it. 0 never hides it.
116 Cursor struct{ HideAfter time.Duration }117 Cursor struct{ HideAfter time.Duration }
@@ -146,20 +147,35 @@ type Config struct {
146 }147 }
147 }148 }
148 149
149-// TouchpadConfig configures touchpads. NaturalScroll moves the content with150+// PointerConfig holds the settings mice and touchpads share. NaturalScroll
150-// the fingers, for two-finger scroll and three-finger swipes. Tap clicks on151+// moves the content with the fingers or the wheel instead of against them.
151-// a tap: one finger left, two right, three middle. AccelSpeed (-1 to 1) and152+// AccelSpeed (-1 to 1) and AccelProfile (AccelAdaptive or AccelFlat) set the
152-// AccelProfile (AccelAdaptive or AccelFlat) set the pointer speed;153+// pointer speed. LeftHanded swaps the left and right buttons.
153-// ScrollFactor multiplies two-finger scroll.154+type PointerConfig struct {
154-type TouchpadConfig struct {
155 NaturalScroll bool155 NaturalScroll bool
156- Tap bool
157 AccelSpeed float64156 AccelSpeed float64
158 AccelProfile string157 AccelProfile string
159- ScrollFactor float64158+ LeftHanded bool
159+}
160+
161+// TouchpadConfig configures touchpads: the shared pointer settings
162+// (NaturalScroll also applies to two-finger scroll and swipes), plus Tap,
163+// which clicks on a tap (one finger left, two right, three middle), and
164+// ScrollFactor, which multiplies two-finger scroll.
165+type TouchpadConfig struct {
166+ PointerConfig
167+ Tap bool
168+ ScrollFactor float64
169+}
170+
171+// InputDevicesConfig is the live configuration of the input devices, sent to
172+// the input adapter as one value. It is comparable.
173+type InputDevicesConfig struct {
174+ Touchpad TouchpadConfig
175+ Mouse PointerConfig
160 }176 }
161 177
162-// Touchpad pointer acceleration profiles.178+// Pointer acceleration profiles, for mice and touchpads.
163 const (179 const (
164 AccelAdaptive = "adaptive"180 AccelAdaptive = "adaptive"
165 AccelFlat = "flat"181 AccelFlat = "flat"

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close