diff --git a/src/Surface.zig b/src/Surface.zig index c79facf22..3462df8f0 100644 --- a/src/Surface.zig +++ b/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; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index de257cdd4..efd1480af 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -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; }; diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 9f222b2c3..90d08dfba 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -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, diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 47eec5b3e..3cc96d7e0 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -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; }; diff --git a/src/config.zig b/src/config.zig index ae41072a0..b1e847153 100644 --- a/src/config.zig +++ b/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 diff --git a/src/input.zig b/src/input.zig index a356cefc5..e2deed0e2 100644 --- a/src/input.zig +++ b/src/input.zig @@ -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; diff --git a/src/input/function_keys.zig b/src/input/function_keys.zig new file mode 100644 index 000000000..52dfe75bf --- /dev/null +++ b/src/input/function_keys.zig @@ -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); + } + } +} diff --git a/src/input/key.zig b/src/input/key.zig index 6ef6ae38d..2d951b996 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -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; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 15fcc23da..ff9b0c8f3 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -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, diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index f7d684018..543f40ada 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -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 }, +}; diff --git a/src/terminal/main.zig b/src/terminal/main.zig index fcb39f5d4..296089b68 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -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; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 4164d3ad0..39d6a03db 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -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. + '\\' => {}, } } }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index a3d8a45ab..1aec3f269 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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}), }