mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-19 22:10:29 +00:00
update to use new RemapSet
This commit is contained in:
@@ -333,7 +333,7 @@ const DerivedConfig = struct {
|
||||
notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish,
|
||||
notify_on_command_finish_action: configpkg.Config.NotifyOnCommandFinishAction,
|
||||
notify_on_command_finish_after: Duration,
|
||||
key_remaps: []const input.KeyRemap,
|
||||
key_remaps: input.KeyRemapSet,
|
||||
|
||||
const Link = struct {
|
||||
regex: oni.Regex,
|
||||
@@ -409,7 +409,7 @@ const DerivedConfig = struct {
|
||||
.notify_on_command_finish = config.@"notify-on-command-finish",
|
||||
.notify_on_command_finish_action = config.@"notify-on-command-finish-action",
|
||||
.notify_on_command_finish_after = config.@"notify-on-command-finish-after",
|
||||
.key_remaps = config.@"key-remap".value.items,
|
||||
.key_remaps = try config.@"key-remap".clone(alloc),
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
// so that the memory is captured from allocs above.
|
||||
@@ -2582,8 +2582,8 @@ pub fn keyEventIsBinding(
|
||||
) bool {
|
||||
// Apply key remappings for consistency with keyCallback
|
||||
var event = event_orig;
|
||||
if (self.config.key_remaps.len > 0) {
|
||||
event.mods = input.KeyRemap.applyRemaps(self.config.key_remaps, event_orig.mods);
|
||||
if (self.config.key_remaps.isRemapped(event_orig.mods)) {
|
||||
event.mods = self.config.key_remaps.apply(event_orig.mods);
|
||||
}
|
||||
|
||||
switch (event.action) {
|
||||
@@ -2620,8 +2620,8 @@ pub fn keyCallback(
|
||||
// Apply key remappings to transform modifiers before any processing.
|
||||
// This allows users to remap modifier keys at the app level.
|
||||
var event = event_orig;
|
||||
if (self.config.key_remaps.len > 0) {
|
||||
event.mods = input.KeyRemap.applyRemaps(self.config.key_remaps, event_orig.mods);
|
||||
if (self.config.key_remaps.isRemapped(event_orig.mods)) {
|
||||
event.mods = self.config.key_remaps.apply(event_orig.mods);
|
||||
}
|
||||
|
||||
// Crash metadata in case we crash in here
|
||||
|
||||
@@ -37,7 +37,7 @@ const RepeatableStringMap = @import("RepeatableStringMap.zig");
|
||||
pub const Path = @import("path.zig").Path;
|
||||
pub const RepeatablePath = @import("path.zig").RepeatablePath;
|
||||
const ClipboardCodepointMap = @import("ClipboardCodepointMap.zig");
|
||||
const KeyRemapSet = @import("key_mods.zig").RemapSet;
|
||||
const KeyRemapSet = @import("../input/key_mods.zig").RemapSet;
|
||||
|
||||
// We do this instead of importing all of terminal/main.zig to
|
||||
// limit the dependency graph. This is important because some things
|
||||
@@ -1796,7 +1796,7 @@ keybind: Keybinds = .{},
|
||||
///
|
||||
/// Currently only supported on macOS. Linux/GTK support is planned for
|
||||
/// a future release.
|
||||
@"key-remap": RepeatableKeyRemap = .{},
|
||||
@"key-remap": KeyRemapSet = .empty,
|
||||
|
||||
/// Horizontal window padding. This applies padding between the terminal cells
|
||||
/// and the left and right window borders. The value is in points, meaning that
|
||||
@@ -4477,6 +4477,9 @@ pub fn finalize(self: *Config) !void {
|
||||
}
|
||||
|
||||
self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0);
|
||||
|
||||
// Finalize key remapping set for efficient lookups
|
||||
self.@"key-remap".finalize();
|
||||
}
|
||||
|
||||
/// Callback for src/cli/args.zig to allow us to handle special cases
|
||||
@@ -8062,112 +8065,6 @@ pub const RepeatableLink = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// RepeatableKeyRemap is used for the key-remap configuration which
|
||||
/// allows remapping modifier keys within Ghostty.
|
||||
pub const RepeatableKeyRemap = struct {
|
||||
const Self = @This();
|
||||
|
||||
value: std.ArrayListUnmanaged(inputpkg.KeyRemap) = .empty,
|
||||
|
||||
pub fn parseCLI(self: *Self, alloc: Allocator, input_: ?[]const u8) !void {
|
||||
// Empty/unset input clears the list
|
||||
const input = input_ orelse "";
|
||||
if (input.len == 0) {
|
||||
self.value.clearRetainingCapacity();
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the key remap
|
||||
const remap = inputpkg.KeyRemap.parse(input) catch |err| switch (err) {
|
||||
error.InvalidFormat => return error.InvalidValue,
|
||||
error.InvalidModifier => return error.InvalidValue,
|
||||
};
|
||||
|
||||
// Reserve space and append
|
||||
try self.value.ensureUnusedCapacity(alloc, 1);
|
||||
self.value.appendAssumeCapacity(remap);
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const Self, alloc: Allocator) Allocator.Error!Self {
|
||||
return .{
|
||||
.value = try self.value.clone(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
/// Compare if two values are equal. Required by Config.
|
||||
pub fn equal(self: Self, other: Self) bool {
|
||||
if (self.value.items.len != other.value.items.len) return false;
|
||||
for (self.value.items, other.value.items) |a, b| {
|
||||
if (!a.equal(b)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Used by Formatter
|
||||
pub fn formatEntry(self: Self, formatter: formatterpkg.EntryFormatter) !void {
|
||||
if (self.value.items.len == 0) {
|
||||
try formatter.formatEntry(void, {});
|
||||
return;
|
||||
}
|
||||
|
||||
for (self.value.items) |item| {
|
||||
// Format as "from=to"
|
||||
var buf: [64]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
writer.print("{s}={s}", .{ @tagName(item.from), @tagName(item.to) }) catch
|
||||
return error.OutOfMemory;
|
||||
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||
}
|
||||
}
|
||||
|
||||
test "RepeatableKeyRemap parseCLI" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: RepeatableKeyRemap = .{};
|
||||
|
||||
try list.parseCLI(alloc, "ctrl=super");
|
||||
try testing.expectEqual(@as(usize, 1), list.value.items.len);
|
||||
try testing.expectEqual(inputpkg.KeyRemap.ModKey.ctrl, list.value.items[0].from);
|
||||
try testing.expectEqual(inputpkg.KeyRemap.ModKey.super, list.value.items[0].to);
|
||||
|
||||
try list.parseCLI(alloc, "alt=shift");
|
||||
try testing.expectEqual(@as(usize, 2), list.value.items.len);
|
||||
}
|
||||
|
||||
test "RepeatableKeyRemap parseCLI clear" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: RepeatableKeyRemap = .{};
|
||||
|
||||
try list.parseCLI(alloc, "ctrl=super");
|
||||
try testing.expectEqual(@as(usize, 1), list.value.items.len);
|
||||
|
||||
// Empty clears the list
|
||||
try list.parseCLI(alloc, "");
|
||||
try testing.expectEqual(@as(usize, 0), list.value.items.len);
|
||||
}
|
||||
|
||||
test "RepeatableKeyRemap parseCLI invalid" {
|
||||
const testing = std.testing;
|
||||
var arena = ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
var list: RepeatableKeyRemap = .{};
|
||||
|
||||
try testing.expectError(error.InvalidValue, list.parseCLI(alloc, "foo=bar"));
|
||||
try testing.expectError(error.InvalidValue, list.parseCLI(alloc, "ctrl"));
|
||||
}
|
||||
};
|
||||
|
||||
/// Options for copy on select behavior.
|
||||
pub const CopyOnSelect = enum {
|
||||
/// Disables copy on select entirely.
|
||||
|
||||
@@ -33,7 +33,6 @@ pub const ScrollMods = mouse.ScrollMods;
|
||||
pub const SplitFocusDirection = Binding.Action.SplitFocusDirection;
|
||||
pub const SplitResizeDirection = Binding.Action.SplitResizeDirection;
|
||||
pub const Trigger = Binding.Trigger;
|
||||
pub const KeyRemap = @import("input/KeyRemap.zig");
|
||||
|
||||
// Keymap is only available on macOS right now. We could implement it
|
||||
// in theory for XKB too on Linux but we don't need it right now.
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
//! Key remapping support for modifier keys within Ghostty.
|
||||
//!
|
||||
//! This module allows users to remap modifier keys (ctrl, alt, shift, super)
|
||||
//! at the application level without affecting system-wide settings.
|
||||
//!
|
||||
//! Syntax: `key-remap = from=to`
|
||||
//!
|
||||
//! Examples:
|
||||
//! key-remap = ctrl=super -- Ctrl acts as Super
|
||||
//! key-remap = left_alt=ctrl -- Left Alt acts as Ctrl
|
||||
//!
|
||||
//! Remapping is one-way and non-transitive:
|
||||
//! - `ctrl=super` means Ctrl→Super, but Super stays Super
|
||||
//! - `ctrl=super` + `alt=ctrl` means Alt→Ctrl (NOT Super)
|
||||
|
||||
const KeyRemap = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const key = @import("key.zig");
|
||||
const Mods = key.Mods;
|
||||
|
||||
from: ModKey,
|
||||
to: ModKey,
|
||||
|
||||
pub const ModKey = enum {
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
super,
|
||||
left_ctrl,
|
||||
left_alt,
|
||||
left_shift,
|
||||
left_super,
|
||||
right_ctrl,
|
||||
right_alt,
|
||||
right_shift,
|
||||
right_super,
|
||||
|
||||
pub fn isGeneric(self: ModKey) bool {
|
||||
return switch (self) {
|
||||
.ctrl, .alt, .shift, .super => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn parse(input: []const u8) ?ModKey {
|
||||
const map = std.StaticStringMap(ModKey).initComptime(.{
|
||||
.{ "ctrl", .ctrl },
|
||||
.{ "control", .ctrl },
|
||||
.{ "alt", .alt },
|
||||
.{ "opt", .alt },
|
||||
.{ "option", .alt },
|
||||
.{ "shift", .shift },
|
||||
.{ "super", .super },
|
||||
.{ "cmd", .super },
|
||||
.{ "command", .super },
|
||||
.{ "left_ctrl", .left_ctrl },
|
||||
.{ "left_control", .left_ctrl },
|
||||
.{ "leftctrl", .left_ctrl },
|
||||
.{ "leftcontrol", .left_ctrl },
|
||||
.{ "left_alt", .left_alt },
|
||||
.{ "left_opt", .left_alt },
|
||||
.{ "left_option", .left_alt },
|
||||
.{ "leftalt", .left_alt },
|
||||
.{ "leftopt", .left_alt },
|
||||
.{ "leftoption", .left_alt },
|
||||
.{ "left_shift", .left_shift },
|
||||
.{ "leftshift", .left_shift },
|
||||
.{ "left_super", .left_super },
|
||||
.{ "left_cmd", .left_super },
|
||||
.{ "left_command", .left_super },
|
||||
.{ "leftsuper", .left_super },
|
||||
.{ "leftcmd", .left_super },
|
||||
.{ "leftcommand", .left_super },
|
||||
.{ "right_ctrl", .right_ctrl },
|
||||
.{ "right_control", .right_ctrl },
|
||||
.{ "rightctrl", .right_ctrl },
|
||||
.{ "rightcontrol", .right_ctrl },
|
||||
.{ "right_alt", .right_alt },
|
||||
.{ "right_opt", .right_alt },
|
||||
.{ "right_option", .right_alt },
|
||||
.{ "rightalt", .right_alt },
|
||||
.{ "rightopt", .right_alt },
|
||||
.{ "rightoption", .right_alt },
|
||||
.{ "right_shift", .right_shift },
|
||||
.{ "rightshift", .right_shift },
|
||||
.{ "right_super", .right_super },
|
||||
.{ "right_cmd", .right_super },
|
||||
.{ "right_command", .right_super },
|
||||
.{ "rightsuper", .right_super },
|
||||
.{ "rightcmd", .right_super },
|
||||
.{ "rightcommand", .right_super },
|
||||
});
|
||||
|
||||
var buf: [32]u8 = undefined;
|
||||
if (input.len > buf.len) return null;
|
||||
const lower = std.ascii.lowerString(&buf, input);
|
||||
return map.get(lower);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn parse(input: []const u8) !KeyRemap {
|
||||
const eql_idx = std.mem.indexOf(u8, input, "=") orelse
|
||||
return error.InvalidFormat;
|
||||
|
||||
const from_str = std.mem.trim(u8, input[0..eql_idx], " \t");
|
||||
const to_str = std.mem.trim(u8, input[eql_idx + 1 ..], " \t");
|
||||
|
||||
if (from_str.len == 0 or to_str.len == 0) {
|
||||
return error.InvalidFormat;
|
||||
}
|
||||
|
||||
const from = ModKey.parse(from_str) orelse return error.InvalidModifier;
|
||||
const to = ModKey.parse(to_str) orelse return error.InvalidModifier;
|
||||
|
||||
return .{ .from = from, .to = to };
|
||||
}
|
||||
|
||||
pub fn apply(self: KeyRemap, mods: Mods) ?Mods {
|
||||
var result = mods;
|
||||
var matched = false;
|
||||
|
||||
switch (self.from) {
|
||||
.ctrl => if (mods.ctrl) {
|
||||
result.ctrl = false;
|
||||
matched = true;
|
||||
},
|
||||
.left_ctrl => if (mods.ctrl and mods.sides.ctrl == .left) {
|
||||
result.ctrl = false;
|
||||
matched = true;
|
||||
},
|
||||
.right_ctrl => if (mods.ctrl and mods.sides.ctrl == .right) {
|
||||
result.ctrl = false;
|
||||
matched = true;
|
||||
},
|
||||
.alt => if (mods.alt) {
|
||||
result.alt = false;
|
||||
matched = true;
|
||||
},
|
||||
.left_alt => if (mods.alt and mods.sides.alt == .left) {
|
||||
result.alt = false;
|
||||
matched = true;
|
||||
},
|
||||
.right_alt => if (mods.alt and mods.sides.alt == .right) {
|
||||
result.alt = false;
|
||||
matched = true;
|
||||
},
|
||||
.shift => if (mods.shift) {
|
||||
result.shift = false;
|
||||
matched = true;
|
||||
},
|
||||
.left_shift => if (mods.shift and mods.sides.shift == .left) {
|
||||
result.shift = false;
|
||||
matched = true;
|
||||
},
|
||||
.right_shift => if (mods.shift and mods.sides.shift == .right) {
|
||||
result.shift = false;
|
||||
matched = true;
|
||||
},
|
||||
.super => if (mods.super) {
|
||||
result.super = false;
|
||||
matched = true;
|
||||
},
|
||||
.left_super => if (mods.super and mods.sides.super == .left) {
|
||||
result.super = false;
|
||||
matched = true;
|
||||
},
|
||||
.right_super => if (mods.super and mods.sides.super == .right) {
|
||||
result.super = false;
|
||||
matched = true;
|
||||
},
|
||||
}
|
||||
|
||||
if (!matched) return null;
|
||||
|
||||
switch (self.to) {
|
||||
.ctrl, .left_ctrl => {
|
||||
result.ctrl = true;
|
||||
result.sides.ctrl = .left;
|
||||
},
|
||||
.right_ctrl => {
|
||||
result.ctrl = true;
|
||||
result.sides.ctrl = .right;
|
||||
},
|
||||
.alt, .left_alt => {
|
||||
result.alt = true;
|
||||
result.sides.alt = .left;
|
||||
},
|
||||
.right_alt => {
|
||||
result.alt = true;
|
||||
result.sides.alt = .right;
|
||||
},
|
||||
.shift, .left_shift => {
|
||||
result.shift = true;
|
||||
result.sides.shift = .left;
|
||||
},
|
||||
.right_shift => {
|
||||
result.shift = true;
|
||||
result.sides.shift = .right;
|
||||
},
|
||||
.super, .left_super => {
|
||||
result.super = true;
|
||||
result.sides.super = .left;
|
||||
},
|
||||
.right_super => {
|
||||
result.super = true;
|
||||
result.sides.super = .right;
|
||||
},
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Apply remaps non-transitively: each remap checks the original mods.
|
||||
pub fn applyRemaps(remaps: []const KeyRemap, mods: Mods) Mods {
|
||||
var result = mods;
|
||||
for (remaps) |remap| {
|
||||
if (remap.apply(mods)) |_| {
|
||||
switch (remap.from) {
|
||||
.ctrl, .left_ctrl, .right_ctrl => result.ctrl = false,
|
||||
.alt, .left_alt, .right_alt => result.alt = false,
|
||||
.shift, .left_shift, .right_shift => result.shift = false,
|
||||
.super, .left_super, .right_super => result.super = false,
|
||||
}
|
||||
switch (remap.to) {
|
||||
.ctrl, .left_ctrl => {
|
||||
result.ctrl = true;
|
||||
result.sides.ctrl = .left;
|
||||
},
|
||||
.right_ctrl => {
|
||||
result.ctrl = true;
|
||||
result.sides.ctrl = .right;
|
||||
},
|
||||
.alt, .left_alt => {
|
||||
result.alt = true;
|
||||
result.sides.alt = .left;
|
||||
},
|
||||
.right_alt => {
|
||||
result.alt = true;
|
||||
result.sides.alt = .right;
|
||||
},
|
||||
.shift, .left_shift => {
|
||||
result.shift = true;
|
||||
result.sides.shift = .left;
|
||||
},
|
||||
.right_shift => {
|
||||
result.shift = true;
|
||||
result.sides.shift = .right;
|
||||
},
|
||||
.super, .left_super => {
|
||||
result.super = true;
|
||||
result.sides.super = .left;
|
||||
},
|
||||
.right_super => {
|
||||
result.super = true;
|
||||
result.sides.super = .right;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn clone(self: KeyRemap, alloc: Allocator) Allocator.Error!KeyRemap {
|
||||
_ = alloc;
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn equal(self: KeyRemap, other: KeyRemap) bool {
|
||||
return self.from == other.from and self.to == other.to;
|
||||
}
|
||||
|
||||
test "ModKey.parse" {
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expectEqual(ModKey.ctrl, ModKey.parse("ctrl").?);
|
||||
try testing.expectEqual(ModKey.ctrl, ModKey.parse("control").?);
|
||||
try testing.expectEqual(ModKey.ctrl, ModKey.parse("CTRL").?);
|
||||
try testing.expectEqual(ModKey.alt, ModKey.parse("alt").?);
|
||||
try testing.expectEqual(ModKey.super, ModKey.parse("cmd").?);
|
||||
try testing.expectEqual(ModKey.left_ctrl, ModKey.parse("left_ctrl").?);
|
||||
try testing.expectEqual(ModKey.right_alt, ModKey.parse("right_alt").?);
|
||||
try testing.expect(ModKey.parse("foo") == null);
|
||||
}
|
||||
|
||||
test "parse" {
|
||||
const testing = std.testing;
|
||||
|
||||
const remap = try parse("ctrl=super");
|
||||
try testing.expectEqual(ModKey.ctrl, remap.from);
|
||||
try testing.expectEqual(ModKey.super, remap.to);
|
||||
|
||||
const spaced = try parse(" ctrl = super ");
|
||||
try testing.expectEqual(ModKey.ctrl, spaced.from);
|
||||
|
||||
try testing.expectError(error.InvalidFormat, parse("ctrl"));
|
||||
try testing.expectError(error.InvalidModifier, parse("foo=bar"));
|
||||
}
|
||||
|
||||
test "apply" {
|
||||
const testing = std.testing;
|
||||
|
||||
const remap = try parse("ctrl=super");
|
||||
const mods = Mods{ .ctrl = true };
|
||||
const result = remap.apply(mods).?;
|
||||
|
||||
try testing.expect(!result.ctrl);
|
||||
try testing.expect(result.super);
|
||||
try testing.expect(remap.apply(Mods{ .alt = true }) == null);
|
||||
}
|
||||
|
||||
test "applyRemaps non-transitive" {
|
||||
const testing = std.testing;
|
||||
|
||||
const remaps = [_]KeyRemap{
|
||||
try parse("ctrl=super"),
|
||||
try parse("alt=ctrl"),
|
||||
};
|
||||
|
||||
const mods = Mods{ .alt = true };
|
||||
const result = applyRemaps(&remaps, mods);
|
||||
|
||||
try testing.expect(!result.alt);
|
||||
try testing.expect(result.ctrl);
|
||||
try testing.expect(!result.super);
|
||||
}
|
||||
@@ -232,8 +232,21 @@ pub const RemapSet = struct {
|
||||
InvalidMod,
|
||||
};
|
||||
|
||||
pub fn deinit(self: *RemapSet, alloc: Allocator) void {
|
||||
self.map.deinit(alloc);
|
||||
/// Parse from CLI input. Required by Config.
|
||||
pub fn parseCLI(self: *RemapSet, alloc: Allocator, input: ?[]const u8) !void {
|
||||
const value = input orelse "";
|
||||
|
||||
// Empty value resets the set
|
||||
if (value.len == 0) {
|
||||
self.map.clearRetainingCapacity();
|
||||
self.mask = .{};
|
||||
return;
|
||||
}
|
||||
|
||||
self.parse(alloc, value) catch |err| switch (err) {
|
||||
error.OutOfMemory => return error.OutOfMemory,
|
||||
error.MissingAssignment, error.InvalidMod => return error.InvalidValue,
|
||||
};
|
||||
}
|
||||
|
||||
/// Parse a modifier remap and add it to the set.
|
||||
@@ -294,6 +307,10 @@ pub const RemapSet = struct {
|
||||
self.mask.update(from_right);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *RemapSet, alloc: Allocator) void {
|
||||
self.map.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Must be called prior to any remappings so that the mapping
|
||||
/// is sorted properly. Otherwise, you will get invalid results.
|
||||
pub fn finalize(self: *RemapSet) void {
|
||||
@@ -317,6 +334,67 @@ pub const RemapSet = struct {
|
||||
self.map.sort(Context{ .keys = self.map.keys() });
|
||||
}
|
||||
|
||||
/// Deep copy of the struct. Required by Config.
|
||||
pub fn clone(self: *const RemapSet, alloc: Allocator) Allocator.Error!RemapSet {
|
||||
return .{
|
||||
.map = try self.map.clone(alloc),
|
||||
.mask = self.mask,
|
||||
};
|
||||
}
|
||||
|
||||
/// Compare if two RemapSets are equal. Required by Config.
|
||||
pub fn equal(self: RemapSet, other: RemapSet) bool {
|
||||
if (self.map.count() != other.map.count()) return false;
|
||||
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const other_value = other.map.get(entry.key_ptr.*) orelse return false;
|
||||
if (!entry.value_ptr.equal(other_value)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Used by Formatter. Required by Config.
|
||||
pub fn formatEntry(self: RemapSet, formatter: anytype) !void {
|
||||
if (self.map.count() == 0) {
|
||||
try formatter.formatEntry(void, {});
|
||||
return;
|
||||
}
|
||||
|
||||
var it = self.map.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const from = entry.key_ptr.*;
|
||||
const to = entry.value_ptr.*;
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&buf);
|
||||
const writer = fbs.writer();
|
||||
formatMod(writer, from) catch return error.OutOfMemory;
|
||||
writer.writeByte('=') catch return error.OutOfMemory;
|
||||
formatMod(writer, to) catch return error.OutOfMemory;
|
||||
try formatter.formatEntry([]const u8, fbs.getWritten());
|
||||
}
|
||||
}
|
||||
|
||||
fn formatMod(writer: anytype, mods: Mods) !void {
|
||||
// Check which mod is set and format it with optional side prefix
|
||||
inline for (.{ "shift", "ctrl", "alt", "super" }) |name| {
|
||||
if (@field(mods, name)) {
|
||||
const side = @field(mods.sides, name);
|
||||
if (side == .right) {
|
||||
try writer.writeAll("right_");
|
||||
} else {
|
||||
// Only write left_ if we need to distinguish
|
||||
// For now, always write left_ if it's a sided mapping
|
||||
try writer.writeAll("left_");
|
||||
}
|
||||
try writer.writeAll(name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a single mode in a single remapping string. E.g.
|
||||
/// `ctrl` or `left_shift`.
|
||||
fn parseMod(input: []const u8) error{InvalidMod}!struct { Mod, ?Mod.Side } {
|
||||
@@ -599,3 +677,144 @@ test "RemapSet: isRemapped checks mask" {
|
||||
try testing.expect(!set.isRemapped(.{ .alt = true }));
|
||||
try testing.expect(!set.isRemapped(.{ .shift = true }));
|
||||
}
|
||||
|
||||
test "RemapSet: clone creates independent copy" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(alloc);
|
||||
|
||||
try set.parse(alloc, "ctrl=super");
|
||||
set.finalize();
|
||||
|
||||
var cloned = try set.clone(alloc);
|
||||
defer cloned.deinit(alloc);
|
||||
|
||||
try testing.expect(set.equal(cloned));
|
||||
try testing.expect(cloned.isRemapped(.{ .ctrl = true }));
|
||||
}
|
||||
|
||||
test "RemapSet: equal compares correctly" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set1: RemapSet = .empty;
|
||||
defer set1.deinit(alloc);
|
||||
|
||||
var set2: RemapSet = .empty;
|
||||
defer set2.deinit(alloc);
|
||||
|
||||
try testing.expect(set1.equal(set2));
|
||||
|
||||
try set1.parse(alloc, "ctrl=super");
|
||||
try testing.expect(!set1.equal(set2));
|
||||
|
||||
try set2.parse(alloc, "ctrl=super");
|
||||
try testing.expect(set1.equal(set2));
|
||||
|
||||
try set1.parse(alloc, "alt=shift");
|
||||
try testing.expect(!set1.equal(set2));
|
||||
}
|
||||
|
||||
test "RemapSet: parseCLI basic" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(alloc);
|
||||
|
||||
try set.parseCLI(alloc, "ctrl=super");
|
||||
try testing.expectEqual(@as(usize, 2), set.map.count());
|
||||
}
|
||||
|
||||
test "RemapSet: parseCLI empty clears" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(alloc);
|
||||
|
||||
try set.parseCLI(alloc, "ctrl=super");
|
||||
try testing.expectEqual(@as(usize, 2), set.map.count());
|
||||
|
||||
try set.parseCLI(alloc, "");
|
||||
try testing.expectEqual(@as(usize, 0), set.map.count());
|
||||
}
|
||||
|
||||
test "RemapSet: parseCLI invalid" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(alloc);
|
||||
|
||||
try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "foo=bar"));
|
||||
try testing.expectError(error.InvalidValue, set.parseCLI(alloc, "ctrl"));
|
||||
}
|
||||
|
||||
test "RemapSet: formatEntry empty" {
|
||||
const testing = std.testing;
|
||||
const formatterpkg = @import("../config/formatter.zig");
|
||||
|
||||
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
const set: RemapSet = .empty;
|
||||
try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer));
|
||||
try testing.expectEqualSlices(u8, "key-remap = \n", buf.written());
|
||||
}
|
||||
|
||||
test "RemapSet: formatEntry single sided" {
|
||||
const testing = std.testing;
|
||||
const formatterpkg = @import("../config/formatter.zig");
|
||||
|
||||
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(testing.allocator);
|
||||
|
||||
try set.parse(testing.allocator, "left_ctrl=super");
|
||||
set.finalize();
|
||||
|
||||
try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer));
|
||||
try testing.expectEqualSlices(u8, "key-remap = left_ctrl=left_super\n", buf.written());
|
||||
}
|
||||
|
||||
test "RemapSet: formatEntry unsided creates two entries" {
|
||||
const testing = std.testing;
|
||||
const formatterpkg = @import("../config/formatter.zig");
|
||||
|
||||
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(testing.allocator);
|
||||
|
||||
try set.parse(testing.allocator, "ctrl=super");
|
||||
set.finalize();
|
||||
|
||||
try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer));
|
||||
// Unsided creates both left and right mappings
|
||||
const written = buf.written();
|
||||
try testing.expect(std.mem.indexOf(u8, written, "left_ctrl=left_super") != null);
|
||||
try testing.expect(std.mem.indexOf(u8, written, "right_ctrl=left_super") != null);
|
||||
}
|
||||
|
||||
test "RemapSet: formatEntry right sided" {
|
||||
const testing = std.testing;
|
||||
const formatterpkg = @import("../config/formatter.zig");
|
||||
|
||||
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
|
||||
defer buf.deinit();
|
||||
|
||||
var set: RemapSet = .empty;
|
||||
defer set.deinit(testing.allocator);
|
||||
|
||||
try set.parse(testing.allocator, "left_alt=right_ctrl");
|
||||
set.finalize();
|
||||
|
||||
try set.formatEntry(formatterpkg.entryFormatter("key-remap", &buf.writer));
|
||||
try testing.expectEqualSlices(u8, "key-remap = left_alt=right_ctrl\n", buf.written());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user