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`.
This commit is contained in:
parent
8f59075bc5
commit
44aaee3a51
@ -56,7 +56,7 @@ function __riverctl_completion ()
|
|||||||
"focus-output"|"focus-view"|"send-to-output"|"swap") OPTS="next previous" ;;
|
"focus-output"|"focus-view"|"send-to-output"|"swap") OPTS="next previous" ;;
|
||||||
"move"|"snap") OPTS="up down left right" ;;
|
"move"|"snap") OPTS="up down left right" ;;
|
||||||
"resize") OPTS="horizontal vertical" ;;
|
"resize") OPTS="horizontal vertical" ;;
|
||||||
"map") OPTS="-release -repeat" ;;
|
"map") OPTS="-release -repeat -layout" ;;
|
||||||
"unmap") OPTS="-release" ;;
|
"unmap") OPTS="-release" ;;
|
||||||
"attach-mode") OPTS="top bottom" ;;
|
"attach-mode") OPTS="top bottom" ;;
|
||||||
"focus-follows-cursor") OPTS="disabled normal" ;;
|
"focus-follows-cursor") OPTS="disabled normal" ;;
|
||||||
|
@ -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 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 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 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 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 attach-mode' -a 'top bottom'
|
||||||
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-follows-cursor' -a 'disabled normal'
|
complete -c riverctl -x -n '__fish_seen_subcommand_from focus-follows-cursor' -a 'disabled normal'
|
||||||
|
@ -169,7 +169,7 @@ _riverctl()
|
|||||||
snap) _alternative 'arguments:args:(up down left right)' ;;
|
snap) _alternative 'arguments:args:(up down left right)' ;;
|
||||||
send-to-output) _alternative 'arguments:args:(next previous)' ;;
|
send-to-output) _alternative 'arguments:args:(next previous)' ;;
|
||||||
swap) _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)' ;;
|
unmap) _alternative 'arguments:optional:(-release)' ;;
|
||||||
attach-mode) _alternative 'arguments:args:(top bottom)' ;;
|
attach-mode) _alternative 'arguments:args:(top bottom)' ;;
|
||||||
focus-follows-cursor) _alternative 'arguments:args:(disabled normal)' ;;
|
focus-follows-cursor) _alternative 'arguments:args:(disabled normal)' ;;
|
||||||
|
2
deps/zig-xkbcommon
vendored
2
deps/zig-xkbcommon
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 60dde0523907df672ec9ca8b9efb25a1c7ca4d82
|
Subproject commit 0f6eda023e6f52ea001c597fda0a7c3e7a2ccce0
|
@ -193,13 +193,19 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_
|
|||||||
*enter-mode* _name_
|
*enter-mode* _name_
|
||||||
Switch to given mode if it exists.
|
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
|
Run _command_ when _key_ is pressed while _modifiers_ are held down
|
||||||
and in the specified _mode_.
|
and in the specified _mode_.
|
||||||
|
|
||||||
- _-release_: if passed activate on key release instead of key press
|
- _-release_: if passed activate on key release instead of key press
|
||||||
- _-repeat_: if passed activate repeatedly until key release; may not
|
- _-repeat_: if passed activate repeatedly until key release; may not
|
||||||
be used with -release
|
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
|
- _mode_: name of the mode for which to create the mapping
|
||||||
- _modifiers_: one or more of the modifiers listed above, separated
|
- _modifiers_: one or more of the modifiers listed above, separated
|
||||||
by a plus sign (+).
|
by a plus sign (+).
|
||||||
|
@ -85,54 +85,40 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
|
|||||||
// Translate libinput keycode -> xkbcommon
|
// Translate libinput keycode -> xkbcommon
|
||||||
const keycode = event.keycode + 8;
|
const keycode = event.keycode + 8;
|
||||||
|
|
||||||
// TODO: These modifiers aren't properly handled, see sway's code
|
|
||||||
const modifiers = wlr_keyboard.getModifiers();
|
const modifiers = wlr_keyboard.getModifiers();
|
||||||
const released = event.state == .released;
|
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;
|
// Hide cursor when typing
|
||||||
|
for (keysyms) |sym| {
|
||||||
// First check translated keysyms as xkb reports them
|
if (server.config.cursor_hide_when_typing == .enabled and
|
||||||
for (wlr_keyboard.xkb_state.?.keyGetSyms(keycode)) |sym| {
|
!released and
|
||||||
if (!released and !isModifier(sym)) non_modifier_pressed = true;
|
!isModifier(sym))
|
||||||
|
{
|
||||||
// Handle builtin mapping only when keys are pressed
|
self.seat.cursor.hide();
|
||||||
if (!released and handleBuiltinMapping(sym)) {
|
|
||||||
handled = true;
|
|
||||||
break;
|
|
||||||
} else if (self.seat.handleMapping(sym, modifiers, released)) {
|
|
||||||
handled = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If not yet handled, check keysyms ignoring modifiers (e.g. 1 instead of !)
|
// Handle builtin mapping, only when keys are pressed
|
||||||
// Important for mappings like Mod+Shift+1
|
for (keysyms) |sym| {
|
||||||
if (!handled) {
|
if (!released and handleBuiltinMapping(sym)) return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!handled) {
|
// Handle user-defined mapping
|
||||||
// Otherwise, we pass it along to the client.
|
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;
|
const wlr_seat = self.seat.wlr_seat;
|
||||||
wlr_seat.setKeyboard(self.input_device);
|
wlr_seat.setKeyboard(self.input_device);
|
||||||
wlr_seat.keyboardNotifyKey(event.time_msec, event.keycode, event.state);
|
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 {
|
fn isModifier(keysym: xkb.Keysym) bool {
|
||||||
|
@ -26,6 +26,10 @@ keysym: xkb.Keysym,
|
|||||||
modifiers: wlr.Keyboard.ModifierMask,
|
modifiers: wlr.Keyboard.ModifierMask,
|
||||||
command_args: []const [:0]const u8,
|
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
|
/// When set to true the mapping will be executed on key release rather than on press
|
||||||
release: bool,
|
release: bool,
|
||||||
|
|
||||||
@ -37,6 +41,7 @@ pub fn init(
|
|||||||
modifiers: wlr.Keyboard.ModifierMask,
|
modifiers: wlr.Keyboard.ModifierMask,
|
||||||
release: bool,
|
release: bool,
|
||||||
repeat: bool,
|
repeat: bool,
|
||||||
|
layout_index: ?u32,
|
||||||
command_args: []const []const u8,
|
command_args: []const []const u8,
|
||||||
) !Self {
|
) !Self {
|
||||||
const owned_args = try util.gpa.alloc([:0]u8, command_args.len);
|
const owned_args = try util.gpa.alloc([:0]u8, command_args.len);
|
||||||
@ -50,6 +55,7 @@ pub fn init(
|
|||||||
.modifiers = modifiers,
|
.modifiers = modifiers,
|
||||||
.release = release,
|
.release = release,
|
||||||
.repeat = repeat,
|
.repeat = repeat,
|
||||||
|
.layout_index = layout_index,
|
||||||
.command_args = owned_args,
|
.command_args = owned_args,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -58,3 +64,61 @@ pub fn deinit(self: Self) void {
|
|||||||
for (self.command_args) |arg| util.gpa.free(arg);
|
for (self.command_args) |arg| util.gpa.free(arg);
|
||||||
util.gpa.free(self.command_args);
|
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;
|
||||||
|
}
|
||||||
|
@ -342,17 +342,20 @@ pub fn handleViewUnmap(self: *Self, view: *View) void {
|
|||||||
if (self.focused == .view and self.focused.view == view) self.focus(null);
|
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
|
/// Returns true if the key was handled
|
||||||
pub fn handleMapping(
|
pub fn handleMapping(
|
||||||
self: *Self,
|
self: *Self,
|
||||||
keysym: xkb.Keysym,
|
keycode: xkb.Keycode,
|
||||||
modifiers: wlr.Keyboard.ModifierMask,
|
modifiers: wlr.Keyboard.ModifierMask,
|
||||||
released: bool,
|
released: bool,
|
||||||
|
xkb_state: *xkb.State,
|
||||||
) bool {
|
) bool {
|
||||||
const modes = &server.config.modes;
|
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| {
|
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) {
|
if (mapping.repeat) {
|
||||||
self.repeating_mapping = mapping;
|
self.repeating_mapping = mapping;
|
||||||
self.mapping_repeat_timer.timerUpdate(server.config.repeat_delay) catch {
|
self.mapping_repeat_timer.timerUpdate(server.config.repeat_delay) catch {
|
||||||
@ -360,10 +363,10 @@ pub fn handleMapping(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.runCommand(mapping.command_args);
|
self.runCommand(mapping.command_args);
|
||||||
return true;
|
handled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle any user-defined mapping for switches
|
/// Handle any user-defined mapping for switches
|
||||||
|
@ -40,7 +40,7 @@ pub fn map(
|
|||||||
args: []const [:0]const u8,
|
args: []const [:0]const u8,
|
||||||
out: *?[]const u8,
|
out: *?[]const u8,
|
||||||
) Error!void {
|
) Error!void {
|
||||||
const optionals = parseOptionalArgs(args[1..]);
|
const optionals = try parseOptionalArgs(args[1..]);
|
||||||
// offset caused by optional arguments
|
// offset caused by optional arguments
|
||||||
const offset = optionals.i;
|
const offset = optionals.i;
|
||||||
if (args.len - offset < 5) return Error.NotEnoughArguments;
|
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 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();
|
errdefer new.deinit();
|
||||||
|
|
||||||
if (mappingExists(mode_mappings, modifiers, keysym, optionals.release)) |current| {
|
if (mappingExists(mode_mappings, modifiers, keysym, optionals.release)) |current| {
|
||||||
@ -316,32 +323,41 @@ const OptionalArgsContainer = struct {
|
|||||||
i: usize,
|
i: usize,
|
||||||
release: bool,
|
release: bool,
|
||||||
repeat: bool,
|
repeat: bool,
|
||||||
|
layout_index: ?u32,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Parses optional args (such as -release) and return the index of the first argument that is
|
/// Parses optional args (such as -release) and return the index of the first argument that is
|
||||||
/// not an optional argument
|
/// not an optional argument
|
||||||
/// Returns an OptionalArgsContainer with the settings set according to the args
|
/// 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
|
// Set to defaults
|
||||||
var parsed = OptionalArgsContainer{
|
var parsed = OptionalArgsContainer{
|
||||||
// i is the number of arguments consumed
|
// i is the number of arguments consumed
|
||||||
.i = 0,
|
.i = 0,
|
||||||
.release = false,
|
.release = false,
|
||||||
.repeat = false,
|
.repeat = false,
|
||||||
|
.layout_index = null,
|
||||||
};
|
};
|
||||||
|
|
||||||
var i: usize = 0;
|
var j: usize = 0;
|
||||||
for (args) |arg| {
|
while (j < args.len) : (j += 1) {
|
||||||
if (mem.eql(u8, arg, "-release")) {
|
if (mem.eql(u8, args[j], "-release")) {
|
||||||
parsed.release = true;
|
parsed.release = true;
|
||||||
i += 1;
|
parsed.i += 1;
|
||||||
} else if (mem.eql(u8, arg, "-repeat")) {
|
} else if (mem.eql(u8, args[j], "-repeat")) {
|
||||||
parsed.repeat = true;
|
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 {
|
} else {
|
||||||
// Break if the arg is not an option
|
// Break if the arg is not an option
|
||||||
parsed.i = i;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -354,7 +370,7 @@ fn parseOptionalArgs(args: []const []const u8) OptionalArgsContainer {
|
|||||||
/// Example:
|
/// Example:
|
||||||
/// unmap normal Mod4+Shift Return
|
/// unmap normal Mod4+Shift Return
|
||||||
pub fn unmap(seat: *Seat, args: []const [:0]const u8, out: *?[]const u8) Error!void {
|
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
|
// offset caused by optional arguments
|
||||||
const offset = optionals.i;
|
const offset = optionals.i;
|
||||||
if (args.len - offset < 4) return Error.NotEnoughArguments;
|
if (args.len - offset < 4) return Error.NotEnoughArguments;
|
||||||
|
Loading…
Reference in New Issue
Block a user