From 60fdefc3fdecb7e4d3697634e91b07356a3a5b4d Mon Sep 17 00:00:00 2001 From: Peter Kaplan Date: Mon, 7 Feb 2022 14:51:23 +0100 Subject: [PATCH] input: add map-switch/unmap-switch commands This allows running a command on a laptop's lid being opened/closed or a tablet's button/switch being pressed/toggled. --- completions/bash/riverctl | 2 + completions/fish/riverctl.fish | 2 + completions/zsh/_riverctl | 2 + deps/zig-wlroots | 2 +- doc/riverctl.1.scd | 21 ++++++ river/Mode.zig | 3 + river/Seat.zig | 37 ++++++++-- river/Switch.zig | 108 ++++++++++++++++++++++++++++ river/SwitchMapping.zig | 47 +++++++++++++ river/command.zig | 2 + river/command/map.zig | 124 +++++++++++++++++++++++++++++++++ 11 files changed, 345 insertions(+), 5 deletions(-) create mode 100644 river/Switch.zig create mode 100644 river/SwitchMapping.zig diff --git a/completions/bash/riverctl b/completions/bash/riverctl index bfee3f1..692dcaa 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -34,8 +34,10 @@ function __riverctl_completion () enter-mode \ map \ map-pointer \ + map-switch \ unmap \ unmap-pointer \ + unmap-switch \ attach-mode \ background-color \ border-color-focused \ diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index 7b2ee76..36cf750 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -45,8 +45,10 @@ complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'declare-mode' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'enter-mode' -d 'Switch to given mode if it exists' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'map' -d 'Run command when key is pressed while modifiers are held down and in the specified mode' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'map-pointer' -d 'Move or resize views when button and modifers are held down while in the specified mode' +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'map-switch ' -d 'Run command when river receives a switch event in the specified mode' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'unmap' -d 'Remove the mapping defined by the arguments' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'unmap-pointer' -d 'Remove the pointer mapping defined by the arguments' +complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'unmap-switch' -d 'Remove the switch mapping defined by the arguments' # Configuration complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'attach-mode' -d 'Configure where new views should attach to the view stack' complete -c riverctl -x -n '__fish_riverctl_complete_arg 1' -a 'background-color' -d 'Set the background color' diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl index 2aa663e..745484d 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -39,8 +39,10 @@ _riverctl_subcommands() 'enter-mode:Switch to given mode if it exists' 'map:Run command when key is pressed while modifiers are held down and in the specified mode' 'map-pointer:Move or resize views when button and modifiers are held down while in the specified mode' + 'map-switch:Run command when river receives a switch event in the specified mode' 'unmap:Remove the mapping defined by the arguments' 'unmap-pointer:Remove the pointer mapping defined by the arguments' + 'unmap-switch:Remove the switch mapping defined by the arguments' # Configuration 'attach-mode:Configure where new views should attach to the view stack' 'background-color:Set the background color' diff --git a/deps/zig-wlroots b/deps/zig-wlroots index 49a5f81..42d08b0 160000 --- a/deps/zig-wlroots +++ b/deps/zig-wlroots @@ -1 +1 @@ -Subproject commit 49a5f81a71f7b14a3b0e52a5d5d8aa1a9e893bda +Subproject commit 42d08b0b1e50f7f0d142275f89c7e899ca8c78d3 diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index d4ba768..fec47dd 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -218,6 +218,20 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ - move-view - resize-view +*map-switch* _mode_ *lid*|*tablet* _state_ _command_ + Run _command_ when river receives a certain switch event. + + - _mode_: name of the mode for which to create the mapping + - _lid_|_tablet_: 'lid switch' and 'tablet mode switch' are supported + - _state_: + - possible states for _lid_: + - close + - open + - possible states for _tablet_: + - on + - off + - _command_: any command that may be run with riverctl + *unmap* [_-release_] _mode_ _modifiers_ _key_ Remove the mapping defined by the arguments: @@ -235,6 +249,13 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_ by a plus sign (+). - _button_: the name of a linux input event code as described above +*unmap-switch* _mode_ *lid*|*tablet* _state_ + Remove the switch mapping defined by the arguments: + + - _mode_: name of the mode for which to remove the mapping + - _lid_|_tablet_: the switch for which to remove the mapping + - _state_: a state as listed above + ## CONFIGURATION *attach-mode* *top*|*bottom* diff --git a/river/Mode.zig b/river/Mode.zig index cb4a4a6..5793c50 100644 --- a/river/Mode.zig +++ b/river/Mode.zig @@ -21,12 +21,15 @@ const util = @import("util.zig"); const Mapping = @import("Mapping.zig"); const PointerMapping = @import("PointerMapping.zig"); +const SwitchMapping = @import("SwitchMapping.zig"); mappings: std.ArrayListUnmanaged(Mapping) = .{}, pointer_mappings: std.ArrayListUnmanaged(PointerMapping) = .{}, +switch_mappings: std.ArrayListUnmanaged(SwitchMapping) = .{}, pub fn deinit(self: *Self) void { for (self.mappings.items) |m| m.deinit(); self.mappings.deinit(util.gpa); self.pointer_mappings.deinit(util.gpa); + self.switch_mappings.deinit(util.gpa); } diff --git a/river/Seat.zig b/river/Seat.zig index 76d4d84..8929813 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -31,6 +31,7 @@ const DragIcon = @import("DragIcon.zig"); const Cursor = @import("Cursor.zig"); const InputManager = @import("InputManager.zig"); const Keyboard = @import("Keyboard.zig"); +const Switch = @import("Switch.zig"); const Mapping = @import("Mapping.zig"); const LayerSurface = @import("LayerSurface.zig"); const Output = @import("Output.zig"); @@ -55,6 +56,9 @@ cursor: Cursor = undefined, /// Mulitple keyboards are handled separately keyboards: std.TailQueue(Keyboard) = .{}, +/// There are two kind of switches: lid switches and tablet mode switches +switches: std.TailQueue(Switch) = .{}, + /// ID of the current keymap mode mode_id: usize = 0, @@ -123,6 +127,11 @@ pub fn deinit(self: *Self) void { util.gpa.destroy(node); } + while (self.switches.pop()) |node| { + node.data.deinit(); + util.gpa.destroy(node); + } + while (self.focus_stack.first) |node| { self.focus_stack.remove(node); util.gpa.destroy(node); @@ -350,17 +359,30 @@ pub fn handleMapping( log.err("failed to update mapping repeat timer", .{}); }; } - self.runMappedCommand(mapping); + self.runCommand(mapping.command_args); return true; } } return false; } -fn runMappedCommand(self: *Self, mapping: *const Mapping) void { +/// Handle any user-defined mapping for switches +pub fn handleSwitchMapping( + self: *Self, + switch_type: Switch.Type, + switch_state: Switch.State, +) void { + const modes = &server.config.modes; + for (modes.items[self.mode_id].switch_mappings.items) |mapping| { + if (std.meta.eql(mapping.switch_type, switch_type) and std.meta.eql(mapping.switch_state, switch_state)) { + self.runCommand(mapping.command_args); + } + } +} + +fn runCommand(self: *Self, args: []const [:0]const u8) void { var out: ?[]const u8 = null; defer if (out) |s| util.gpa.free(s); - const args = mapping.command_args; command.run(self, args, &out) catch |err| { const failure_message = switch (err) { command.Error.Other => out.?, @@ -392,7 +414,7 @@ fn handleMappingRepeatTimeout(self: *Self) callconv(.C) c_int { self.mapping_repeat_timer.timerUpdate(ms_delay) catch { log.err("failed to update mapping repeat timer", .{}); }; - self.runMappedCommand(mapping); + self.runCommand(mapping.command_args); } return 0; } @@ -403,6 +425,7 @@ pub fn addDevice(self: *Self, device: *wlr.InputDevice) void { switch (device.type) { .keyboard => self.addKeyboard(device) catch return, .pointer => self.addPointer(device), + .switch_device => self.addSwitch(device) catch return, else => return, } @@ -438,6 +461,12 @@ fn addPointer(self: Self, device: *wlr.InputDevice) void { self.cursor.wlr_cursor.attachInputDevice(device); } +fn addSwitch(self: *Self, device: *wlr.InputDevice) !void { + const node = try util.gpa.create(std.TailQueue(Switch).Node); + node.data.init(self, device); + self.switches.append(node); +} + fn handleRequestSetSelection( listener: *wl.Listener(*wlr.Seat.event.RequestSetSelection), event: *wlr.Seat.event.RequestSetSelection, diff --git a/river/Switch.zig b/river/Switch.zig new file mode 100644 index 0000000..23667a3 --- /dev/null +++ b/river/Switch.zig @@ -0,0 +1,108 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2022 The River Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); +const wlr = @import("wlroots"); +const wl = @import("wayland").server.wl; + +const server = &@import("main.zig").server; +const util = @import("util.zig"); + +const Seat = @import("Seat.zig"); + +const log = std.log.scoped(.switch_device); + +pub const Type = enum { + lid, + tablet, +}; + +pub const State = union(Type) { + lid: LidState, + tablet: TabletState, +}; + +pub const LidState = enum { + open, + close, +}; + +pub const TabletState = enum { + off, + on, +}; + +seat: *Seat, +input_device: *wlr.InputDevice, + +switch_device: wl.Listener(*wlr.Switch.event.Toggle) = wl.Listener(*wlr.Switch.event.Toggle).init(handleToggle), +destroy: wl.Listener(*wlr.Switch) = wl.Listener(*wlr.Switch).init(handleDestroy), + +pub fn init(self: *Self, seat: *Seat, input_device: *wlr.InputDevice) void { + self.* = .{ + .seat = seat, + .input_device = input_device, + }; + + const wlr_switch = self.input_device.device.switch_device; + + wlr_switch.events.toggle.add(&self.switch_device); +} + +pub fn deinit(self: *Self) void { + self.destroy.link.remove(); +} + +fn handleToggle(listener: *wl.Listener(*wlr.Switch.event.Toggle), event: *wlr.Switch.event.Toggle) void { + // This event is raised when the lid witch or the tablet mode switch is toggled. + const self = @fieldParentPtr(Self, "switch_device", listener); + + self.seat.handleActivity(); + + var switch_type: Type = undefined; + var switch_state: State = undefined; + switch (event.switch_type) { + .lid => { + switch_type = .lid; + switch_state = switch (event.switch_state) { + .off => .{ .lid = .open }, + .on => .{ .lid = .close }, + .toggle => unreachable, + }; + }, + .tablet_mode => { + switch_type = .tablet; + switch_state = switch (event.switch_state) { + .off => .{ .tablet = .off }, + .on => .{ .tablet = .on }, + .toggle => unreachable, + }; + }, + } + + self.seat.handleSwitchMapping(switch_type, switch_state); +} + +fn handleDestroy(listener: *wl.Listener(*wlr.Switch), _: *wlr.Switch) void { + const self = @fieldParentPtr(Self, "destroy", listener); + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + + self.seat.switches.remove(node); + self.deinit(); + util.gpa.destroy(node); +} diff --git a/river/SwitchMapping.zig b/river/SwitchMapping.zig new file mode 100644 index 0000000..9346780 --- /dev/null +++ b/river/SwitchMapping.zig @@ -0,0 +1,47 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2022 The River Developers +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 3. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const Switch = @import("Switch.zig"); +const util = @import("util.zig"); + +switch_type: Switch.Type, +switch_state: Switch.State, +command_args: []const [:0]const u8, + +pub fn init( + switch_type: Switch.Type, + switch_state: Switch.State, + command_args: []const []const u8, +) !Self { + const owned_args = try util.gpa.alloc([:0]u8, command_args.len); + errdefer util.gpa.free(owned_args); + for (command_args) |arg, i| { + errdefer for (owned_args[0..i]) |a| util.gpa.free(a); + owned_args[i] = try util.gpa.dupeZ(u8, arg); + } + return Self{ + .switch_type = switch_type, + .switch_state = switch_state, + .command_args = owned_args, + }; +} + +pub fn deinit(self: Self) void { + for (self.command_args) |arg| util.gpa.free(arg); + util.gpa.free(self.command_args); +} diff --git a/river/command.zig b/river/command.zig index 0a0bcc4..1606f06 100644 --- a/river/command.zig +++ b/river/command.zig @@ -64,6 +64,7 @@ const command_impls = std.ComptimeStringMap( .{ "list-inputs", @import("command/input.zig").listInputs }, .{ "map", @import("command/map.zig").map }, .{ "map-pointer", @import("command/map.zig").mapPointer }, + .{ "map-switch", @import("command/map.zig").mapSwitch }, .{ "move", @import("command/move.zig").move }, .{ "output-layout", @import("command/layout.zig").outputLayout }, .{ "resize", @import("command/move.zig").resize }, @@ -84,6 +85,7 @@ const command_impls = std.ComptimeStringMap( .{ "toggle-view-tags", @import("command/tags.zig").toggleViewTags }, .{ "unmap", @import("command/map.zig").unmap }, .{ "unmap-pointer", @import("command/map.zig").unmapPointer }, + .{ "unmap-switch", @import("command/map.zig").unmapSwitch }, .{ "xcursor-theme", @import("command/xcursor_theme.zig").xcursorTheme }, .{ "zoom", @import("command/zoom.zig").zoom }, }, diff --git a/river/command/map.zig b/river/command/map.zig index 80abda8..3d549d8 100644 --- a/river/command/map.zig +++ b/river/command/map.zig @@ -27,6 +27,8 @@ const util = @import("../util.zig"); const Error = @import("../command.zig").Error; const Mapping = @import("../Mapping.zig"); const PointerMapping = @import("../PointerMapping.zig"); +const SwitchMapping = @import("../SwitchMapping.zig"); +const Switch = @import("../Switch.zig"); const Seat = @import("../Seat.zig"); /// Create a new mapping for a given mode @@ -77,6 +79,40 @@ pub fn map( } } +/// Create a new switch mapping for a given mode +/// +/// Example: +/// map-switch normal lid close spawn "wlr-randr --output eDP-1 --off" +pub fn mapSwitch( + _: *Seat, + args: []const [:0]const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 5) return Error.NotEnoughArguments; + + const mode_id = try modeNameToId(args[1], out); + const switch_type = try parseSwitchType(args[2], out); + const switch_state = try parseSwitchState(switch_type, args[3], out); + + const new = try SwitchMapping.init(switch_type, switch_state, args[4..]); + errdefer new.deinit(); + + const mode_mappings = &server.config.modes.items[mode_id].switch_mappings; + + if (switchMappingExists(mode_mappings, switch_type, switch_state)) |current| { + mode_mappings.items[current].deinit(); + mode_mappings.items[current] = new; + // Warn user if they overwrote an existing keybinding using riverctl. + out.* = try std.fmt.allocPrint( + util.gpa, + "overwrote an existing keybinding: map-switch {s} {s} {s}", + .{ args[1], args[2], args[3] }, + ); + } else { + try mode_mappings.append(util.gpa, new); + } +} + /// Create a new pointer mapping for a given mode /// /// Example: @@ -148,6 +184,21 @@ fn mappingExists( return null; } +/// Returns the index of the SwitchMapping with matching switch_type and switch_state, if any. +fn switchMappingExists( + switch_mappings: *std.ArrayListUnmanaged(SwitchMapping), + switch_type: Switch.Type, + switch_state: Switch.State, +) ?usize { + for (switch_mappings.items) |mapping, i| { + if (mapping.switch_type == switch_type and std.meta.eql(mapping.switch_state, switch_state)) { + return i; + } + } + + return null; +} + /// Returns the index of the PointerMapping with matching modifiers and event code, if any. fn pointerMappingExists( pointer_mappings: *std.ArrayListUnmanaged(PointerMapping), @@ -210,6 +261,57 @@ fn parseModifiers(modifiers_str: []const u8, out: *?[]const u8) !wlr.Keyboard.Mo return modifiers; } +fn parseSwitchType( + switch_type_str: []const u8, + out: *?[]const u8, +) !Switch.Type { + return std.meta.stringToEnum(Switch.Type, switch_type_str) orelse { + out.* = try std.fmt.allocPrint( + util.gpa, + "invalid switch '{s}', must be 'lid' or 'tablet'", + .{switch_type_str}, + ); + return Error.Other; + }; +} + +fn parseSwitchState( + switch_type: Switch.Type, + switch_state_str: []const u8, + out: *?[]const u8, +) !Switch.State { + switch (switch_type) { + .lid => { + const lid_state = std.meta.stringToEnum( + Switch.LidState, + switch_state_str, + ) orelse { + out.* = try std.fmt.allocPrint( + util.gpa, + "invalid lid state '{s}', must be 'close' or 'open'", + .{switch_state_str}, + ); + return Error.Other; + }; + return Switch.State{ .lid = lid_state }; + }, + .tablet => { + const tablet_state = std.meta.stringToEnum( + Switch.TabletState, + switch_state_str, + ) orelse { + out.* = try std.fmt.allocPrint( + util.gpa, + "invalid tablet state '{s}', must be 'on' or 'off'", + .{switch_state_str}, + ); + return Error.Other; + }; + return Switch.State{ .tablet = tablet_state }; + }, + } +} + const OptionalArgsContainer = struct { i: usize, release: bool, @@ -273,6 +375,28 @@ pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!v mapping.deinit(); } +/// Remove a switch mapping from a given mode +/// +/// Example: +/// unmap-switch normal tablet on +pub fn unmapSwitch( + _: *Seat, + args: []const [:0]const u8, + out: *?[]const u8, +) Error!void { + if (args.len < 4) return Error.NotEnoughArguments; + + const mode_id = try modeNameToId(args[1], out); + const switch_type = try parseSwitchType(args[2], out); + const switch_state = try parseSwitchState(switch_type, args[3], out); + + const mode_mappings = &server.config.modes.items[mode_id].switch_mappings; + const mapping_idx = switchMappingExists(mode_mappings, switch_type, switch_state) orelse return; + + var mapping = mode_mappings.swapRemove(mapping_idx); + mapping.deinit(); +} + /// Remove a pointer mapping for a given mode /// /// Example: