mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 01:06:08 +03:00
Merge pull request #282 from mitchellh/kitty-keys
Many keyboard input improvements (no Kitty protocol yet)
This commit is contained in:
299
src/Surface.zig
299
src/Surface.zig
@ -142,6 +142,7 @@ const DerivedConfig = struct {
|
||||
confirm_close_surface: bool,
|
||||
mouse_interval: u64,
|
||||
macos_non_native_fullscreen: bool,
|
||||
macos_option_as_alt: bool,
|
||||
|
||||
pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig {
|
||||
var arena = ArenaAllocator.init(alloc_gpa);
|
||||
@ -158,6 +159,7 @@ const DerivedConfig = struct {
|
||||
.confirm_close_surface = config.@"confirm-close-surface",
|
||||
.mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms
|
||||
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
|
||||
.macos_option_as_alt = config.@"macos-option-as-alt",
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
// so that the memory is captured from allocs above.
|
||||
@ -985,7 +987,11 @@ pub fn preeditCallback(self: *Surface, preedit: ?u21) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
pub fn charCallback(self: *Surface, codepoint: u21) !void {
|
||||
pub fn charCallback(
|
||||
self: *Surface,
|
||||
codepoint: u21,
|
||||
mods: input.Mods,
|
||||
) !void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
@ -1000,7 +1006,9 @@ pub fn charCallback(self: *Surface, codepoint: u21) !void {
|
||||
}
|
||||
|
||||
// Critical area
|
||||
{
|
||||
const critical: struct {
|
||||
alt_esc_prefix: bool,
|
||||
} = critical: {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
@ -1013,15 +1021,42 @@ pub fn charCallback(self: *Surface, codepoint: u21) !void {
|
||||
// We want to scroll to the bottom
|
||||
// TODO: detect if we're at the bottom to avoid the render call here.
|
||||
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
||||
|
||||
break :critical .{
|
||||
.alt_esc_prefix = self.io.terminal.modes.alt_esc_prefix,
|
||||
};
|
||||
};
|
||||
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
|
||||
// Prefix our data with ESC if we have alt pressed.
|
||||
var i: u8 = 0;
|
||||
if (mods.alt) alt: {
|
||||
// If the terminal explicitly disabled this feature using mode 1036,
|
||||
// then we don't send the prefix.
|
||||
if (!critical.alt_esc_prefix) {
|
||||
log.debug("alt_esc_prefix disabled with mode, not sending esc prefix", .{});
|
||||
break :alt;
|
||||
}
|
||||
|
||||
// On macOS, we have to opt-in to using alt because option
|
||||
// by default is a unicode character sequence.
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
if (!self.config.macos_option_as_alt) {
|
||||
log.debug("macos_option_as_alt disabled, not sending esc prefix", .{});
|
||||
break :alt;
|
||||
}
|
||||
}
|
||||
|
||||
data[i] = 0x1b;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Ask our IO thread to write the data
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
const len = try std.unicode.utf8Encode(codepoint, &data);
|
||||
const len = try std.unicode.utf8Encode(codepoint, data[i..]);
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_small = .{
|
||||
.data = data,
|
||||
.len = len,
|
||||
.len = len + i,
|
||||
},
|
||||
}, .{ .forever = {} });
|
||||
|
||||
@ -1055,15 +1090,20 @@ pub fn keyCallback(
|
||||
} else |_| {}
|
||||
}
|
||||
|
||||
if (action == .press or action == .repeat) {
|
||||
// Mods for bindings never include caps/num lock.
|
||||
const binding_mods = mods: {
|
||||
var binding_mods = mods;
|
||||
binding_mods.caps_lock = false;
|
||||
binding_mods.num_lock = false;
|
||||
break :mods binding_mods;
|
||||
};
|
||||
// We only handle press events
|
||||
if (action != .press and action != .repeat) return false;
|
||||
|
||||
// Mods for bindings never include caps/num lock.
|
||||
const binding_mods = mods: {
|
||||
var binding_mods = mods;
|
||||
binding_mods.caps_lock = false;
|
||||
binding_mods.num_lock = false;
|
||||
break :mods binding_mods;
|
||||
};
|
||||
|
||||
// Check if we're processing a binding first. If so, that negates
|
||||
// any further key processing.
|
||||
{
|
||||
const binding_action_: ?input.Binding.Action = action: {
|
||||
var trigger: input.Binding.Trigger = .{
|
||||
.mods = binding_mods,
|
||||
@ -1085,86 +1125,163 @@ pub fn keyCallback(
|
||||
try self.performBindingAction(binding_action);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle non-printables
|
||||
const char: u8 = char: {
|
||||
const mods_int: u8 = @bitCast(binding_mods);
|
||||
const ctrl_only: u8 = @bitCast(input.Mods{ .ctrl = true });
|
||||
// We'll need to know these values here on.
|
||||
self.renderer_state.mutex.lock();
|
||||
const cursor_key_application = self.io.terminal.modes.cursor_keys;
|
||||
const keypad_key_application = self.io.terminal.modes.keypad_keys;
|
||||
const modify_other_keys = self.io.terminal.modes.modify_other_keys;
|
||||
self.renderer_state.mutex.unlock();
|
||||
|
||||
// If we're only pressing control, check if this is a character
|
||||
// we convert to a non-printable.
|
||||
if (mods_int == ctrl_only) {
|
||||
const val: u8 = switch (key) {
|
||||
.left_bracket => 0x1B,
|
||||
.backslash => 0x1C,
|
||||
.right_bracket => 0x1D,
|
||||
.backspace => 0x08,
|
||||
.a => 0x01,
|
||||
.b => 0x02,
|
||||
.c => 0x03,
|
||||
.d => 0x04,
|
||||
.e => 0x05,
|
||||
.f => 0x06,
|
||||
.g => 0x07,
|
||||
.h => 0x08,
|
||||
.i => 0x09,
|
||||
.j => 0x0A,
|
||||
.k => 0x0B,
|
||||
.l => 0x0C,
|
||||
.m => 0x0D,
|
||||
.n => 0x0E,
|
||||
.o => 0x0F,
|
||||
.p => 0x10,
|
||||
.q => 0x11,
|
||||
.r => 0x12,
|
||||
.s => 0x13,
|
||||
.t => 0x14,
|
||||
.u => 0x15,
|
||||
.v => 0x16,
|
||||
.w => 0x17,
|
||||
.x => 0x18,
|
||||
.y => 0x19,
|
||||
.z => 0x1A,
|
||||
else => 0,
|
||||
};
|
||||
|
||||
if (val > 0) break :char val;
|
||||
}
|
||||
|
||||
// Otherwise, we don't care what modifiers we press we do this.
|
||||
break :char switch (key) {
|
||||
.backspace => 0x7F,
|
||||
.enter => '\r',
|
||||
.tab => '\t',
|
||||
.escape => 0x1B,
|
||||
else => 0,
|
||||
};
|
||||
};
|
||||
if (char > 0) {
|
||||
// Ask our IO thread to write the data
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
data[0] = @intCast(char);
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_small = .{
|
||||
.data = data,
|
||||
.len = 1,
|
||||
},
|
||||
}, .{ .forever = {} });
|
||||
|
||||
// After sending all our messages we have to notify our IO thread
|
||||
try self.io_thread.wakeup.notify();
|
||||
|
||||
// Control charactesr trigger a scroll
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
self.scrollToBottom() catch |err| {
|
||||
log.warn("error scrolling to bottom err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
// Check if we're processing a function key.
|
||||
for (input.function_keys.keys.get(key)) |entry| {
|
||||
switch (entry.cursor) {
|
||||
.any => {},
|
||||
.normal => if (cursor_key_application) continue,
|
||||
.application => if (!cursor_key_application) continue,
|
||||
}
|
||||
|
||||
switch (entry.keypad) {
|
||||
.any => {},
|
||||
.normal => if (keypad_key_application) continue,
|
||||
.application => if (!keypad_key_application) continue,
|
||||
}
|
||||
|
||||
switch (entry.modify_other_keys) {
|
||||
.any => {},
|
||||
.set => if (modify_other_keys) continue,
|
||||
.set_other => if (!modify_other_keys) continue,
|
||||
}
|
||||
|
||||
const mods_int: u8 = @bitCast(binding_mods);
|
||||
const entry_mods_int: u8 = @bitCast(entry.mods);
|
||||
if (entry_mods_int == 0) {
|
||||
if (mods_int != 0 and !entry.mods_empty_is_any) continue;
|
||||
// mods are either empty, or empty means any so we allow it.
|
||||
} else if (entry_mods_int != mods_int) {
|
||||
// any set mods require an exact match
|
||||
continue;
|
||||
}
|
||||
|
||||
// log.debug("function key match: {}", .{entry});
|
||||
|
||||
// We found a match, send the sequence and return we as handled.
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
@memcpy(data[0..entry.sequence.len], entry.sequence);
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_small = .{
|
||||
.data = data,
|
||||
.len = @intCast(entry.sequence.len),
|
||||
},
|
||||
}, .{ .forever = {} });
|
||||
try self.io_thread.wakeup.notify();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we have alt pressed, we're going to prefix any of the
|
||||
// translations below with ESC (0x1B).
|
||||
const alt = binding_mods.alt;
|
||||
const unalt_mods = unalt_mods: {
|
||||
var unalt_mods = binding_mods;
|
||||
unalt_mods.alt = false;
|
||||
break :unalt_mods unalt_mods;
|
||||
};
|
||||
|
||||
// Handle non-printables
|
||||
const char: u8 = char: {
|
||||
const mods_int: u8 = @bitCast(unalt_mods);
|
||||
const ctrl_only: u8 = @bitCast(input.Mods{ .ctrl = true });
|
||||
|
||||
// If we're only pressing control, check if this is a character
|
||||
// we convert to a non-printable. The best table I've found for
|
||||
// this is:
|
||||
// https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-ctrl-mapping-of-ascii-keys
|
||||
//
|
||||
// Note that depending on the apprt, these might be handled as
|
||||
// composed characters. But not all app runtimes will do this;
|
||||
// some only compose printable characters. So we manually handle
|
||||
// this here.
|
||||
if (mods_int != ctrl_only) break :char 0;
|
||||
break :char switch (key) {
|
||||
.space => 0,
|
||||
.slash => 0x1F,
|
||||
.zero => 0x30,
|
||||
.one => 0x31,
|
||||
.two => 0x00,
|
||||
.three => 0x1B,
|
||||
.four => 0x1C,
|
||||
.five => 0x1D,
|
||||
.six => 0x1E,
|
||||
.seven => 0x1F,
|
||||
.eight => 0x7F,
|
||||
.nine => 0x39,
|
||||
.backslash => 0x1C,
|
||||
.left_bracket => 0x1B,
|
||||
.right_bracket => 0x1D,
|
||||
.a => 0x01,
|
||||
.b => 0x02,
|
||||
.c => 0x03,
|
||||
.d => 0x04,
|
||||
.e => 0x05,
|
||||
.f => 0x06,
|
||||
.g => 0x07,
|
||||
.h => 0x08,
|
||||
.i => 0x09,
|
||||
.j => 0x0A,
|
||||
.k => 0x0B,
|
||||
.l => 0x0C,
|
||||
.m => 0x0D,
|
||||
.n => 0x0E,
|
||||
.o => 0x0F,
|
||||
.p => 0x10,
|
||||
.q => 0x11,
|
||||
.r => 0x12,
|
||||
.s => 0x13,
|
||||
.t => 0x14,
|
||||
.u => 0x15,
|
||||
.v => 0x16,
|
||||
.w => 0x17,
|
||||
.x => 0x18,
|
||||
.y => 0x19,
|
||||
.z => 0x1A,
|
||||
else => 0,
|
||||
};
|
||||
};
|
||||
if (char > 0) {
|
||||
// Ask our IO thread to write the data
|
||||
var data: termio.Message.WriteReq.Small.Array = undefined;
|
||||
|
||||
// Write our data. If we need to alt-prefix we add that first.
|
||||
var i: u8 = 0;
|
||||
if (alt) {
|
||||
data[i] = 0x1B;
|
||||
i += 1;
|
||||
}
|
||||
data[i] = @intCast(char);
|
||||
i += 1;
|
||||
|
||||
_ = self.io_thread.mailbox.push(.{
|
||||
.write_small = .{
|
||||
.data = data,
|
||||
.len = i,
|
||||
},
|
||||
}, .{ .forever = {} });
|
||||
|
||||
// After sending all our messages we have to notify our IO thread
|
||||
try self.io_thread.wakeup.notify();
|
||||
|
||||
// Control charactesr trigger a scroll
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
self.scrollToBottom() catch |err| {
|
||||
log.warn("error scrolling to bottom err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -390,13 +390,23 @@ pub const Surface = struct {
|
||||
// We don't handle release events because we don't use them yet.
|
||||
if (action != .press and action != .repeat) return;
|
||||
|
||||
// If we're on macOS and we have macos-option-as-alt enabled,
|
||||
// then we strip the alt modifier from the mods for translation.
|
||||
const translate_mods = translate_mods: {
|
||||
var translate_mods = mods;
|
||||
if (self.app.config.@"macos-option-as-alt")
|
||||
translate_mods.alt = false;
|
||||
|
||||
break :translate_mods translate_mods;
|
||||
};
|
||||
|
||||
// Translate our key using the keymap for our localized keyboard layout.
|
||||
var buf: [128]u8 = undefined;
|
||||
const result = try self.app.keymap.translate(
|
||||
&buf,
|
||||
&self.keymap_state,
|
||||
@intCast(keycode),
|
||||
mods,
|
||||
translate_mods,
|
||||
);
|
||||
|
||||
// If we aren't composing, then we set our preedit to empty no matter what.
|
||||
@ -472,7 +482,7 @@ pub const Surface = struct {
|
||||
|
||||
// Next, we want to call the char callback with each codepoint.
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
self.core_surface.charCallback(cp) catch |err| {
|
||||
self.core_surface.charCallback(cp, mods) catch |err| {
|
||||
log.err("error in char callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -481,7 +491,7 @@ pub const Surface = struct {
|
||||
|
||||
pub fn charCallback(self: *Surface, cp_: u32) void {
|
||||
const cp = std.math.cast(u21, cp_) orelse return;
|
||||
self.core_surface.charCallback(cp) catch |err| {
|
||||
self.core_surface.charCallback(cp, .{}) catch |err| {
|
||||
log.err("error in char callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
@ -285,6 +285,7 @@ pub const Surface = struct {
|
||||
/// This is set to true when keyCallback consumes the input, suppressing
|
||||
/// the charCallback from being fired.
|
||||
key_consumed: bool = false,
|
||||
key_mods: input.Mods = .{},
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
@ -597,7 +598,7 @@ pub const Surface = struct {
|
||||
return;
|
||||
}
|
||||
|
||||
core_win.charCallback(codepoint) catch |err| {
|
||||
core_win.charCallback(codepoint, core_win.rt_surface.key_mods) catch |err| {
|
||||
log.err("error in char callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
@ -610,16 +611,12 @@ pub const Surface = struct {
|
||||
glfw_action: glfw.Action,
|
||||
glfw_mods: glfw.Mods,
|
||||
) void {
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
_ = scancode;
|
||||
|
||||
const core_win = window.getUserPointer(CoreSurface) orelse return;
|
||||
|
||||
// Reset our consumption state
|
||||
core_win.rt_surface.key_consumed = false;
|
||||
|
||||
const tracy = trace(@src());
|
||||
defer tracy.end();
|
||||
|
||||
// Convert our glfw types into our input types
|
||||
const mods: input.Mods = @bitCast(glfw_mods);
|
||||
const action: input.Action = switch (glfw_action) {
|
||||
@ -755,6 +752,7 @@ pub const Surface = struct {
|
||||
|
||||
// TODO: we need to do mapped keybindings
|
||||
|
||||
core_win.rt_surface.key_mods = mods;
|
||||
core_win.rt_surface.key_consumed = core_win.keyCallback(
|
||||
action,
|
||||
key,
|
||||
|
@ -1184,7 +1184,7 @@ pub const Surface = struct {
|
||||
/// in a keypress, we let those automatically work.
|
||||
fn gtkKeyPressed(
|
||||
ec_key: *c.GtkEventControllerKey,
|
||||
_: c.guint,
|
||||
keyval: c.guint,
|
||||
keycode: c.guint,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
ud: ?*anyopaque,
|
||||
@ -1225,8 +1225,9 @@ pub const Surface = struct {
|
||||
break :key input.Key.fromASCII(self.im_buf[0]) orelse physical_key;
|
||||
} else .invalid;
|
||||
|
||||
// log.debug("key pressed key={} physical_key={} composing={} text_len={} mods={}", .{
|
||||
// log.debug("key pressed key={} keyval={x} physical_key={} composing={} text_len={} mods={}", .{
|
||||
// key,
|
||||
// keyval,
|
||||
// physical_key,
|
||||
// self.im_composing,
|
||||
// self.im_len,
|
||||
@ -1273,6 +1274,21 @@ pub const Surface = struct {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If we aren't composing and have no text, we try to convert the keyval
|
||||
// to a text value. We have to do this because GTK will not process
|
||||
// "Ctrl+Shift+1" (on US keyboards) as "Ctrl+!" but instead as "".
|
||||
// But the keyval is set correctly so we can at least extract that.
|
||||
if (self.im_len == 0) {
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
if (keyval_unicode != 0) {
|
||||
if (std.math.cast(u21, keyval_unicode)) |cp| {
|
||||
if (std.unicode.utf8Encode(cp, &self.im_buf)) |len| {
|
||||
self.im_len = len;
|
||||
} else |_| {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Next, we want to call the char callback with each codepoint.
|
||||
if (self.im_len > 0) {
|
||||
const text = self.im_buf[0..self.im_len];
|
||||
@ -1282,7 +1298,7 @@ pub const Surface = struct {
|
||||
};
|
||||
var it = view.iterator();
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
self.core_surface.charCallback(cp) catch |err| {
|
||||
self.core_surface.charCallback(cp, mods) catch |err| {
|
||||
log.err("error in char callback err={}", .{err});
|
||||
return 0;
|
||||
};
|
||||
@ -1390,7 +1406,7 @@ pub const Surface = struct {
|
||||
};
|
||||
var it = view.iterator();
|
||||
while (it.nextCodepoint()) |cp| {
|
||||
self.core_surface.charCallback(cp) catch |err| {
|
||||
self.core_surface.charCallback(cp, .{}) catch |err| {
|
||||
log.err("error in char callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
139
src/config.zig
139
src/config.zig
@ -238,6 +238,19 @@ pub const Config = struct {
|
||||
/// animations.
|
||||
@"macos-non-native-fullscreen": bool = false,
|
||||
|
||||
/// If true, the Option key will be treated as Alt. This makes terminal
|
||||
/// sequences expecting Alt to work properly, but will break Unicode
|
||||
/// input sequences on macOS if you use them via the alt key. You may
|
||||
/// set this to false to restore the macOS alt-key unicode sequences
|
||||
/// but this will break terminal sequences expecting Alt to work.
|
||||
///
|
||||
/// Note that if an Option-sequence doesn't produce a printable
|
||||
/// character, it will be treated as Alt regardless of this setting.
|
||||
/// (i.e. alt+ctrl+a).
|
||||
///
|
||||
/// This does not work with GLFW builds.
|
||||
@"macos-option-as-alt": bool = false,
|
||||
|
||||
/// This is set by the CLI parser for deinit.
|
||||
_arena: ?ArenaAllocator = null,
|
||||
|
||||
@ -308,9 +321,6 @@ pub const Config = struct {
|
||||
errdefer result.deinit();
|
||||
const alloc = result._arena.?.allocator();
|
||||
|
||||
// Add the PC style function keys first so that we can override any later.
|
||||
try result.defaultPCStyleFunctionKeys(alloc);
|
||||
|
||||
// Add our default keybindings
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
@ -632,129 +642,6 @@ pub const Config = struct {
|
||||
return result;
|
||||
}
|
||||
|
||||
fn defaultPCStyleFunctionKeys(result: *Config, alloc: Allocator) !void {
|
||||
// Some control keys
|
||||
try result.keybind.set.put(alloc, .{ .key = .up }, .{ .cursor_key = .{
|
||||
.normal = "\x1b[A",
|
||||
.application = "\x1bOA",
|
||||
} });
|
||||
try result.keybind.set.put(alloc, .{ .key = .down }, .{ .cursor_key = .{
|
||||
.normal = "\x1b[B",
|
||||
.application = "\x1bOB",
|
||||
} });
|
||||
try result.keybind.set.put(alloc, .{ .key = .right }, .{ .cursor_key = .{
|
||||
.normal = "\x1b[C",
|
||||
.application = "\x1bOC",
|
||||
} });
|
||||
try result.keybind.set.put(alloc, .{ .key = .left }, .{ .cursor_key = .{
|
||||
.normal = "\x1b[D",
|
||||
.application = "\x1bOD",
|
||||
} });
|
||||
try result.keybind.set.put(alloc, .{ .key = .home }, .{ .cursor_key = .{
|
||||
.normal = "\x1b[H",
|
||||
.application = "\x1bOH",
|
||||
} });
|
||||
try result.keybind.set.put(alloc, .{ .key = .end }, .{ .cursor_key = .{
|
||||
.normal = "\x1b[F",
|
||||
.application = "\x1bOF",
|
||||
} });
|
||||
|
||||
try result.keybind.set.put(
|
||||
alloc,
|
||||
.{ .key = .tab, .mods = .{ .shift = true } },
|
||||
.{ .csi = "Z" },
|
||||
);
|
||||
|
||||
try result.keybind.set.put(alloc, .{ .key = .insert }, .{ .csi = "2~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .delete }, .{ .csi = "3~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .page_up }, .{ .csi = "5~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .page_down }, .{ .csi = "6~" });
|
||||
|
||||
// From xterm:
|
||||
// Note that F1 through F4 are prefixed with SS3 , while the other keys are
|
||||
// prefixed with CSI . Older versions of xterm implement different escape
|
||||
// sequences for F1 through F4, with a CSI prefix. These can be activated
|
||||
// by setting the oldXtermFKeys resource. However, since they do not
|
||||
// correspond to any hardware terminal, they have been deprecated. (The
|
||||
// DEC VT220 reserves F1 through F5 for local functions such as Setup).
|
||||
try result.keybind.set.put(alloc, .{ .key = .f1 }, .{ .csi = "11~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f2 }, .{ .csi = "12~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f3 }, .{ .csi = "13~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f4 }, .{ .csi = "14~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f5 }, .{ .csi = "15~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f6 }, .{ .csi = "17~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f7 }, .{ .csi = "18~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f8 }, .{ .csi = "19~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f9 }, .{ .csi = "20~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f10 }, .{ .csi = "21~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f11 }, .{ .csi = "23~" });
|
||||
try result.keybind.set.put(alloc, .{ .key = .f12 }, .{ .csi = "24~" });
|
||||
|
||||
// From: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
|
||||
//
|
||||
// In normal mode, i.e., a Sun/PC keyboard when the sunKeyboard resource is
|
||||
// false (and none of the other keyboard resources such as oldXtermFKeys
|
||||
// resource is set), xterm encodes function key modifiers as parameters
|
||||
// appended before the final character of the control sequence. As a
|
||||
// special case, the SS3 sent before F1 through F4 is altered to CSI when
|
||||
// sending a function key modifier as a parameter.
|
||||
//
|
||||
// Code Modifiers
|
||||
// ---------+---------------------------
|
||||
// 2 | Shift
|
||||
// 3 | Alt
|
||||
// 4 | Shift + Alt
|
||||
// 5 | Control
|
||||
// 6 | Shift + Control
|
||||
// 7 | Alt + Control
|
||||
// 8 | Shift + Alt + Control
|
||||
// 9 | Meta
|
||||
// 10 | Meta + Shift
|
||||
// 11 | Meta + Alt
|
||||
// 12 | Meta + Alt + Shift
|
||||
// 13 | Meta + Ctrl
|
||||
// 14 | Meta + Ctrl + Shift
|
||||
// 15 | Meta + Ctrl + Alt
|
||||
// 16 | Meta + Ctrl + Alt + Shift
|
||||
// ---------+---------------------------
|
||||
const modifiers: []const inputpkg.Mods = &.{
|
||||
.{ .shift = true },
|
||||
.{ .alt = true },
|
||||
.{ .shift = true, .alt = true },
|
||||
.{ .ctrl = true },
|
||||
.{ .shift = true, .ctrl = true },
|
||||
.{ .alt = true, .ctrl = true },
|
||||
.{ .shift = true, .alt = true, .ctrl = true },
|
||||
// todo: do we do meta or not?
|
||||
};
|
||||
inline for (modifiers, 2..) |mods, code| {
|
||||
const m: []const u8 = &.{code + 48};
|
||||
const set = &result.keybind.set;
|
||||
try set.put(alloc, .{ .key = .end, .mods = mods }, .{ .csi = "1;" ++ m ++ "F" });
|
||||
try set.put(alloc, .{ .key = .home, .mods = mods }, .{ .csi = "1;" ++ m ++ "H" });
|
||||
try set.put(alloc, .{ .key = .insert, .mods = mods }, .{ .csi = "2;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .delete, .mods = mods }, .{ .csi = "3;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .page_up, .mods = mods }, .{ .csi = "5;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .page_down, .mods = mods }, .{ .csi = "6;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .up, .mods = mods }, .{ .csi = "1;" ++ m ++ "A" });
|
||||
try set.put(alloc, .{ .key = .down, .mods = mods }, .{ .csi = "1;" ++ m ++ "B" });
|
||||
try set.put(alloc, .{ .key = .right, .mods = mods }, .{ .csi = "1;" ++ m ++ "C" });
|
||||
try set.put(alloc, .{ .key = .left, .mods = mods }, .{ .csi = "1;" ++ m ++ "D" });
|
||||
try set.put(alloc, .{ .key = .f1, .mods = mods }, .{ .csi = "11;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f2, .mods = mods }, .{ .csi = "12;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f3, .mods = mods }, .{ .csi = "13;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f4, .mods = mods }, .{ .csi = "14;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f5, .mods = mods }, .{ .csi = "15;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f6, .mods = mods }, .{ .csi = "17;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f7, .mods = mods }, .{ .csi = "18;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f8, .mods = mods }, .{ .csi = "19;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f9, .mods = mods }, .{ .csi = "20;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f10, .mods = mods }, .{ .csi = "21;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f11, .mods = mods }, .{ .csi = "23;" ++ m ++ "~" });
|
||||
try set.put(alloc, .{ .key = .f12, .mods = mods }, .{ .csi = "24;" ++ m ++ "~" });
|
||||
}
|
||||
}
|
||||
|
||||
/// This sets either "ctrl" or "super" to true (but not both)
|
||||
/// on mods depending on if the build target is Mac or not. On
|
||||
/// Mac, we default to super (i.e. super+c for copy) and on
|
||||
|
@ -3,6 +3,7 @@ const builtin = @import("builtin");
|
||||
|
||||
pub usingnamespace @import("input/mouse.zig");
|
||||
pub usingnamespace @import("input/key.zig");
|
||||
pub const function_keys = @import("input/function_keys.zig");
|
||||
pub const keycodes = @import("input/keycodes.zig");
|
||||
pub const Binding = @import("input/Binding.zig");
|
||||
pub const SplitDirection = Binding.Action.SplitDirection;
|
||||
|
287
src/input/function_keys.zig
Normal file
287
src/input/function_keys.zig
Normal file
@ -0,0 +1,287 @@
|
||||
//! This is the list of "PC style function keys" that xterm supports for
|
||||
//! the legacy keyboard protocols. These always take priority since at the
|
||||
//! time of writing this, even the most modern keyboard protocols still
|
||||
//! are backwards compatible with regards to these sequences.
|
||||
//!
|
||||
//! This is based on a variety of sources cross-referenced but mostly
|
||||
//! based on foot's keymap.h: https://codeberg.org/dnkl/foot/src/branch/master/keymap.h
|
||||
|
||||
const std = @import("std");
|
||||
const key = @import("key.zig");
|
||||
|
||||
pub const CursorMode = enum { any, normal, application };
|
||||
pub const KeypadMode = enum { any, normal, application };
|
||||
|
||||
/// A bit confusing so I'll document this one: this is the "modify other keys"
|
||||
/// setting. We only change behavior for "set_other" which is ESC [ 4; 2 m.
|
||||
/// So this can be "any" which means we don't care what's going on. Or it
|
||||
/// can be "set" which means modify keys must be set EXCEPT FOR "other keys"
|
||||
/// mode, and "set_other" which means modify keys must be set to "other keys"
|
||||
/// mode.
|
||||
///
|
||||
/// See: https://invisible-island.net/xterm/modified-keys.html
|
||||
pub const ModifyKeys = enum {
|
||||
any,
|
||||
set,
|
||||
set_other,
|
||||
};
|
||||
|
||||
/// A single entry in the table of keys.
|
||||
pub const Entry = struct {
|
||||
/// The exact set of modifiers that must be active for this entry to match.
|
||||
/// If mods_empty_is_any is true then empty mods means any set of mods can
|
||||
/// match. Otherwise, empty mods means no mods must be active.
|
||||
mods: key.Mods = .{},
|
||||
mods_empty_is_any: bool = true,
|
||||
|
||||
/// The state required for cursor/keypad mode.
|
||||
cursor: CursorMode = .any,
|
||||
keypad: KeypadMode = .any,
|
||||
|
||||
/// Whether or not this entry should be used
|
||||
modify_other_keys: ModifyKeys = .any,
|
||||
|
||||
/// The sequence to send to the pty if this entry matches.
|
||||
sequence: []const u8,
|
||||
};
|
||||
|
||||
/// The list of modifier combinations for modify other key sequences.
|
||||
/// The mode value is index + 2.
|
||||
pub const modifiers: []const key.Mods = &.{
|
||||
.{ .shift = true },
|
||||
.{ .alt = true },
|
||||
.{ .shift = true, .alt = true },
|
||||
.{ .ctrl = true },
|
||||
.{ .shift = true, .ctrl = true },
|
||||
.{ .alt = true, .ctrl = true },
|
||||
.{ .shift = true, .alt = true, .ctrl = true },
|
||||
.{ .super = true },
|
||||
.{ .shift = true, .super = true },
|
||||
.{ .alt = true, .super = true },
|
||||
.{ .shift = true, .alt = true, .super = true },
|
||||
.{ .ctrl = true, .super = true },
|
||||
.{ .shift = true, .ctrl = true, .super = true },
|
||||
.{ .alt = true, .ctrl = true, .super = true },
|
||||
.{ .shift = true, .alt = true, .ctrl = true, .super = true },
|
||||
};
|
||||
|
||||
/// This is the array of entries for the PC style function keys as mapped to
|
||||
/// our set of possible key codes. Not every key has any entries.
|
||||
pub const KeyEntryArray = std.EnumArray(key.Key, []const Entry);
|
||||
|
||||
// The list of keys that we support and their entry values. It is expected
|
||||
// that you'll just use a for loop through the entry values, there are never
|
||||
// more than a couple dozen or so.
|
||||
pub const keys = keys: {
|
||||
var result = KeyEntryArray.initFill(&.{});
|
||||
|
||||
result.set(.up, pcStyle("\x1b[1;{}A") ++ cursorKey("\x1b[A", "\x1bOA"));
|
||||
result.set(.down, pcStyle("\x1b[1;{}B") ++ cursorKey("\x1b[B", "\x1bOB"));
|
||||
result.set(.right, pcStyle("\x1b[1;{}C") ++ cursorKey("\x1b[C", "\x1bOC"));
|
||||
result.set(.left, pcStyle("\x1b[1;{}D") ++ cursorKey("\x1b[D", "\x1bOD"));
|
||||
result.set(.home, pcStyle("\x1b[1;{}H") ++ cursorKey("\x1b[H", "\x1bOH"));
|
||||
result.set(.end, pcStyle("\x1b[1;{}F") ++ cursorKey("\x1b[F", "\x1bOF"));
|
||||
result.set(.insert, pcStyle("\x1b[2;{}~") ++ .{.{ .sequence = "\x1B[2~" }});
|
||||
result.set(.delete, pcStyle("\x1b[3;{}~") ++ .{.{ .sequence = "\x1B[3~" }});
|
||||
result.set(.page_up, pcStyle("\x1b[5;{}~") ++ .{.{ .sequence = "\x1B[5~" }});
|
||||
result.set(.page_down, pcStyle("\x1b[6;{}~") ++ .{.{ .sequence = "\x1B[6~" }});
|
||||
|
||||
// Function Keys. todo: f13-f35 but we need to add to input.Key
|
||||
result.set(.f1, pcStyle("\x1b[11;{}~") ++ .{.{ .sequence = "\x1BOP" }});
|
||||
result.set(.f2, pcStyle("\x1b[12;{}~") ++ .{.{ .sequence = "\x1BOQ" }});
|
||||
result.set(.f3, pcStyle("\x1b[13;{}~") ++ .{.{ .sequence = "\x1BOR" }});
|
||||
result.set(.f4, pcStyle("\x1b[14;{}~") ++ .{.{ .sequence = "\x1BOS" }});
|
||||
result.set(.f5, pcStyle("\x1b[15;{}~") ++ .{.{ .sequence = "\x1B[15~" }});
|
||||
result.set(.f6, pcStyle("\x1b[17;{}~") ++ .{.{ .sequence = "\x1B[17~" }});
|
||||
result.set(.f7, pcStyle("\x1b[18;{}~") ++ .{.{ .sequence = "\x1B[18~" }});
|
||||
result.set(.f8, pcStyle("\x1b[19;{}~") ++ .{.{ .sequence = "\x1B[19~" }});
|
||||
result.set(.f9, pcStyle("\x1b[20;{}~") ++ .{.{ .sequence = "\x1B[20~" }});
|
||||
result.set(.f10, pcStyle("\x1b[21;{}~") ++ .{.{ .sequence = "\x1B[21~" }});
|
||||
result.set(.f11, pcStyle("\x1b[23;{}~") ++ .{.{ .sequence = "\x1B[23~" }});
|
||||
result.set(.f12, pcStyle("\x1b[24;{}~") ++ .{.{ .sequence = "\x1B[24~" }});
|
||||
|
||||
// Keypad keys
|
||||
result.set(.kp_0, kpDefault("p") ++ pcStyle("\x1bO{}p"));
|
||||
result.set(.kp_1, kpDefault("q") ++ pcStyle("\x1bO{}q"));
|
||||
result.set(.kp_2, kpDefault("r") ++ pcStyle("\x1bO{}r"));
|
||||
result.set(.kp_3, kpDefault("s") ++ pcStyle("\x1bO{}s"));
|
||||
result.set(.kp_4, kpDefault("t") ++ pcStyle("\x1bO{}t"));
|
||||
result.set(.kp_5, kpDefault("u") ++ pcStyle("\x1bO{}u"));
|
||||
result.set(.kp_6, kpDefault("v") ++ pcStyle("\x1bO{}v"));
|
||||
result.set(.kp_7, kpDefault("w") ++ pcStyle("\x1bO{}w"));
|
||||
result.set(.kp_8, kpDefault("x") ++ pcStyle("\x1bO{}x"));
|
||||
result.set(.kp_9, kpDefault("y") ++ pcStyle("\x1bO{}y"));
|
||||
result.set(.kp_decimal, kpDefault("n") ++ pcStyle("\x1bO{}n"));
|
||||
result.set(.kp_divide, kpDefault("o") ++ pcStyle("\x1bO{}o"));
|
||||
result.set(.kp_multiply, kpDefault("j") ++ pcStyle("\x1bO{}j"));
|
||||
result.set(.kp_subtract, kpDefault("m") ++ pcStyle("\x1bO{}m"));
|
||||
result.set(.kp_add, kpDefault("k") ++ pcStyle("\x1bO{}k"));
|
||||
result.set(.kp_enter, kpDefault("M") ++ pcStyle("\x1bO{}M"));
|
||||
|
||||
result.set(.backspace, &.{
|
||||
// Modify Keys Normal
|
||||
.{ .mods = .{ .shift = true }, .modify_other_keys = .set, .sequence = "\x7f" },
|
||||
.{ .mods = .{ .alt = true }, .modify_other_keys = .set, .sequence = "\x1b\x7f" },
|
||||
.{ .mods = .{ .alt = true, .shift = true }, .modify_other_keys = .set, .sequence = "\x1b\x7f" },
|
||||
.{ .mods = .{ .ctrl = true, .shift = true }, .modify_other_keys = .set, .sequence = "\x08" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true }, .modify_other_keys = .set, .sequence = "\x1b\x08" },
|
||||
.{ .mods = .{ .super = true }, .modify_other_keys = .set, .sequence = "\x7f" },
|
||||
.{ .mods = .{ .super = true, .shift = true }, .modify_other_keys = .set, .sequence = "\x7f" },
|
||||
.{ .mods = .{ .alt = true, .super = true }, .modify_other_keys = .set, .sequence = "\x1b\x7f" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .shift = true }, .modify_other_keys = .set, .sequence = "\x1b\x7f" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true }, .modify_other_keys = .set, .sequence = "\x08" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true, .shift = true }, .modify_other_keys = .set, .sequence = "\x08" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true }, .modify_other_keys = .set, .sequence = "\x1b\x08" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true, .shift = true }, .modify_other_keys = .set, .sequence = "\x1b\x08" },
|
||||
|
||||
// Modify Keys Other
|
||||
.{ .mods = .{ .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;2;127~" },
|
||||
.{ .mods = .{ .alt = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;3;127~" },
|
||||
.{ .mods = .{ .alt = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;4;127~" },
|
||||
.{ .mods = .{ .ctrl = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;6;127~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;7;127~" },
|
||||
.{ .mods = .{ .alt = true, .shift = true, .ctrl = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;8;127~" },
|
||||
.{ .mods = .{ .super = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;9;127~" },
|
||||
.{ .mods = .{ .super = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;10;127~" },
|
||||
.{ .mods = .{ .alt = true, .super = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;11;127~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;12;127~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;13;127~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;14;127~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;15;127~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true, .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;16;127~" },
|
||||
|
||||
.{ .mods = .{ .ctrl = true }, .sequence = "\x08" },
|
||||
.{ .sequence = "\x7f" },
|
||||
});
|
||||
|
||||
result.set(.tab, &.{
|
||||
// Modify Keys Normal
|
||||
.{ .mods = .{ .shift = true }, .modify_other_keys = .set, .sequence = "\x1b[Z" },
|
||||
.{ .mods = .{ .alt = true }, .modify_other_keys = .set, .sequence = "\x1b\t" },
|
||||
|
||||
// Modify Keys Other
|
||||
.{ .mods = .{ .shift = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;2;9~" },
|
||||
.{ .mods = .{ .alt = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;3;9~" },
|
||||
|
||||
// Everything else
|
||||
.{ .mods = .{ .alt = true, .shift = true }, .sequence = "\x1b[27;4;9~" },
|
||||
.{ .mods = .{ .ctrl = true }, .sequence = "\x1b[27;5;9~" },
|
||||
.{ .mods = .{ .ctrl = true, .shift = true }, .sequence = "\x1b[27;6;9~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true }, .sequence = "\x1b[27;7;9~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;8;9~" },
|
||||
.{ .mods = .{ .super = true }, .sequence = "\x1b[27;9;9~" },
|
||||
.{ .mods = .{ .super = true, .shift = true }, .sequence = "\x1b[27;10;9~" },
|
||||
.{ .mods = .{ .alt = true, .super = true }, .sequence = "\x1b[27;11;9~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .shift = true }, .sequence = "\x1b[27;12;9~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true }, .sequence = "\x1b[27;13;9~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;14;9~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true }, .sequence = "\x1b[27;15;9~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;16;9~" },
|
||||
|
||||
.{ .sequence = "\t" },
|
||||
});
|
||||
|
||||
result.set(.enter, &.{
|
||||
.{ .mods = .{ .shift = true }, .sequence = "\x1b[27;2;13~" },
|
||||
|
||||
// Modify Keys Normal
|
||||
.{ .mods = .{ .alt = true }, .modify_other_keys = .set, .sequence = "\x1b\r" },
|
||||
|
||||
// Modify Keys Other
|
||||
.{ .mods = .{ .alt = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;3;13~" },
|
||||
|
||||
// Everything else
|
||||
.{ .mods = .{ .alt = true, .shift = true }, .sequence = "\x1b[27;4;13~" },
|
||||
.{ .mods = .{ .ctrl = true }, .sequence = "\x1b[27;5;13~" },
|
||||
.{ .mods = .{ .ctrl = true, .shift = true }, .sequence = "\x1b[27;6;13~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true }, .sequence = "\x1b[27;7;13~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;8;13~" },
|
||||
.{ .mods = .{ .super = true }, .sequence = "\x1b[27;9;13~" },
|
||||
.{ .mods = .{ .super = true, .shift = true }, .sequence = "\x1b[27;10;13~" },
|
||||
.{ .mods = .{ .alt = true, .super = true }, .sequence = "\x1b[27;11;13~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .shift = true }, .sequence = "\x1b[27;12;13~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true }, .sequence = "\x1b[27;13;13~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;14;13~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true }, .sequence = "\x1b[27;15;13~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;16;13~" },
|
||||
|
||||
.{ .sequence = "\r" },
|
||||
});
|
||||
|
||||
result.set(.escape, &.{
|
||||
.{ .mods = .{ .shift = true }, .sequence = "\x1b[27;2;27~" },
|
||||
.{ .mods = .{ .alt = true }, .sequence = "\x1b\x1b" },
|
||||
.{ .mods = .{ .alt = true, .shift = true }, .sequence = "\x1b[27;4;27~" },
|
||||
.{ .mods = .{ .ctrl = true }, .sequence = "\x1b[27;5;27~" },
|
||||
.{ .mods = .{ .ctrl = true, .shift = true }, .sequence = "\x1b[27;6;27~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true }, .sequence = "\x1b[27;7;27~" },
|
||||
.{ .mods = .{ .alt = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;8;27~" },
|
||||
.{ .mods = .{ .super = true }, .sequence = "\x1b[27;9;27~" },
|
||||
.{ .mods = .{ .super = true, .shift = true }, .sequence = "\x1b[27;10;27~" },
|
||||
.{ .mods = .{ .alt = true, .super = true }, .sequence = "\x1b[27;11;27~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .shift = true }, .sequence = "\x1b[27;12;27~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true }, .sequence = "\x1b[27;13;27~" },
|
||||
.{ .mods = .{ .super = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;14;27~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true }, .sequence = "\x1b[27;15;27~" },
|
||||
.{ .mods = .{ .alt = true, .super = true, .ctrl = true, .shift = true }, .sequence = "\x1b[27;16;27~" },
|
||||
.{ .sequence = "\x1b" },
|
||||
});
|
||||
|
||||
break :keys result;
|
||||
};
|
||||
|
||||
/// Returns the default keypad application mode entry.
|
||||
fn kpDefault(comptime suffix: []const u8) []const Entry {
|
||||
return &.{
|
||||
.{
|
||||
.mods_empty_is_any = false,
|
||||
.keypad = .application,
|
||||
.sequence = "\x1bO" ++ suffix,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns entries that are dependent on cursor key settings.
|
||||
fn cursorKey(
|
||||
comptime normal: []const u8,
|
||||
comptime application: []const u8,
|
||||
) []const Entry {
|
||||
return &.{
|
||||
.{ .cursor = .normal, .sequence = normal },
|
||||
.{ .cursor = .application, .sequence = application },
|
||||
};
|
||||
}
|
||||
|
||||
/// Constructs a set of pcStyle function keys using the given format. The
|
||||
/// format should have exactly one "hole" for the mods code.
|
||||
/// Example: "\x1b[11;{}~" for F1.
|
||||
fn pcStyle(comptime fmt: []const u8) []const Entry {
|
||||
// The comptime {} wrapper is superflous but it prevents us from
|
||||
// accidentally running this function at runtime.
|
||||
comptime {
|
||||
var entries: [modifiers.len]Entry = undefined;
|
||||
for (modifiers, 2.., 0..) |mods, code, i| {
|
||||
entries[i] = .{
|
||||
.mods = mods,
|
||||
.sequence = std.fmt.comptimePrint(fmt, .{code}),
|
||||
};
|
||||
}
|
||||
|
||||
return &entries;
|
||||
}
|
||||
}
|
||||
|
||||
test "keys" {
|
||||
const testing = std.testing;
|
||||
|
||||
// Force resolution for comptime evaluation.
|
||||
_ = keys;
|
||||
|
||||
// All key sequences must fit into a termio array.
|
||||
const max = @import("../termio.zig").Message.WriteReq.Small.Max;
|
||||
for (keys.values) |entries| {
|
||||
for (entries) |entry| {
|
||||
try testing.expect(entry.sequence.len <= max);
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,24 @@ pub const Mods = packed struct(u8) {
|
||||
num_lock: bool = false,
|
||||
_padding: u2 = 0,
|
||||
|
||||
/// Returns true if no modifiers are set.
|
||||
pub fn empty(self: Mods) bool {
|
||||
return @as(u8, @bitCast(self)) == 0;
|
||||
}
|
||||
|
||||
/// Returns true if two mods are equal.
|
||||
pub fn equal(self: Mods, other: Mods) bool {
|
||||
return @as(u8, @bitCast(self)) == @as(u8, @bitCast(other));
|
||||
}
|
||||
|
||||
/// Returns the mods without locks set.
|
||||
pub fn withoutLocks(self: Mods) Mods {
|
||||
var copy = self;
|
||||
copy.caps_lock = false;
|
||||
copy.num_lock = false;
|
||||
return copy;
|
||||
}
|
||||
|
||||
// For our own understanding
|
||||
test {
|
||||
const testing = std.testing;
|
||||
|
@ -90,6 +90,8 @@ modes: packed struct {
|
||||
|
||||
deccolm: bool = false, // 3,
|
||||
deccolm_supported: bool = false, // 40
|
||||
keypad_keys: bool = false, // 66
|
||||
alt_esc_prefix: bool = true, // 1036
|
||||
|
||||
focus_event: bool = false, // 1004
|
||||
mouse_alternate_scroll: bool = true, // 1007
|
||||
@ -98,6 +100,10 @@ modes: packed struct {
|
||||
|
||||
bracketed_paste: bool = false, // 2004
|
||||
|
||||
// This is set via ESC[4;2m. Any other modify key mode just sets
|
||||
// this to false.
|
||||
modify_other_keys: bool = false,
|
||||
|
||||
// This isn't a mode, this is set by OSC 133 using the "A" event.
|
||||
// If this is true, it tells us that the shell supports redrawing
|
||||
// the prompt and that when we resize, if the cursor is at a prompt,
|
||||
|
@ -88,6 +88,9 @@ pub const Mode = enum(u16) {
|
||||
/// mode ?3 is set or unset.
|
||||
enable_mode_3 = 40,
|
||||
|
||||
/// DECNKM. Sets application keypad mode if enabled.
|
||||
keypad_keys = 66,
|
||||
|
||||
/// "Normal" mouse events: click/release, scroll
|
||||
mouse_event_normal = 1000,
|
||||
|
||||
@ -118,6 +121,12 @@ pub const Mode = enum(u16) {
|
||||
/// Report mouse position in the SGR format as pixels, instead of cells.
|
||||
mouse_format_sgr_pixels = 1016,
|
||||
|
||||
/// The alt key sends esc as a prefix before any character. On by default.
|
||||
alt_esc_prefix = 1036,
|
||||
|
||||
/// altSendsEscape xterm (https://invisible-island.net/xterm/manpage/xterm.html)
|
||||
alt_sends_escape = 1039,
|
||||
|
||||
/// Alternate screen mode with save cursor and clear on enter.
|
||||
alt_screen_save_cursor_clear_enter = 1049,
|
||||
|
||||
@ -189,3 +198,12 @@ pub const StatusDisplay = enum(u16) {
|
||||
// Non-exhaustive so that @intToEnum never fails for unsupported values.
|
||||
_,
|
||||
};
|
||||
|
||||
/// The possible modify key formats to ESC[>{a};{b}m
|
||||
/// Note: this is not complete, we should add more as we support more
|
||||
pub const ModifyKeyFormat = union(enum) {
|
||||
legacy: void,
|
||||
cursor_keys: void,
|
||||
function_keys: void,
|
||||
other_keys: enum { none, numeric_except, numeric },
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ pub const CursorStyle = ansi.CursorStyle;
|
||||
pub const DeviceAttributeReq = ansi.DeviceAttributeReq;
|
||||
pub const DeviceStatusReq = ansi.DeviceStatusReq;
|
||||
pub const Mode = ansi.Mode;
|
||||
pub const ModifyKeyFormat = ansi.ModifyKeyFormat;
|
||||
pub const StatusLineType = ansi.StatusLineType;
|
||||
pub const StatusDisplay = ansi.StatusDisplay;
|
||||
pub const EraseDisplay = csi.EraseDisplay;
|
||||
|
@ -437,38 +437,109 @@ pub fn Stream(comptime Handler: type) type {
|
||||
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
||||
|
||||
// SGR - Select Graphic Rendition
|
||||
'm' => if (action.intermediates.len == 0) {
|
||||
if (@hasDecl(T, "setAttribute")) {
|
||||
'm' => switch (action.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "setAttribute")) {
|
||||
var p: sgr.Parser = .{ .params = action.params, .colon = action.sep == .colon };
|
||||
while (p.next()) |attr| {
|
||||
// log.info("SGR attribute: {}", .{attr});
|
||||
try self.handler.setAttribute(attr);
|
||||
}
|
||||
} else log.warn("unimplemented CSI callback: {}", .{action});
|
||||
} else {
|
||||
// Nothing, but I wanted a place to put this comment:
|
||||
// there are others forms of CSI m that have intermediates.
|
||||
// `vim --clean` uses `CSI ? 4 m` and I don't know what
|
||||
// that means. And there is also `CSI > m` which is used
|
||||
// to control modifier key reporting formats that we don't
|
||||
// support yet.
|
||||
log.debug(
|
||||
"ignoring unimplemented CSI m with intermediates: {s}",
|
||||
.{action.intermediates},
|
||||
);
|
||||
} else log.warn("unimplemented CSI callback: {}", .{action}),
|
||||
|
||||
1 => switch (action.intermediates[0]) {
|
||||
'>' => if (@hasDecl(T, "setModifyKeyFormat")) blk: {
|
||||
if (action.params.len == 0) {
|
||||
// Reset
|
||||
try self.handler.setModifyKeyFormat(.{ .legacy = {} });
|
||||
break :blk;
|
||||
}
|
||||
|
||||
var format: ansi.ModifyKeyFormat = switch (action.params[0]) {
|
||||
0 => .{ .legacy = {} },
|
||||
1 => .{ .cursor_keys = {} },
|
||||
2 => .{ .function_keys = {} },
|
||||
4 => .{ .other_keys = .none },
|
||||
else => {
|
||||
log.warn("invalid setModifyKeyFormat: {}", .{action});
|
||||
break :blk;
|
||||
},
|
||||
};
|
||||
|
||||
if (action.params.len > 2) {
|
||||
log.warn("invalid setModifyKeyFormat: {}", .{action});
|
||||
break :blk;
|
||||
}
|
||||
|
||||
if (action.params.len == 2) {
|
||||
switch (format) {
|
||||
// We don't support any of the subparams yet for these.
|
||||
.legacy => {},
|
||||
.cursor_keys => {},
|
||||
.function_keys => {},
|
||||
|
||||
// We only support the numeric form.
|
||||
.other_keys => |*v| switch (action.params[1]) {
|
||||
2 => v.* = .numeric,
|
||||
else => v.* = .none,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try self.handler.setModifyKeyFormat(format);
|
||||
} else log.warn("unimplemented setModifyKeyFormat: {}", .{action}),
|
||||
|
||||
else => log.warn(
|
||||
"unknown CSI m with intermediate: {}",
|
||||
.{action.intermediates[0]},
|
||||
),
|
||||
},
|
||||
|
||||
else => {
|
||||
// Nothing, but I wanted a place to put this comment:
|
||||
// there are others forms of CSI m that have intermediates.
|
||||
// `vim --clean` uses `CSI ? 4 m` and I don't know what
|
||||
// that means. And there is also `CSI > m` which is used
|
||||
// to control modifier key reporting formats that we don't
|
||||
// support yet.
|
||||
log.warn(
|
||||
"ignoring unimplemented CSI m with intermediates: {s}",
|
||||
.{action.intermediates},
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
// CPR - Request Cursor Position Report
|
||||
// TODO: test
|
||||
'n' => if (@hasDecl(T, "deviceStatusReport")) try self.handler.deviceStatusReport(
|
||||
switch (action.params.len) {
|
||||
1 => @enumFromInt(action.params[0]),
|
||||
else => {
|
||||
log.warn("invalid erase characters command: {}", .{action});
|
||||
return;
|
||||
'n' => switch (action.intermediates.len) {
|
||||
0 => if (@hasDecl(T, "deviceStatusReport")) try self.handler.deviceStatusReport(
|
||||
switch (action.params.len) {
|
||||
1 => @enumFromInt(action.params[0]),
|
||||
else => {
|
||||
log.warn("invalid erase characters command: {}", .{action});
|
||||
return;
|
||||
},
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
||||
|
||||
1 => switch (action.intermediates[0]) {
|
||||
'>' => if (@hasDecl(T, "setModifyKeyFormat")) {
|
||||
// This isn't strictly correct. CSI > n has parameters that
|
||||
// control what exactly is being disabled. However, we
|
||||
// only support reverting back to modify other keys in
|
||||
// numeric except format.
|
||||
try self.handler.setModifyKeyFormat(.{ .other_keys = .numeric_except });
|
||||
} else log.warn("unimplemented setModifyKeyFormat: {}", .{action}),
|
||||
|
||||
else => log.warn(
|
||||
"unknown CSI m with intermediate: {}",
|
||||
.{action.intermediates[0]},
|
||||
),
|
||||
},
|
||||
) else log.warn("unimplemented CSI callback: {}", .{action}),
|
||||
|
||||
else => log.warn(
|
||||
"ignoring unimplemented CSI n with intermediates: {s}",
|
||||
.{action.intermediates},
|
||||
),
|
||||
},
|
||||
|
||||
// DECSCUSR - Select Cursor Style
|
||||
// TODO: test
|
||||
@ -765,10 +836,24 @@ pub fn Stream(comptime Handler: type) type {
|
||||
},
|
||||
} else log.warn("unimplemented invokeCharset: {}", .{action}),
|
||||
|
||||
// Set application keypad mode
|
||||
'=' => if (@hasDecl(T, "setMode")) {
|
||||
try self.handler.setMode(.keypad_keys, true);
|
||||
} else log.warn("unimplemented setMode: {}", .{action}),
|
||||
|
||||
// Reset application keypad mode
|
||||
'>' => if (@hasDecl(T, "setMode")) {
|
||||
try self.handler.setMode(.keypad_keys, false);
|
||||
} else log.warn("unimplemented setMode: {}", .{action}),
|
||||
|
||||
else => if (@hasDecl(T, "escUnimplemented"))
|
||||
try self.handler.escUnimplemented(action)
|
||||
else
|
||||
log.warn("unimplemented ESC action: {}", .{action}),
|
||||
|
||||
// Sets ST (string terminator). We don't have to do anything
|
||||
// because our parser always accepts ST.
|
||||
'\\' => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -1183,6 +1183,17 @@ const StreamHandler = struct {
|
||||
self.terminal.setScrollingRegion(top, bot);
|
||||
}
|
||||
|
||||
pub fn setModifyKeyFormat(self: *StreamHandler, format: terminal.ModifyKeyFormat) !void {
|
||||
self.terminal.modes.modify_other_keys = false;
|
||||
switch (format) {
|
||||
.other_keys => |v| switch (v) {
|
||||
.numeric => self.terminal.modes.modify_other_keys = true,
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void {
|
||||
// Note: this function doesn't need to grab the render state or
|
||||
// terminal locks because it is only called from process() which
|
||||
@ -1193,6 +1204,10 @@ const StreamHandler = struct {
|
||||
self.terminal.modes.cursor_keys = enabled;
|
||||
},
|
||||
|
||||
.keypad_keys => {
|
||||
self.terminal.modes.keypad_keys = enabled;
|
||||
},
|
||||
|
||||
.insert => {
|
||||
self.terminal.modes.insert = enabled;
|
||||
},
|
||||
@ -1259,8 +1274,8 @@ const StreamHandler = struct {
|
||||
.mouse_format_sgr_pixels => self.terminal.modes.mouse_format = if (enabled) .sgr_pixels else .x10,
|
||||
|
||||
.mouse_alternate_scroll => self.terminal.modes.mouse_alternate_scroll = enabled,
|
||||
|
||||
.focus_event => self.terminal.modes.focus_event = enabled,
|
||||
.alt_esc_prefix => self.terminal.modes.alt_esc_prefix = enabled,
|
||||
|
||||
else => if (enabled) log.warn("unimplemented mode: {}", .{mode}),
|
||||
}
|
||||
|
Reference in New Issue
Block a user