diff --git a/completions/bash/riverctl b/completions/bash/riverctl index b5fb34b..3475386 100644 --- a/completions/bash/riverctl +++ b/completions/bash/riverctl @@ -51,7 +51,8 @@ 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"|"unmap") OPTS="-release" ;; + "map") OPTS="-release -repeat" ;; + "unmap") OPTS="-release" ;; "attach-mode") OPTS="top bottom" ;; "focus-follows-cursor") OPTS="disabled normal" ;; "set-cursor-warp") OPTS="disabled on-output-change" ;; diff --git a/completions/fish/riverctl.fish b/completions/fish/riverctl.fish index 3609e57..88be779 100644 --- a/completions/fish/riverctl.fish +++ b/completions/fish/riverctl.fish @@ -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 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' +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 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 f606565..d782340 100644 --- a/completions/zsh/_riverctl +++ b/completions/zsh/_riverctl @@ -133,7 +133,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)' ;; + map) _alternative 'arguments:optional:(-release -repeat)' ;; unmap) _alternative 'arguments:optional:(-release)' ;; attach-mode) _alternative 'arguments:args:(top bottom)' ;; focus-follows-cursor) _alternative 'arguments:args:(disabled normal)' ;; diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd index a589c45..d1d4666 100644 --- a/doc/riverctl.1.scd +++ b/doc/riverctl.1.scd @@ -185,11 +185,13 @@ 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_] _mode_ _modifiers_ _key_ _command_ +*map* [_-release_|_-repeat_] _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 - _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 f0a9d71..ba82180 100644 --- a/river/Keyboard.zig +++ b/river/Keyboard.zig @@ -81,6 +81,8 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa self.seat.handleActivity(); + self.seat.clearRepeatingMapping(); + // Translate libinput keycode -> xkbcommon const keycode = event.keycode + 8; diff --git a/river/Mapping.zig b/river/Mapping.zig index 280c79f..bb284e1 100644 --- a/river/Mapping.zig +++ b/river/Mapping.zig @@ -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 release: bool, +/// When set to true the mapping will be executed repeatedly while key is pressed +repeat: bool, + pub fn init( keysym: xkb.Keysym, modifiers: wlr.Keyboard.ModifierMask, release: bool, + repeat: bool, command_args: []const []const u8, ) !Self { const owned_args = try util.gpa.alloc([:0]u8, command_args.len); @@ -46,6 +50,7 @@ pub fn init( .keysym = keysym, .modifiers = modifiers, .release = release, + .repeat = repeat, .command_args = owned_args, }; } diff --git a/river/Seat.zig b/river/Seat.zig index 7b61827..0a5db7d 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -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 Mapping = @import("Mapping.zig"); const LayerSurface = @import("LayerSurface.zig"); const Output = @import("Output.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 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 /// is currently available for focus. 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), 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.* = .{ // This will be automatically destroyed when the display is destroyed .wlr_seat = try wlr.Seat.create(server.wl_server, name), .focused_output = &server.root.noop_output, + .mapping_repeat_timer = mapping_repeat_timer, }; 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 { self.cursor.deinit(); + self.mapping_repeat_timer.remove(); while (self.keyboards.pop()) |node| { node.data.deinit(); @@ -318,32 +331,61 @@ pub fn handleMapping( released: bool, ) bool { 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) { - // Execute the bound command - const args = mapping.command_args; - var out: ?[]const u8 = null; - defer if (out) |s| util.gpa.free(s); - 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 }); + if (mapping.repeat) { + self.repeating_mapping = mapping; + self.mapping_repeat_timer.timerUpdate(server.config.repeat_delay) catch { + log.err("failed to update mapping repeat timer", .{}); }; } + self.runMappedCommand(mapping); return true; } } 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 /// capabilities. pub fn addDevice(self: *Self, device: *wlr.InputDevice) void { diff --git a/river/command.zig b/river/command.zig index c67f15b..d32101e 100644 --- a/river/command.zig +++ b/river/command.zig @@ -103,6 +103,7 @@ pub const Error = error{ InvalidRgba, InvalidValue, UnknownOption, + ConflictingOptions, OutOfMemory, Other, }; @@ -136,6 +137,7 @@ pub fn errToMsg(err: Error) [:0]const u8 { Error.NoCommand => "no command given", Error.UnknownCommand => "unknown command", Error.UnknownOption => "unknown option", + Error.ConflictingOptions => "options conflict", Error.NotEnoughArguments => "not enough arguments", Error.TooManyArguments => "too many arguments", Error.Overflow => "value out of bounds", diff --git a/river/command/map.zig b/river/command/map.zig index 96e1d25..0ef834a 100644 --- a/river/command/map.zig +++ b/river/command/map.zig @@ -44,19 +44,25 @@ pub fn map( const offset = optionals.i; 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 modifiers = try parseModifiers(allocator, args[2 + offset], out); const keysym = try parseKeysym(allocator, args[3 + offset], out); 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(); if (mappingExists(mode_mappings, modifiers, keysym, optionals.release)) |current| { mode_mappings.items[current].deinit(); mode_mappings.items[current] = new; } 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); } } @@ -200,6 +206,7 @@ fn parseModifiers( const OptionalArgsContainer = struct { i: usize, release: bool, + repeat: bool, }; /// 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 = 0, .release = false, + .repeat = false, }; var i: usize = 0; @@ -219,6 +227,9 @@ fn parseOptionalArgs(args: []const []const u8) OptionalArgsContainer { if (std.mem.eql(u8, arg, "-release")) { parsed.release = true; i += 1; + } else if (std.mem.eql(u8, arg, "-repeat")) { + parsed.repeat = true; + i += 1; } else { // Break if the arg is not an option parsed.i = i; @@ -251,6 +262,11 @@ pub fn unmap( const mode_mappings = &server.config.modes.items[mode_id].mappings; 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); mapping.deinit(); }