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 index 60dde05..0f6eda0 160000 --- a/deps/zig-xkbcommon +++ b/deps/zig-xkbcommon @@ -1 +1 @@ -Subproject commit 60dde0523907df672ec9ca8b9efb25a1c7ca4d82 +Subproject commit 0f6eda023e6f52ea001c597fda0a7c3e7a2ccce0 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;