diff --git a/src/Window.zig b/src/Window.zig index ebe3b67c7..074d04f26 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -980,6 +980,29 @@ fn keyCallback( win.io_thread.wakeup.send() catch {}; }, + .cursor_key => |ck| { + // We send a different sequence depending on if we're + // in cursor keys mode. We're in "normal" mode if cursor + // keys mdoe is NOT set. + const normal = normal: { + win.renderer_state.mutex.lock(); + defer win.renderer_state.mutex.unlock(); + break :normal !win.io.terminal.modes.cursor_keys; + }; + + if (normal) { + _ = win.io_thread.mailbox.push(.{ + .write_stable = ck.normal, + }, .{ .forever = {} }); + } else { + _ = win.io_thread.mailbox.push(.{ + .write_stable = ck.application, + }, .{ .forever = {} }); + } + + win.io_thread.wakeup.send() catch {}; + }, + .copy_to_clipboard => { // We can read from the renderer state without holding // the lock because only we will write to this field. diff --git a/src/config.zig b/src/config.zig index 1b06ace87..fd92e2de7 100644 --- a/src/config.zig +++ b/src/config.zig @@ -190,12 +190,31 @@ pub const Config = struct { .{ .paste_from_clipboard = {} }, ); - try result.keybind.set.put(alloc, .{ .key = .up }, .{ .csi = "A" }); - try result.keybind.set.put(alloc, .{ .key = .down }, .{ .csi = "B" }); - try result.keybind.set.put(alloc, .{ .key = .right }, .{ .csi = "C" }); - try result.keybind.set.put(alloc, .{ .key = .left }, .{ .csi = "D" }); - try result.keybind.set.put(alloc, .{ .key = .home }, .{ .csi = "H" }); - try result.keybind.set.put(alloc, .{ .key = .end }, .{ .csi = "F" }); + 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 = .page_up }, .{ .csi = "5~" }); try result.keybind.set.put(alloc, .{ .key = .page_down }, .{ .csi = "6~" }); diff --git a/src/input/Binding.zig b/src/input/Binding.zig index e0629bd36..3e29f1080 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -15,6 +15,7 @@ action: Action, pub const Error = error{ InvalidFormat, + InvalidAction, }; /// Parse the format "ctrl+a=csi:A" into a binding. The format is @@ -101,6 +102,9 @@ pub fn parse(input: []const u8) !Binding { break :action @unionInit(Action, field.name, param); }, + // Cursor keys can't be set currently + Action.CursorKey => return Error.InvalidAction, + else => unreachable, } } @@ -127,6 +131,10 @@ pub const Action = union(enum) { /// without the CSI header ("ESC ]" or "\x1b]"). csi: []const u8, + /// Send data to the pty depending on whether cursor key mode is + /// enabled ("application") or disabled ("normal"). + cursor_key: CursorKey, + /// Copy and paste. copy_to_clipboard: void, paste_from_clipboard: void, @@ -152,6 +160,11 @@ pub const Action = union(enum) { /// Quit ghostty quit: void, + + pub const CursorKey = struct { + normal: []const u8, + application: []const u8, + }; }; /// Trigger is the associated key state that can trigger an action. diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 90867da0c..5766e9f1b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -72,6 +72,7 @@ previous_char: ?u21 = null, modes: packed struct { const Self = @This(); + cursor_keys: bool = false, // 1 reverse_colors: bool = false, // 5, origin: bool = false, // 6 autowrap: bool = true, // 7 diff --git a/src/terminal/ansi.zig b/src/terminal/ansi.zig index bb2a962e8..a6aa9c832 100644 --- a/src/terminal/ansi.zig +++ b/src/terminal/ansi.zig @@ -42,6 +42,17 @@ pub const RenditionAspect = enum(u16) { /// values correspond to the `?`-prefixed modes, since those are the ones /// of primary interest. The enum value is the mode value. pub const Mode = enum(u16) { + /// This control function selects the sequences the arrow keys send. + /// You can use the four arrow keys to move the cursor through the current + /// page or to send special application commands. + /// + /// If the DECCKM function is set, then the arrow keys send application + /// sequences to the host. + /// + /// If the DECCKM function is reset, then the arrow keys send ANSI cursor + /// sequences to the host. + cursor_keys = 1, + /// Change terminal wide between 80 and 132 column mode. When set /// (with ?40 set), resizes terminal to 132 columns and keeps it that /// wide. When unset, resizes to 80 columns. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 5dec6ac95..718d6418c 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -627,6 +627,10 @@ const StreamHandler = struct { pub fn setMode(self: *StreamHandler, mode: terminal.Mode, enabled: bool) !void { switch (mode) { + .cursor_keys => { + self.terminal.modes.cursor_keys = enabled; + }, + .reverse_colors => { self.terminal.modes.reverse_colors = enabled;