git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commita583488405739e5f25cb5a84ecb6104b8eddc25d
parent796432fbf0
authorLordMZTE <[email protected]>
date2024-02-20 21:54
command/input: add map-to-output

 completions/bash/riverctl      |  3 +-
 completions/fish/riverctl.fish |  1 +
 completions/zsh/_riverctl      |  1 +
 deps/zig-wlroots               |  2 +-
 doc/riverctl.1.scd             |  5 ++++
 river/InputConfig.zig          | 65 ++++++++++++++++++++++++++++++++++++++++--
 river/InputManager.zig         | 13 +++++++++
 river/Root.zig                 |  6 ++++
 river/command/input.zig        | 14 +--------
 9 files changed, 92 insertions(+), 18 deletions(-)

diff --git a/completions/bash/riverctl b/completions/bash/riverctl
index fce2d56..84e9a89 100644
--- a/completions/bash/riverctl
+++ b/completions/bash/riverctl
@@ -95,7 +95,8 @@ function __riverctl_completion ()
 				tap \
 				tap-button-map \
 				scroll-method \
-				scroll-button"
+				scroll-button \
+				map-to-output"
 			COMPREPLY=($(compgen -W "${OPTS}" -- "${COMP_WORDS[3]}"))
 		elif [ "${COMP_WORDS[1]}" == "hide-cursor" ]
 		then
diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish
index 115946f..9a577be 100644
--- a/completions/fish/riverctl.fish
+++ b/completions/fish/riverctl.fish
@@ -116,6 +116,7 @@ complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_
 complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'tap-button-map'       -d 'Configure the button mapping for tapping'
 complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-method'        -d 'Set the scroll method'
 complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'scroll-button'        -d 'Set the scroll button'
+complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 3' -a 'map-to-output'        -d 'Map to a given output'
 
 # Subcommands for the subcommands of 'input'
 complete -c riverctl -n '__fish_seen_subcommand_from input; and __fish_riverctl_complete_arg 4; and __fish_seen_subcommand_from drag drag-lock disable-while-typing disable-while-trackpointing middle-emulation natural-scroll left-handed tap' -a 'enabled disabled'
diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl
index 3c7f7e1..84abfb7 100644
--- a/completions/zsh/_riverctl
+++ b/completions/zsh/_riverctl
@@ -123,6 +123,7 @@ _riverctl()
                                 'tap-button-map:Configure the button mapping for tapping'
                                 'scroll-method:Set the scroll method'
                                 'scroll-button:Set the scroll button'
+                                'map-to-output:Map to a given output'
                             )
 
                         _describe -t command 'command' input_subcommands
diff --git a/deps/zig-wlroots b/deps/zig-wlroots
index 2149026..e7e4995 160000
--- a/deps/zig-wlroots
+++ b/deps/zig-wlroots
@@ -1 +1 @@
-Subproject commit 2149026047c95b3cad2eb2bf1e3e9f66447dae97
+Subproject commit e7e49951662d346f0f522124f2138ecf9c44ded9
diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd
index b3d0c72..ea910d4 100644
--- a/doc/riverctl.1.scd
+++ b/doc/riverctl.1.scd
@@ -527,6 +527,11 @@ However note that not every input device supports every property.
 	Set the scroll button of an input device. _button_ is the name of a Linux
 	input event code.
 
+*input* _name_ *map-to-output* _output_|*disabled*
+	Maps the input to a given output. This is valid even if the output isn't
+	currently active and will lead to the device being mapped once it is
+	connected.
+
 # EXAMPLES
 
 Bind Super+Return in normal mode to spawn a *foot*(1) terminal:
diff --git a/river/InputConfig.zig b/river/InputConfig.zig
index 4b6fa37..2b23312 100644
--- a/river/InputConfig.zig
+++ b/river/InputConfig.zig
@@ -214,6 +214,40 @@ pub const ScrollButton = struct {
     }
 };
 
+pub const MapToOutput = struct {
+    output_name: ?[]const u8,
+
+    fn apply(map_to_output: MapToOutput, device: *wlr.InputDevice) void {
+        const output = out: {
+            if (map_to_output.output_name) |name| {
+                var it = server.root.active_outputs.iterator(.forward);
+                while (it.next()) |outp| {
+                    if (mem.eql(u8, mem.span(outp.wlr_output.name), name)) {
+                        break :out outp.wlr_output;
+                    }
+                }
+            }
+
+            break :out null;
+        };
+
+        switch (device.type) {
+            .pointer, .touch, .tablet_tool => {
+                log.debug("mapping input '{s}' -> '{s}'", .{
+                    device.name,
+                    if (output) |o| o.name else "<no output>",
+                });
+
+                // TODO: support multiple seats
+                server.input_manager.defaultSeat().cursor.wlr_cursor.mapInputToOutput(device, output);
+            },
+
+            // These devices do not support being mapped to outputs.
+            .keyboard, .tablet_pad, .switch_device => {},
+        }
+    }
+};
+
 glob: []const u8,
 
 // Note: Field names equal name of the setting in the 'input' command.
@@ -232,9 +266,12 @@ tap: ?TapState = null,
 @"pointer-accel": ?PointerAccel = null,
 @"scroll-method": ?ScrollMethod = null,
 @"scroll-button": ?ScrollButton = null,
