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:
parent
6e51a8fcdd
commit
2bdf9e20a5
9 changed files with 92 additions and 22 deletions
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue