From 0cb7c49cc3a863020c68a94397616630cceaceb4 Mon Sep 17 00:00:00 2001 From: LordMZTE Date: Tue, 20 Feb 2024 21:54:06 +0100 Subject: [PATCH] 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 "", + }); + + // 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 {