Merge pull request #282 from mitchellh/kitty-keys

Many keyboard input improvements (no Kitty protocol yet)
This commit is contained in:
Mitchell Hashimoto
2023-08-13 16:30:25 -07:00
committed by GitHub
13 changed files with 713 additions and 254 deletions

View File

@ -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;

View File

@ -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;
};

View File

@ -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,

View File

@ -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;
};

View File

@ -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

View File

@ -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
View 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);
}
}
}

View File

@ -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;

View File

@ -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,

View File

@ -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 },
};

View File

@ -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;

View File

@ -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.
'\\' => {},
}
}
};

View File

@ -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}),
}