aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Kaplan <peter@pkap.de>2022-04-20 15:38:18 +0200
committerIsaac Freund <mail@isaacfreund.com>2022-04-27 18:31:08 +0200
commit44aaee3a51b971817ccacd1c1dd4897f58dd25e7 (patch)
tree8fa20b4c674cf6c576c5bb296de2716424b1f15e
parent8f59075bc505910207bb803d3530c6daee36577c (diff)
downloadriver-44aaee3a51b971817ccacd1c1dd4897f58dd25e7.tar.gz
river-44aaee3a51b971817ccacd1c1dd4897f58dd25e7.tar.xz
command/map: layout-pinned mappings
e.g. `riverctl map -layout 0 normal Super Y spawn foot` When this mapping is checked against a pressed key, layout 0 will be used to translate the pressed key instead of the currently active layout. The number denotes to an index of the layouts set with `XKB_DEFAULT_LAYOUT`.
-rw-r--r--completions/bash/riverctl2
-rw-r--r--completions/fish/riverctl.fish2
-rw-r--r--completions/zsh/_riverctl2
m---------deps/zig-xkbcommon0
-rw-r--r--doc/riverctl.1.scd8
-rw-r--r--river/Keyboard.zig54
-rw-r--r--river/Mapping.zig64
-rw-r--r--river/Seat.zig13
-rw-r--r--river/command/map.zig40
9 files changed, 130 insertions, 55 deletions
diff --git a/completions/bash/riverctl b/completions/bash/riverctl
index b39cf26..881946f 100644
--- a/completions/bash/riverctl
+++ b/completions/bash/riverctl
@@ -56,7 +56,7 @@ function __riverctl_completion ()
"focus-output"|"focus-view"|"send-to-output"|"swap") OPTS="next previous" ;;
"move"|"snap") OPTS="up down left right" ;;
"resize") OPTS="horizontal vertical" ;;
- "map") OPTS="-release -repeat" ;;
+ "map") OPTS="-release -repeat -layout" ;;
"unmap") OPTS="-release" ;;
"attach-mode") OPTS="top bottom" ;;
"focus-follows-cursor") OPTS="disabled normal" ;;
diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish
index 8b92cb6..7a09063 100644
--- a/completions/fish/riverctl.fish
+++ b/completions/fish/riverctl.fish
@@ -70,7 +70,7 @@ complete -c riverctl -x -n '__fish_seen_subcommand_from resize' -a
complete -c riverctl -x -n '__fish_seen_subcommand_from snap' -a 'up down left right'
complete -c riverctl -x -n '__fish_seen_subcommand_from send-to-output' -a 'next previous'
complete -c riverctl -x -n '__fish_seen_subcommand_from swap' -a 'next previous'
-complete -c riverctl -x -n '__fish_seen_subcommand_from map' -a '-release -repeat'
+complete -c riverctl -x -n '__fish_seen_subcommand_from map' -a '-release -repeat -layout'
complete -c riverctl -x -n '__fish_seen_subcommand_from unmap' -a '-release'
complete -c riverctl -x -n '__fish_seen_subcommand_from attach-mode' -a 'top bottom'
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-follows-cursor' -a 'disabled normal'
diff --git a/completions/zsh/_riverctl b/completions/zsh/_riverctl
index f1c3d6d..8fec8fe 100644
--- a/completions/zsh/_riverctl
+++ b/completions/zsh/_riverctl
@@ -169,7 +169,7 @@ _riverctl()
snap) _alternative 'arguments:args:(up down left right)' ;;
send-to-output) _alternative 'arguments:args:(next previous)' ;;
swap) _alternative 'arguments:args:(next previous)' ;;
- map) _alternative 'arguments:optional:(-release -repeat)' ;;
+ map) _alternative 'arguments:optional:(-release -repeat -layout)' ;;
unmap) _alternative 'arguments:optional:(-release)' ;;
attach-mode) _alternative 'arguments:args:(top bottom)' ;;
focus-follows-cursor) _alternative 'arguments:args:(disabled normal)' ;;
diff --git a/deps/zig-xkbcommon b/deps/zig-xkbcommon
-Subproject 60dde0523907df672ec9ca8b9efb25a1c7ca4d8
+Subproject 0f6eda023e6f52ea001c597fda0a7c3e7a2ccce
diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd
index 0826850..42e1500 100644
--- a/doc/riverctl.1.scd
+++ b/doc/riverctl.1.scd
@@ -193,13 +193,19 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_
*enter-mode* _name_
Switch to given mode if it exists.
-*map* [_-release_|_-repeat_] _mode_ _modifiers_ _key_ _command_
+*map* [_-release_|_-repeat_|_-layout_ _index_] _mode_ _modifiers_ _key_ _command_
Run _command_ when _key_ is pressed while _modifiers_ are held down
and in the specified _mode_.
- _-release_: if passed activate on key release instead of key press
- _-repeat_: if passed activate repeatedly until key release; may not
be used with -release
+ - _-layout_: if passed, a specific layout is pinned to the mapping.
+ When the mapping is checked against a pressed key, this layout is
+ used to translate the key independent of the active layout
+ - _index_: zero-based index of a layout set with the environment variable
+ *XKB_DEFAULT_LAYOUT*; see *river*(1) for an example; if the index is
+ out of range, the _-layout_ option will have no effect
- _mode_: name of the mode for which to create the mapping
- _modifiers_: one or more of the modifiers listed above, separated
by a plus sign (+).
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index ac10234..5a8b941 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -85,54 +85,40 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
// Translate libinput keycode -> xkbcommon
const keycode = event.keycode + 8;
- // TODO: These modifiers aren't properly handled, see sway's code
const modifiers = wlr_keyboard.getModifiers();
const released = event.state == .released;
- var handled = false;
+ const xkb_state = wlr_keyboard.xkb_state orelse return;
+ const keysyms = xkb_state.keyGetSyms(keycode);
- var non_modifier_pressed = false;
-
- // First check translated keysyms as xkb reports them
- for (wlr_keyboard.xkb_state.?.keyGetSyms(keycode)) |sym| {
- if (!released and !isModifier(sym)) non_modifier_pressed = true;
-
- // Handle builtin mapping only when keys are pressed
- if (!released and handleBuiltinMapping(sym)) {
- handled = true;
- break;
- } else if (self.seat.handleMapping(sym, modifiers, released)) {
- handled = true;
+ // Hide cursor when typing
+ for (keysyms) |sym| {
+ if (server.config.cursor_hide_when_typing == .enabled and
+ !released and
+ !isModifier(sym))
+ {
+ self.seat.cursor.hide();
break;
}
}
- // If not yet handled, check keysyms ignoring modifiers (e.g. 1 instead of !)
- // Important for mappings like Mod+Shift+1
- if (!handled) {
- const layout_index = wlr_keyboard.xkb_state.?.keyGetLayout(keycode);
- for (wlr_keyboard.keymap.?.keyGetSymsByLevel(keycode, layout_index, 0)) |sym| {
- // Handle builtin mapping only when keys are pressed
- if (!released and handleBuiltinMapping(sym)) {
- handled = true;
- break;
- } else if (self.seat.handleMapping(sym, modifiers, released)) {
- handled = true;
- break;
- }
- }
+ // Handle builtin mapping, only when keys are pressed
+ for (keysyms) |sym| {
+ if (!released and handleBuiltinMapping(sym)) return;
}
- if (!handled) {
- // Otherwise, we pass it along to the client.
+ // Handle user-defined mapping
+ if (!self.seat.handleMapping(
+ keycode,
+ modifiers,
+ released,
+ xkb_state,
+ )) {
+ // If key was not handled, we pass it along to the client.
const wlr_seat = self.seat.wlr_seat;
wlr_seat.setKeyboard(self.input_device);
wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
}
-
- if (non_modifier_pressed and server.config.cursor_hide_when_typing == .enabled) {
- self.seat.cursor.hide();
- }
}
fn isModifier(keysym: xkb.Keysym) bool {
diff --git a/river/Mapping.zig b/river/Mapping.zig
index e4e98b0..21b32fe 100644
--- a/river/Mapping.zig
+++ b/river/Mapping.zig
@@ -26,6 +26,10 @@ keysym: xkb.Keysym,
modifiers: wlr.Keyboard.ModifierMask,
command_args: []const [:0]const u8,
+// This is set for mappings with layout-pinning
+// If set, the layout with this index is always used to translate the given keycode
+layout_index: ?u32,
+
/// When set to true the mapping will be executed on key release rather than on press
release: bool,
@@ -37,6 +41,7 @@ pub fn init(
modifiers: wlr.Keyboard.ModifierMask,
release: bool,
repeat: bool,
+ layout_index: ?u32,
command_args: []const []const u8,
) !Self {
const owned_args = try util.gpa.alloc([:0]u8, command_args.len);
@@ -50,6 +55,7 @@ pub fn init(
.modifiers = modifiers,
.release = release,
.repeat = repeat,
+ .layout_index = layout_index,
.command_args = owned_args,
};
}
@@ -58,3 +64,61 @@ pub fn deinit(self: Self) void {
for (self.command_args) |arg| util.gpa.free(arg);
util.gpa.free(self.command_args);
}
+
+/// Compare mapping with given keycode, modifiers and keyboard state
+pub fn match(
+ self: Self,
+ keycode: xkb.Keycode,
+ modifiers_raw: wlr.Keyboard.ModifierMask,
+ released: bool,
+ xkb_state: *xkb.State,
+) bool {
+ if (released != self.release) return false;
+
+ const keymap = xkb_state.getKeymap();
+
+ // If the mapping has no pinned layout, use the active layout.
+ // It doesn't matter if the index is out of range, since xkbcommon
+ // will fall back to the active layout if so.
+ const layout_index = self.layout_index orelse xkb_state.keyGetLayout(keycode);
+
+ // Raw keysyms and modifiers as if modifiers didn't change keysyms.
+ // E.g. pressing `Super+Shift 1` does not translate to `Super Exclam`.
+ const keysyms_raw = keymap.keyGetSymsByLevel(
+ keycode,
+ layout_index,
+ 0,
+ );
+
+ if (std.meta.eql(modifiers_raw, self.modifiers)) {
+ for (keysyms_raw) |sym| {
+ if (sym == self.keysym) {
+ return true;
+ }
+ }
+ }
+
+ // Keysyms and modifiers as translated by xkb.
+ // Modifiers used to translate the key are consumed.
+ // E.g. pressing `Super+Shift 1` translates to `Super Exclam`.
+ const keysyms_translated = keymap.keyGetSymsByLevel(
+ keycode,
+ layout_index,
+ xkb_state.keyGetLevel(keycode, layout_index),
+ );
+
+ const consumed = xkb_state.keyGetConsumedMods2(keycode, xkb.ConsumedMode.xkb);
+ const modifiers_translated = @bitCast(
+ wlr.Keyboard.ModifierMask,
+ @bitCast(u32, modifiers_raw) & ~consumed,
+ );
+
+ if (std.meta.eql(modifiers_translated, self.modifiers)) {
+ for (keysyms_translated) |sym| {
+ if (sym == self.keysym) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
diff --git a/river/Seat.zig b/river/Seat.zig
index 8929813..ebe23bb 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -342,17 +342,20 @@ pub fn handleViewUnmap(self: *Self, view: *View) void {
if (self.focused == .view and self.focused.view == view) self.focus(null);
}
-/// Handle any user-defined mapping for the passed keysym and modifiers
+/// Handle any user-defined mapping for passed keycode, modifiers and keyboard state
/// Returns true if the key was handled
pub fn handleMapping(
self: *Self,
- keysym: xkb.Keysym,
+ keycode: xkb.Keycode,
modifiers: wlr.Keyboard.ModifierMask,
released: bool,
+ xkb_state: *xkb.State,
) bool {
const modes = &server.config.modes;
+ // In case more than on mapping matches, all of them are activated
+ var handled = false;
for (modes.items[self.mode_id].mappings.items) |*mapping| {
- if (std.meta.eql(modifiers, mapping.modifiers) and keysym == mapping.keysym and released == mapping.release) {
+ if (mapping.match(keycode, modifiers, released, xkb_state)) {
if (mapping.repeat) {
self.repeating_mapping = mapping;
self.mapping_repeat_timer.timerUpdate(server.config.repeat_delay) catch {
@@ -360,10 +363,10 @@ pub fn handleMapping(
};
}
self.runCommand(mapping.command_args);
- return true;
+ handled = true;
}
}
- return false;
+ return handled;
}
/// Handle any user-defined mapping for switches
diff --git a/river/command/map.zig b/river/command/map.zig
index 3d549d8..4a33503 100644
--- a/river/command/map.zig
+++ b/river/command/map.zig
@@ -40,7 +40,7 @@ pub fn map(
args: []const [:0]const u8,
out: *?[]const u8,
) Error!void {
- const optionals = parseOptionalArgs(args[1..]);
+ const optionals = try parseOptionalArgs(args[1..]);
// offset caused by optional arguments
const offset = optionals.i;
if (args.len - offset < 5) return Error.NotEnoughArguments;
@@ -57,7 +57,14 @@ pub fn map(
const mode_mappings = &server.config.modes.items[mode_id].mappings;
- const new = try Mapping.init(keysym, modifiers, optionals.release, optionals.repeat, args[4 + offset ..]);
+ const new = try Mapping.init(
+ keysym,
+ modifiers,
+ optionals.release,
+ optionals.repeat,
+ optionals.layout_index,
+ args[4 + offset ..],
+ );
errdefer new.deinit();
if (mappingExists(mode_mappings, modifiers, keysym, optionals.release)) |current| {
@@ -316,32 +323,41 @@ const OptionalArgsContainer = struct {
i: usize,
release: bool,
repeat: bool,
+ layout_index: ?u32,
};
/// Parses optional args (such as -release) and return the index of the first argument that is
/// not an optional argument
/// Returns an OptionalArgsContainer with the settings set according to the args
-/// Errors cant occur because it returns as soon as it gets an unknown argument
-fn parseOptionalArgs(args: []const []const u8) OptionalArgsContainer {
+fn parseOptionalArgs(args: []const []const u8) !OptionalArgsContainer {
// Set to defaults
var parsed = OptionalArgsContainer{
// i is the number of arguments consumed
.i = 0,
.release = false,
.repeat = false,
+ .layout_index = null,
};
- var i: usize = 0;
- for (args) |arg| {
- if (mem.eql(u8, arg, "-release")) {
+ var j: usize = 0;
+ while (j < args.len) : (j += 1) {
+ if (mem.eql(u8, args[j], "-release")) {
parsed.release = true;
- i += 1;
- } else if (mem.eql(u8, arg, "-repeat")) {
+ parsed.i += 1;
+ } else if (mem.eql(u8, args[j], "-repeat")) {
parsed.repeat = true;
- i += 1;
+ parsed.i += 1;
+ } else if (mem.eql(u8, args[j], "-layout")) {
+ j += 1;
+ if (j == args.len) return Error.NotEnoughArguments;
+ // To keep things simple here, we do not check if the layout index
+ // is out of range. We rely on xkbcommon to handle this case:
+ // xkbcommon will simply use the active layout instead, leaving
+ // this option without effect
+ parsed.layout_index = try std.fmt.parseInt(u32, args[j], 10);
+ parsed.i += 2;
} else {
// Break if the arg is not an option
- parsed.i = i;
break;
}
}
@@ -354,7 +370,7 @@ fn parseOptionalArgs(args: []const []const u8) OptionalArgsContainer {
/// Example:
/// unmap normal Mod4+Shift Return
pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
- const optionals = parseOptionalArgs(args[1..]);
+ const optionals = try parseOptionalArgs(args[1..]);
// offset caused by optional arguments
const offset = optionals.i;
if (args.len - offset < 4) return Error.NotEnoughArguments;