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.
This commit is contained in:
Peter Kaplan 2022-02-07 14:51:23 +01:00 committed by Isaac Freund
parent ae349b0ce4
commit 60fdefc3fd
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
11 changed files with 345 additions and 5 deletions

View File

@ -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 \

View File

@ -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'

View File

@ -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'

2
deps/zig-wlroots vendored

@ -1 +1 @@
Subproject commit 49a5f81a71f7b14a3b0e52a5d5d8aa1a9e893bda
Subproject commit 42d08b0b1e50f7f0d142275f89c7e899ca8c78d3

View File

@ -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*

View File

@ -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);
}

View File

@ -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,

108
river/Switch.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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);
}

47
river/SwitchMapping.zig Normal file
View File

@ -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 <https://www.gnu.org/licenses/>.
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);
}

View File

@ -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 },
},

View File

@ -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: