command: support repeating keyboard mappings
Repeating mappings are created using the -repeat option to the map
command:
    % riverctl map normal $mod+Mod1 K -repeat move up 10
- repeating is only supported for key press (not -release) mappings
- unlike -release, -repeat does not create distinct mappings: mapping a
  key with -repeat will replace an existing bare mapping and vice-versa
Resolves #306
			
			
This commit is contained in:
		| @ -51,7 +51,8 @@ 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"|"unmap") OPTS="-release" ;; | 			"map") OPTS="-release -repeat" ;; | ||||||
|  | 			"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" ;; | ||||||
| 			"set-cursor-warp") OPTS="disabled on-output-change" ;; | 			"set-cursor-warp") OPTS="disabled on-output-change" ;; | ||||||
|  | |||||||
| @ -90,7 +90,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' | complete -c riverctl -x -n '__fish_seen_subcommand_from map'                  -a '-release -repeat' | ||||||
| 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' | ||||||
|  | |||||||
| @ -133,7 +133,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)' ;; |                 map) _alternative 'arguments:optional:(-release -repeat)' ;; | ||||||
|                 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)' ;; | ||||||
|  | |||||||
| @ -185,11 +185,13 @@ 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_] _mode_ _modifiers_ _key_ _command_ | *map* [_-release_|_-repeat_] _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 | ||||||
|  | 	  be used with -release | ||||||
| 	- _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 (+). | ||||||
|  | |||||||
| @ -81,6 +81,8 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa | |||||||
|  |  | ||||||
|     self.seat.handleActivity(); |     self.seat.handleActivity(); | ||||||
|  |  | ||||||
|  |     self.seat.clearRepeatingMapping(); | ||||||
|  |  | ||||||
|     // Translate libinput keycode -> xkbcommon |     // Translate libinput keycode -> xkbcommon | ||||||
|     const keycode = event.keycode + 8; |     const keycode = event.keycode + 8; | ||||||
|  |  | ||||||
|  | |||||||
| @ -30,10 +30,14 @@ command_args: []const [:0]const u8, | |||||||
| /// 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, | ||||||
|  |  | ||||||
|  | /// When set to true the mapping will be executed repeatedly while key is pressed | ||||||
|  | repeat: bool, | ||||||
|  |  | ||||||
| pub fn init( | pub fn init( | ||||||
|     keysym: xkb.Keysym, |     keysym: xkb.Keysym, | ||||||
|     modifiers: wlr.Keyboard.ModifierMask, |     modifiers: wlr.Keyboard.ModifierMask, | ||||||
|     release: bool, |     release: bool, | ||||||
|  |     repeat: bool, | ||||||
|     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); | ||||||
| @ -46,6 +50,7 @@ pub fn init( | |||||||
|         .keysym = keysym, |         .keysym = keysym, | ||||||
|         .modifiers = modifiers, |         .modifiers = modifiers, | ||||||
|         .release = release, |         .release = release, | ||||||
|  |         .repeat = repeat, | ||||||
|         .command_args = owned_args, |         .command_args = owned_args, | ||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ const DragIcon = @import("DragIcon.zig"); | |||||||
| const Cursor = @import("Cursor.zig"); | const Cursor = @import("Cursor.zig"); | ||||||
| const InputManager = @import("InputManager.zig"); | const InputManager = @import("InputManager.zig"); | ||||||
| const Keyboard = @import("Keyboard.zig"); | const Keyboard = @import("Keyboard.zig"); | ||||||
|  | const Mapping = @import("Mapping.zig"); | ||||||
| const LayerSurface = @import("LayerSurface.zig"); | const LayerSurface = @import("LayerSurface.zig"); | ||||||
| const Output = @import("Output.zig"); | const Output = @import("Output.zig"); | ||||||
| const SeatStatus = @import("SeatStatus.zig"); | const SeatStatus = @import("SeatStatus.zig"); | ||||||
| @ -60,6 +61,12 @@ mode_id: usize = 0, | |||||||
| /// ID of previous keymap mode, used when returning from "locked" mode | /// ID of previous keymap mode, used when returning from "locked" mode | ||||||
| prev_mode_id: usize = 0, | prev_mode_id: usize = 0, | ||||||
|  |  | ||||||
|  | /// Timer for repeating keyboard mappings | ||||||
|  | mapping_repeat_timer: *wl.EventSource, | ||||||
|  |  | ||||||
|  | /// Currently repeating mapping, if any | ||||||
|  | repeating_mapping: ?*const Mapping = null, | ||||||
|  |  | ||||||
| /// Currently focused output, may be the noop output if no real output | /// Currently focused output, may be the noop output if no real output | ||||||
| /// is currently available for focus. | /// is currently available for focus. | ||||||
| focused_output: *Output, | focused_output: *Output, | ||||||
| @ -83,10 +90,15 @@ request_set_primary_selection: wl.Listener(*wlr.Seat.event.RequestSetPrimarySele | |||||||
|     wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection).init(handleRequestSetPrimarySelection), |     wl.Listener(*wlr.Seat.event.RequestSetPrimarySelection).init(handleRequestSetPrimarySelection), | ||||||
|  |  | ||||||
| pub fn init(self: *Self, name: [*:0]const u8) !void { | pub fn init(self: *Self, name: [*:0]const u8) !void { | ||||||
|  |     const event_loop = server.wl_server.getEventLoop(); | ||||||
|  |     const mapping_repeat_timer = try event_loop.addTimer(*Self, handleMappingRepeatTimeout, self); | ||||||
|  |     errdefer mapping_repeat_timer.remove(); | ||||||
|  |  | ||||||
|     self.* = .{ |     self.* = .{ | ||||||
|         // This will be automatically destroyed when the display is destroyed |         // This will be automatically destroyed when the display is destroyed | ||||||
|         .wlr_seat = try wlr.Seat.create(server.wl_server, name), |         .wlr_seat = try wlr.Seat.create(server.wl_server, name), | ||||||
|         .focused_output = &server.root.noop_output, |         .focused_output = &server.root.noop_output, | ||||||
|  |         .mapping_repeat_timer = mapping_repeat_timer, | ||||||
|     }; |     }; | ||||||
|     self.wlr_seat.data = @ptrToInt(self); |     self.wlr_seat.data = @ptrToInt(self); | ||||||
|  |  | ||||||
| @ -100,6 +112,7 @@ pub fn init(self: *Self, name: [*:0]const u8) !void { | |||||||
|  |  | ||||||
| pub fn deinit(self: *Self) void { | pub fn deinit(self: *Self) void { | ||||||
|     self.cursor.deinit(); |     self.cursor.deinit(); | ||||||
|  |     self.mapping_repeat_timer.remove(); | ||||||
|  |  | ||||||
|     while (self.keyboards.pop()) |node| { |     while (self.keyboards.pop()) |node| { | ||||||
|         node.data.deinit(); |         node.data.deinit(); | ||||||
| @ -318,32 +331,61 @@ pub fn handleMapping( | |||||||
|     released: bool, |     released: bool, | ||||||
| ) bool { | ) bool { | ||||||
|     const modes = &server.config.modes; |     const modes = &server.config.modes; | ||||||
|     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 (std.meta.eql(modifiers, mapping.modifiers) and keysym == mapping.keysym and released == mapping.release) { | ||||||
|             // Execute the bound command |             if (mapping.repeat) { | ||||||
|             const args = mapping.command_args; |                 self.repeating_mapping = mapping; | ||||||
|             var out: ?[]const u8 = null; |                 self.mapping_repeat_timer.timerUpdate(server.config.repeat_delay) catch { | ||||||
|             defer if (out) |s| util.gpa.free(s); |                     log.err("failed to update mapping repeat timer", .{}); | ||||||
|             command.run(util.gpa, self, args, &out) catch |err| { |  | ||||||
|                 const failure_message = switch (err) { |  | ||||||
|                     command.Error.Other => out.?, |  | ||||||
|                     else => command.errToMsg(err), |  | ||||||
|                 }; |  | ||||||
|                 std.log.scoped(.command).err("{s}: {s}", .{ args[0], failure_message }); |  | ||||||
|                 return true; |  | ||||||
|             }; |  | ||||||
|             if (out) |s| { |  | ||||||
|                 const stdout = std.io.getStdOut().writer(); |  | ||||||
|                 stdout.print("{s}", .{s}) catch |err| { |  | ||||||
|                     std.log.scoped(.command).err("{s}: write to stdout failed {}", .{ args[0], err }); |  | ||||||
|                 }; |                 }; | ||||||
|             } |             } | ||||||
|  |             self.runMappedCommand(mapping); | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     return false; |     return false; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | fn runMappedCommand(self: *Self, mapping: *const Mapping) void { | ||||||
|  |     var out: ?[]const u8 = null; | ||||||
|  |     defer if (out) |s| util.gpa.free(s); | ||||||
|  |     const args = mapping.command_args; | ||||||
|  |     command.run(util.gpa, self, args, &out) catch |err| { | ||||||
|  |         const failure_message = switch (err) { | ||||||
|  |             command.Error.Other => out.?, | ||||||
|  |             else => command.errToMsg(err), | ||||||
|  |         }; | ||||||
|  |         std.log.scoped(.command).err("{s}: {s}", .{ args[0], failure_message }); | ||||||
|  |         return; | ||||||
|  |     }; | ||||||
|  |     if (out) |s| { | ||||||
|  |         const stdout = std.io.getStdOut().writer(); | ||||||
|  |         stdout.print("{s}", .{s}) catch |err| { | ||||||
|  |             std.log.scoped(.command).err("{s}: write to stdout failed {}", .{ args[0], err }); | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | pub fn clearRepeatingMapping(self: *Self) void { | ||||||
|  |     self.mapping_repeat_timer.timerUpdate(0) catch { | ||||||
|  |         log.err("failed to clear mapping repeat timer", .{}); | ||||||
|  |     }; | ||||||
|  |     self.repeating_mapping = null; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /// Repeat key mapping | ||||||
|  | fn handleMappingRepeatTimeout(self: *Self) callconv(.C) c_int { | ||||||
|  |     if (self.repeating_mapping) |mapping| { | ||||||
|  |         const rate = server.config.repeat_rate; | ||||||
|  |         const ms_delay = if (rate > 0) 1000 / rate else 0; | ||||||
|  |         self.mapping_repeat_timer.timerUpdate(ms_delay) catch { | ||||||
|  |             log.err("failed to update mapping repeat timer", .{}); | ||||||
|  |         }; | ||||||
|  |         self.runMappedCommand(mapping); | ||||||
|  |     } | ||||||
|  |     return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
| /// Add a newly created input device to the seat and update the reported | /// Add a newly created input device to the seat and update the reported | ||||||
| /// capabilities. | /// capabilities. | ||||||
| pub fn addDevice(self: *Self, device: *wlr.InputDevice) void { | pub fn addDevice(self: *Self, device: *wlr.InputDevice) void { | ||||||
|  | |||||||
| @ -103,6 +103,7 @@ pub const Error = error{ | |||||||
|     InvalidRgba, |     InvalidRgba, | ||||||
|     InvalidValue, |     InvalidValue, | ||||||
|     UnknownOption, |     UnknownOption, | ||||||
|  |     ConflictingOptions, | ||||||
|     OutOfMemory, |     OutOfMemory, | ||||||
|     Other, |     Other, | ||||||
| }; | }; | ||||||
| @ -136,6 +137,7 @@ pub fn errToMsg(err: Error) [:0]const u8 { | |||||||
|         Error.NoCommand => "no command given", |         Error.NoCommand => "no command given", | ||||||
|         Error.UnknownCommand => "unknown command", |         Error.UnknownCommand => "unknown command", | ||||||
|         Error.UnknownOption => "unknown option", |         Error.UnknownOption => "unknown option", | ||||||
|  |         Error.ConflictingOptions => "options conflict", | ||||||
|         Error.NotEnoughArguments => "not enough arguments", |         Error.NotEnoughArguments => "not enough arguments", | ||||||
|         Error.TooManyArguments => "too many arguments", |         Error.TooManyArguments => "too many arguments", | ||||||
|         Error.Overflow => "value out of bounds", |         Error.Overflow => "value out of bounds", | ||||||
|  | |||||||
| @ -44,19 +44,25 @@ pub fn map( | |||||||
|     const offset = optionals.i; |     const offset = optionals.i; | ||||||
|     if (args.len - offset < 5) return Error.NotEnoughArguments; |     if (args.len - offset < 5) return Error.NotEnoughArguments; | ||||||
|  |  | ||||||
|  |     if (optionals.release and optionals.repeat) return Error.ConflictingOptions; | ||||||
|  |  | ||||||
|     const mode_id = try modeNameToId(allocator, seat, args[1 + offset], out); |     const mode_id = try modeNameToId(allocator, seat, args[1 + offset], out); | ||||||
|     const modifiers = try parseModifiers(allocator, args[2 + offset], out); |     const modifiers = try parseModifiers(allocator, args[2 + offset], out); | ||||||
|     const keysym = try parseKeysym(allocator, args[3 + offset], out); |     const keysym = try parseKeysym(allocator, args[3 + offset], out); | ||||||
|  |  | ||||||
|     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, args[4 + offset ..]); |     const new = try Mapping.init(keysym, modifiers, optionals.release, optionals.repeat, 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| { | ||||||
|         mode_mappings.items[current].deinit(); |         mode_mappings.items[current].deinit(); | ||||||
|         mode_mappings.items[current] = new; |         mode_mappings.items[current] = new; | ||||||
|     } else { |     } else { | ||||||
|  |         // Repeating mappings borrow the Mapping directly. To prevent a | ||||||
|  |         // possible crash if the Mapping ArrayList is reallocated, stop any | ||||||
|  |         // currently repeating mappings. | ||||||
|  |         seat.clearRepeatingMapping(); | ||||||
|         try mode_mappings.append(new); |         try mode_mappings.append(new); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -200,6 +206,7 @@ fn parseModifiers( | |||||||
| const OptionalArgsContainer = struct { | const OptionalArgsContainer = struct { | ||||||
|     i: usize, |     i: usize, | ||||||
|     release: bool, |     release: bool, | ||||||
|  |     repeat: bool, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /// 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 | ||||||
| @ -212,6 +219,7 @@ fn parseOptionalArgs(args: []const []const u8) 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, | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     var i: usize = 0; |     var i: usize = 0; | ||||||
| @ -219,6 +227,9 @@ fn parseOptionalArgs(args: []const []const u8) OptionalArgsContainer { | |||||||
|         if (std.mem.eql(u8, arg, "-release")) { |         if (std.mem.eql(u8, arg, "-release")) { | ||||||
|             parsed.release = true; |             parsed.release = true; | ||||||
|             i += 1; |             i += 1; | ||||||
|  |         } else if (std.mem.eql(u8, arg, "-repeat")) { | ||||||
|  |             parsed.repeat = true; | ||||||
|  |             i += 1; | ||||||
|         } else { |         } else { | ||||||
|             // Break if the arg is not an option |             // Break if the arg is not an option | ||||||
|             parsed.i = i; |             parsed.i = i; | ||||||
| @ -251,6 +262,11 @@ pub fn unmap( | |||||||
|     const mode_mappings = &server.config.modes.items[mode_id].mappings; |     const mode_mappings = &server.config.modes.items[mode_id].mappings; | ||||||
|     const mapping_idx = mappingExists(mode_mappings, modifiers, keysym, optionals.release) orelse return; |     const mapping_idx = mappingExists(mode_mappings, modifiers, keysym, optionals.release) orelse return; | ||||||
|  |  | ||||||
|  |     // Repeating mappings borrow the Mapping directly. To prevent a possible | ||||||
|  |     // crash if the Mapping ArrayList is reallocated, stop any currently | ||||||
|  |     // repeating mappings. | ||||||
|  |     seat.clearRepeatingMapping(); | ||||||
|  |  | ||||||
|     var mapping = mode_mappings.swapRemove(mapping_idx); |     var mapping = mode_mappings.swapRemove(mapping_idx); | ||||||
|     mapping.deinit(); |     mapping.deinit(); | ||||||
| } | } | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user