+@"map-to-output": MapToOutput = .{ .output_name = null },
 
 pub fn deinit(self: *Self) void {
     util.gpa.free(self.glob);
+    if (self.@"map-to-output".output_name) |name|
+        util.gpa.free(name);
 }
 
 pub fn apply(self: *const Self, device: *InputDevice) void {
@@ -244,9 +281,17 @@ pub fn apply(self: *const Self, device: *InputDevice) void {
     inline for (@typeInfo(Self).Struct.fields) |field| {
         if (comptime mem.eql(u8, field.name, "glob")) continue;
 
-        if (@field(self, field.name)) |setting| {
+        if (@as(if (@typeInfo(field.type) == .Optional)
+            field.type
+        else
+            ?field.type, @field(self, field.name))) |setting|
+        {
             log.debug("applying setting: {s}", .{field.name});
-            setting.apply(libinput_device);
+            if (comptime mem.eql(u8, field.name, "map-to-output")) {
+                setting.apply(device.wlr_device);
+            } else {
+                setting.apply(libinput_device);
+            }
         }
     }
 }
@@ -265,6 +310,13 @@ pub fn parse(self: *Self, setting: []const u8, value: []const u8) !void {
                 const ret = c.libevdev_event_code_from_name(c.EV_KEY, value.ptr);
                 if (ret < 1) return error.InvalidButton;
                 self.@"scroll-button" = ScrollButton{ .button = @intCast(ret) };
+            } else if (comptime mem.eql(u8, field.name, "map-to-output")) {
+                if (self.@"map-to-output".output_name) |name|
+                    util.gpa.free(name);
+
+                self.@"map-to-output" = MapToOutput{
+                    .output_name = if (std.mem.eql(u8, value, "disabled")) null else try util.gpa.dupe(u8, value),
+                };
             } else {
                 const T = @typeInfo(field.type).Optional.child;
                 if (@typeInfo(T) != .Enum) {
@@ -286,7 +338,11 @@ pub fn write(self: *Self, writer: anytype) !void {
 
     inline for (@typeInfo(Self).Struct.fields) |field| {
         if (comptime mem.eql(u8, field.name, "glob")) continue;
-        if (@field(self, field.name)) |setting| {
+        if (@as(if (@typeInfo(field.type) == .Optional)
+            field.type
+        else
+            ?field.type, @field(self, field.name))) |setting|
+        {
             // Special-case the settings which are not enums.
             if (comptime mem.eql(u8, field.name, "pointer-accel")) {
                 try writer.print("\tpointer-accel: {d}\n", .{setting.value});
@@ -294,6 +350,9 @@ pub fn write(self: *Self, writer: anytype) !void {
                 try writer.print("\tscroll-button: {s}\n", .{
                     mem.sliceTo(c.libevdev_event_code_get_name(c.EV_KEY, setting.button), 0),
                 });
+            } else if (comptime mem.eql(u8, field.name, "map-to-output")) {
+                if (setting.output_name) |outp|
+                    try writer.print("\tmap-to-output: {s}\n", .{outp});
             } else {
                 const T = @typeInfo(field.type).Optional.child;
                 if (@typeInfo(T) != .Enum) {
diff --git a/river/InputManager.zig b/river/InputManager.zig
index 5ae93aa..d5682ec 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -138,6 +138,19 @@ pub fn inputAllowed(self: Self, wlr_surface: *wlr.Surface) bool {
         true;
 }
 
+/// Reconfigures all devices' libinput configuration as well as their output mapping.
+/// This is called on outputs being added or removed and on the input configuration being changed.
+pub fn reconfigureDevices(self: *Self) void {
+    var it = self.devices.iterator(.forward);
+    while (it.next()) |device| {
+        for (self.configs.items) |config| {
+            if (@import("globber").match(device.identifier, config.glob)) {
+                config.apply(device);
+            }
+        }
+    }
+}
+
 fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), wlr_device: *wlr.InputDevice) void {
     const self = @fieldParentPtr(Self, "new_input", listener);
 
diff --git a/river/Root.zig b/river/Root.zig
index dcce9c7..b9775d7 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -246,6 +246,8 @@ fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void {
         }
         wlr_output.destroy();
     };
+
+    server.input_manager.reconfigureDevices();
 }
 
 /// Remove the output from root.active_outputs and the output layout.
@@ -334,6 +336,10 @@ pub fn deactivateOutput(root: *Self, output: *Output) void {
         root.notifyLayoutDemandDone();
     }
     while (output.layouts.first) |node| node.data.destroy();
+
+    // We must call reconfigureDevices here to unmap devices that might be mapped to this output
+    // in order to prevent a segfault in wlroots.
+    server.input_manager.reconfigureDevices();
 }
 
 /// Add the output to root.active_outputs and the output layout if it has not
diff --git a/river/command/input.zig b/river/command/input.zig
index 940ff78..09acae7 100644
--- a/river/command/input.zig
+++ b/river/command/input.zig
@@ -112,19 +112,7 @@ pub fn input(
     // add an input configuration at an arbitrary position in the generality
     // ordered list, so the simplest way to ensure the device is configured
     // correctly is to apply all input configurations again, in order.
-    var it = server.input_manager.devices.iterator(.forward);
-    while (it.next()) |device| {
-        // Device does not match the glob given in the command, so its
-        // configuration state after applying all configs again would be
-        // the same.
-        if (!globber.match(device.identifier, args[1])) continue;
-
-        for (server.input_manager.configs.items) |config| {
-            if (globber.match(device.identifier, config.glob)) {
-                config.apply(device);
-            }
-        }
-    }
+    server.input_manager.reconfigureDevices();
 }
 
 fn lessThan(_: void, a: InputConfig, b: InputConfig) bool {