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:
parent
ae349b0ce4
commit
60fdefc3fd
@ -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 \
|
||||
|
@ -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'
|
||||
|
@ -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
2
deps/zig-wlroots
vendored
@ -1 +1 @@
|
||||
Subproject commit 49a5f81a71f7b14a3b0e52a5d5d8aa1a9e893bda
|
||||
Subproject commit 42d08b0b1e50f7f0d142275f89c7e899ca8c78d3
|
@ -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*
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
108
river/Switch.zig
Normal 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
47
river/SwitchMapping.zig
Normal 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);
|
||||
}
|
@ -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 },
|
||||
},
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user