Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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 {