diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/input | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/input')
| -rw-r--r-- | vendor/libghostty-vt/src/input/Binding.zig | 94 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/input/command.zig | 387 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/input/function_keys.zig | 58 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/input/key.zig | 10 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/input/key_encode.zig | 428 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/input/key_mods.zig | 9 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/input/paste.zig | 168 |
7 files changed, 854 insertions, 300 deletions
diff --git a/vendor/libghostty-vt/src/input/Binding.zig b/vendor/libghostty-vt/src/input/Binding.zig index d60f2933..82b4bc8b 100644 --- a/vendor/libghostty-vt/src/input/Binding.zig +++ b/vendor/libghostty-vt/src/input/Binding.zig @@ -587,6 +587,12 @@ pub const Action = union(enum) { /// the last tab. move_tab: isize, + /// Move a tab to a new window. + /// + /// Only implemented on Linux, but there's a native tab menu provided by + /// macOS. + move_tab_to_new_window, + /// Toggle the tab overview. /// /// This is only supported on Linux and when the system's libadwaita @@ -602,6 +608,13 @@ pub const Action = union(enum) { /// and persists across focus changes within the tab. prompt_tab_title, + /// Change the title of the current window via a pop-up prompt. The + /// title set via this prompt overrides any title set by the terminal + /// and persists across focus changes within the tab. + /// + /// Only implemented on Linux. + prompt_window_title, + /// Set the title for the current focused surface. /// /// If the title is empty, the surface title is reset to an empty title. @@ -612,6 +625,13 @@ pub const Action = union(enum) { /// If the title is empty, the tab title override is cleared. set_tab_title: []const u8, + /// Set the title for the current focused window. + /// + /// If the title is empty, the tab title override is cleared. + /// + /// Only implement on Linux. + set_window_title: []const u8, + /// Create a new split in the specified direction. /// /// Valid arguments: @@ -686,12 +706,14 @@ pub const Action = union(enum) { /// untested. show_on_screen_keyboard, - /// Open the configuration file in the default OS editor. + /// Open the configuration file in an editor. /// - /// If your default OS editor isn't configured then this will fail. - /// Currently, any failures to open the configuration will show up only in - /// the logs. - open_config, + /// * `os_open`: Use the OS's default editor to edit the configuration file. + /// This is the default action. (Available since 1.4.0) + /// * `new_window`: Launch the editor specified in `$EDITOR` or `$VISUAL` in + /// a new Ghostty window to edit the configuration file. GTK only. (Available + /// since 1.4.0.) + open_config: OpenConfig, /// Reload the configuration. /// @@ -741,8 +763,8 @@ pub const Action = union(enum) { /// Maximize or unmaximize the current window. /// - /// This has no effect on macOS as it does not have the concept of - /// maximized windows. + /// On macOS, this zooms the window, which is the closest equivalent + /// since macOS has no concept of a maximized window. toggle_maximize, /// Fullscreen or unfullscreen the current window. @@ -1186,6 +1208,16 @@ pub const Action = union(enum) { pub const default: CloseTabMode = .this; }; + pub const OpenConfig = enum { + /// Open the config in the OS default editor. + os_open, + + /// Open the config in a new window using $EDITOR or $VISUAL + new_window, + + pub const default: OpenConfig = .os_open; + }; + fn parseEnum(comptime T: type, value: []const u8) !T { return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat; } @@ -1370,8 +1402,10 @@ pub const Action = union(enum) { .set_font_size, .prompt_surface_title, .prompt_tab_title, + .prompt_window_title, .set_surface_title, .set_tab_title, + .set_window_title, .clear_screen, .select_all, .scroll_to_top, @@ -1418,6 +1452,7 @@ pub const Action = union(enum) { .last_tab, .goto_tab, .move_tab, + .move_tab_to_new_window, .toggle_tab_overview, .new_split, .goto_split, @@ -1439,30 +1474,35 @@ pub const Action = union(enum) { const all_fields = @typeInfo(Action).@"union".fields; // Find all fields that are app-scoped - var i: usize = 0; - var union_fields: [all_fields.len]std.builtin.Type.UnionField = undefined; - var enum_fields: [all_fields.len]std.builtin.Type.EnumField = undefined; + var i: comptime_int = 0; + var names: [all_fields.len][]const u8 = undefined; + var types: [all_fields.len]type = undefined; + var attrs: [all_fields.len]std.builtin.Type.UnionField.Attributes = undefined; + var raw_values: [all_fields.len]comptime_int = undefined; + for (all_fields) |field| { const action = @unionInit(Action, field.name, undefined); if (action.scope() == s) { - union_fields[i] = field; - enum_fields[i] = .{ .name = field.name, .value = i }; + names[i] = field.name; + types[i] = field.type; + attrs[i] = .{ .@"align" = field.alignment }; + raw_values[i] = i; i += 1; } } + const TagInt = std.math.IntFittingRange(0, i); + var values: [i]TagInt = undefined; + for (raw_values[0..i], &values) |raw, *v| v.* = raw; + // Build our union - return @Type(.{ .@"union" = .{ - .layout = .auto, - .tag_type = @Type(.{ .@"enum" = .{ - .tag_type = std.math.IntFittingRange(0, i), - .fields = enum_fields[0..i], - .decls = &.{}, - .is_exhaustive = true, - } }), - .fields = union_fields[0..i], - .decls = &.{}, - } }); + return @Union( + .auto, + @Enum(TagInt, .exhaustive, names[0..i], &values), + names[0..i], + types[0..i], + attrs[0..i], + ); } /// Returns the scoped version of this action. If the action is not @@ -1519,7 +1559,7 @@ pub const Action = union(enum) { const value_info = @typeInfo(Value); switch (Value) { void => {}, - []const u8 => try std.zig.stringEscape(value, writer), + []const u8 => try writer.print("{s}", .{value}), else => switch (value_info) { .@"enum" => try writer.print("{t}", .{value}), .float => try writer.print("{d}", .{value}), @@ -4606,12 +4646,14 @@ test "action: format" { const testing = std.testing; const alloc = testing.allocator; - const a: Action = .{ .text = "👻" }; + const a: Action = .{ .text = "👻Ghostty'\"" }; var buf: std.Io.Writer.Allocating = .init(alloc); defer buf.deinit(); try a.format(&buf.writer); - try testing.expectEqualStrings("text:\\xf0\\x9f\\x91\\xbb", buf.written()); + + const b = try Binding.Action.parse(buf.written()); + try testing.expect(a.equal(b)); } test "action: format set title" { diff --git a/vendor/libghostty-vt/src/input/command.zig b/vendor/libghostty-vt/src/input/command.zig index ac048eec..e75ede29 100644 --- a/vendor/libghostty-vt/src/input/command.zig +++ b/vendor/libghostty-vt/src/input/command.zig @@ -2,6 +2,7 @@ const std = @import("std"); const assert = @import("../quirks.zig").inlineAssert; const Allocator = std.mem.Allocator; const Action = @import("Binding.zig").Action; +const i18n = @import("../os/i18n.zig"); /// A command is a named binding action that can be executed from /// something like a command palette. @@ -76,6 +77,14 @@ pub const Command = struct { }; } + pub fn translated(self: Command) Command { + return .{ + .action = self.action, + .title = std.mem.span(i18n._(self.title)), + .description = std.mem.span(i18n._(self.description)), + }; + } + /// Implements a comparison function for std.mem.sortUnstable /// and similar functions. The sorting is defined by Ghostty /// to be what we prefer. If a caller wants some other sorting, @@ -137,155 +146,155 @@ fn actionCommands(action: Action.Key) []const Command { .reset => comptime &.{.{ .action = .reset, - .title = "Reset Terminal", - .description = "Reset the terminal to a clean state.", + .title = i18n.N_("Reset Terminal"), + .description = i18n.N_("Reset the terminal to a clean state."), }}, .copy_to_clipboard => comptime &.{ .{ .action = .{ .copy_to_clipboard = .mixed }, - .title = "Copy to Clipboard", - .description = "Copy the selected text to the clipboard in both plain and styled formats.", + .title = i18n.N_("Copy to Clipboard"), + .description = i18n.N_("Copy the selected text to the clipboard in both plain and styled formats."), }, .{ .action = .{ .copy_to_clipboard = .plain }, - .title = "Copy Selection as Plain Text to Clipboard", - .description = "Copy the selected text as plain text to the clipboard.", + .title = i18n.N_("Copy Selection as Plain Text to Clipboard"), + .description = i18n.N_("Copy the selected text as plain text to the clipboard."), }, .{ .action = .{ .copy_to_clipboard = .vt }, - .title = "Copy Selection as ANSI Sequences to Clipboard", - .description = "Copy the selected text as ANSI escape sequences to the clipboard.", + .title = i18n.N_("Copy Selection as ANSI Sequences to Clipboard"), + .description = i18n.N_("Copy the selected text as ANSI escape sequences to the clipboard."), }, .{ .action = .{ .copy_to_clipboard = .html }, - .title = "Copy Selection as HTML to Clipboard", - .description = "Copy the selected text as HTML to the clipboard.", + .title = i18n.N_("Copy Selection as HTML to Clipboard"), + .description = i18n.N_("Copy the selected text as HTML to the clipboard."), } }, .copy_url_to_clipboard => comptime &.{.{ .action = .copy_url_to_clipboard, - .title = "Copy URL to Clipboard", - .description = "Copy the URL under the cursor to the clipboard.", + .title = i18n.N_("Copy URL to Clipboard"), + .description = i18n.N_("Copy the URL under the cursor to the clipboard."), }}, .copy_title_to_clipboard => comptime &.{.{ .action = .copy_title_to_clipboard, - .title = "Copy Terminal Title to Clipboard", - .description = "Copy the terminal title to the clipboard. If the terminal title is not set this has no effect.", + .title = i18n.N_("Copy Terminal Title to Clipboard"), + .description = i18n.N_("Copy the terminal title to the clipboard. If the terminal title is not set this has no effect."), }}, .paste_from_clipboard => comptime &.{.{ .action = .paste_from_clipboard, - .title = "Paste from Clipboard", - .description = "Paste the contents of the main clipboard.", + .title = i18n.N_("Paste from Clipboard"), + .description = i18n.N_("Paste the contents of the main clipboard."), }}, .paste_from_selection => comptime &.{.{ .action = .paste_from_selection, - .title = "Paste from Selection", - .description = "Paste the contents of the selection clipboard.", + .title = i18n.N_("Paste from Selection"), + .description = i18n.N_("Paste the contents of the selection clipboard."), }}, .start_search => comptime &.{.{ .action = .start_search, - .title = "Start Search", - .description = "Start a search if one isn't already active.", + .title = i18n.N_("Start Search"), + .description = i18n.N_("Start a search if one isn't already active."), }}, .search_selection => comptime &.{.{ .action = .search_selection, - .title = "Search Selection", - .description = "Start a search for the current text selection.", + .title = i18n.N_("Search Selection"), + .description = i18n.N_("Start a search for the current text selection."), }}, .end_search => comptime &.{.{ .action = .end_search, - .title = "End Search", - .description = "End the current search if any and hide any GUI elements.", + .title = i18n.N_("End Search"), + .description = i18n.N_("End the current search if any and hide any GUI elements."), }}, .navigate_search => comptime &.{ .{ .action = .{ .navigate_search = .next }, - .title = "Next Search Result", - .description = "Navigate to the next search result, if any.", + .title = i18n.N_("Next Search Result"), + .description = i18n.N_("Navigate to the next search result, if any."), }, .{ .action = .{ .navigate_search = .previous }, - .title = "Previous Search Result", - .description = "Navigate to the previous search result, if any.", + .title = i18n.N_("Previous Search Result"), + .description = i18n.N_("Navigate to the previous search result, if any."), } }, .increase_font_size => comptime &.{.{ .action = .{ .increase_font_size = 1 }, - .title = "Increase Font Size", - .description = "Increase the font size by 1 point.", + .title = i18n.N_("Increase Font Size"), + .description = i18n.N_("Increase the font size by 1 point."), }}, .decrease_font_size => comptime &.{.{ .action = .{ .decrease_font_size = 1 }, - .title = "Decrease Font Size", - .description = "Decrease the font size by 1 point.", + .title = i18n.N_("Decrease Font Size"), + .description = i18n.N_("Decrease the font size by 1 point."), }}, .reset_font_size => comptime &.{.{ .action = .reset_font_size, - .title = "Reset Font Size", - .description = "Reset the font size to the default.", + .title = i18n.N_("Reset Font Size"), + .description = i18n.N_("Reset the font size to the default."), }}, .clear_screen => comptime &.{.{ .action = .clear_screen, - .title = "Clear Screen", - .description = "Clear the screen and scrollback.", + .title = i18n.N_("Clear Screen"), + .description = i18n.N_("Clear the screen and scrollback."), }}, .select_all => comptime &.{.{ .action = .select_all, - .title = "Select All", - .description = "Select all text on the screen.", + .title = i18n.N_("Select All"), + .description = i18n.N_("Select all text on the screen."), }}, .scroll_to_top => comptime &.{.{ .action = .scroll_to_top, - .title = "Scroll to Top", - .description = "Scroll to the top of the screen.", + .title = i18n.N_("Scroll to Top"), + .description = i18n.N_("Scroll to the top of the screen."), }}, .scroll_to_bottom => comptime &.{.{ .action = .scroll_to_bottom, - .title = "Scroll to Bottom", - .description = "Scroll to the bottom of the screen.", + .title = i18n.N_("Scroll to Bottom"), + .description = i18n.N_("Scroll to the bottom of the screen."), }}, .scroll_to_selection => comptime &.{.{ .action = .scroll_to_selection, - .title = "Scroll to Selection", - .description = "Scroll to the selected text.", + .title = i18n.N_("Scroll to Selection"), + .description = i18n.N_("Scroll to the selected text."), }}, .scroll_page_up => comptime &.{.{ .action = .scroll_page_up, - .title = "Scroll Page Up", - .description = "Scroll the screen up by a page.", + .title = i18n.N_("Scroll Page Up"), + .description = i18n.N_("Scroll the screen up by a page."), }}, .scroll_page_down => comptime &.{.{ .action = .scroll_page_down, - .title = "Scroll Page Down", - .description = "Scroll the screen down by a page.", + .title = i18n.N_("Scroll Page Down"), + .description = i18n.N_("Scroll the screen down by a page."), }}, .write_screen_file => comptime &.{ .{ .action = .{ .write_screen_file = .copy }, - .title = "Copy Screen to Temporary File and Copy Path", - .description = "Copy the screen contents to a temporary file and copy the path to the clipboard.", + .title = i18n.N_("Copy Screen to Temporary File and Copy Path"), + .description = i18n.N_("Copy the screen contents to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_screen_file = .paste }, - .title = "Copy Screen to Temporary File and Paste Path", - .description = "Copy the screen contents to a temporary file and paste the path to the file.", + .title = i18n.N_("Copy Screen to Temporary File and Paste Path"), + .description = i18n.N_("Copy the screen contents to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_screen_file = .open }, - .title = "Copy Screen to Temporary File and Open", - .description = "Copy the screen contents to a temporary file and open it.", + .title = i18n.N_("Copy Screen to Temporary File and Open"), + .description = i18n.N_("Copy the screen contents to a temporary file and open it."), }, .{ @@ -293,24 +302,24 @@ fn actionCommands(action: Action.Key) []const Command { .action = .copy, .emit = .html, } }, - .title = "Copy Screen as HTML to Temporary File and Copy Path", - .description = "Copy the screen contents as HTML to a temporary file and copy the path to the clipboard.", + .title = i18n.N_("Copy Screen as HTML to Temporary File and Copy Path"), + .description = i18n.N_("Copy the screen contents as HTML to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_screen_file = .{ .action = .paste, .emit = .html, } }, - .title = "Copy Screen as HTML to Temporary File and Paste Path", - .description = "Copy the screen contents as HTML to a temporary file and paste the path to the file.", + .title = i18n.N_("Copy Screen as HTML to Temporary File and Paste Path"), + .description = i18n.N_("Copy the screen contents as HTML to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_screen_file = .{ .action = .open, .emit = .html, } }, - .title = "Copy Screen as HTML to Temporary File and Open", - .description = "Copy the screen contents as HTML to a temporary file and open it.", + .title = i18n.N_("Copy Screen as HTML to Temporary File and Open"), + .description = i18n.N_("Copy the screen contents as HTML to a temporary file and open it."), }, .{ @@ -318,42 +327,42 @@ fn actionCommands(action: Action.Key) []const Command { .action = .copy, .emit = .vt, } }, - .title = "Copy Screen as ANSI Sequences to Temporary File and Copy Path", - .description = "Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.", + .title = i18n.N_("Copy Screen as ANSI Sequences to Temporary File and Copy Path"), + .description = i18n.N_("Copy the screen contents as ANSI escape sequences to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_screen_file = .{ .action = .paste, .emit = .vt, } }, - .title = "Copy Screen as ANSI Sequences to Temporary File and Paste Path", - .description = "Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file.", + .title = i18n.N_("Copy Screen as ANSI Sequences to Temporary File and Paste Path"), + .description = i18n.N_("Copy the screen contents as ANSI escape sequences to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_screen_file = .{ .action = .open, .emit = .vt, } }, - .title = "Copy Screen as ANSI Sequences to Temporary File and Open", - .description = "Copy the screen contents as ANSI escape sequences to a temporary file and open it.", + .title = i18n.N_("Copy Screen as ANSI Sequences to Temporary File and Open"), + .description = i18n.N_("Copy the screen contents as ANSI escape sequences to a temporary file and open it."), }, }, .write_selection_file => comptime &.{ .{ .action = .{ .write_selection_file = .copy }, - .title = "Copy Selection to Temporary File and Copy Path", - .description = "Copy the selection contents to a temporary file and copy the path to the clipboard.", + .title = i18n.N_("Copy Selection to Temporary File and Copy Path"), + .description = i18n.N_("Copy the selection contents to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_selection_file = .paste }, - .title = "Copy Selection to Temporary File and Paste Path", - .description = "Copy the selection contents to a temporary file and paste the path to the file.", + .title = i18n.N_("Copy Selection to Temporary File and Paste Path"), + .description = i18n.N_("Copy the selection contents to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_selection_file = .open }, - .title = "Copy Selection to Temporary File and Open", - .description = "Copy the selection contents to a temporary file and open it.", + .title = i18n.N_("Copy Selection to Temporary File and Open"), + .description = i18n.N_("Copy the selection contents to a temporary file and open it."), }, .{ @@ -361,24 +370,24 @@ fn actionCommands(action: Action.Key) []const Command { .action = .copy, .emit = .html, } }, - .title = "Copy Selection as HTML to Temporary File and Copy Path", - .description = "Copy the selection contents as HTML to a temporary file and copy the path to the clipboard.", + .title = i18n.N_("Copy Selection as HTML to Temporary File and Copy Path"), + .description = i18n.N_("Copy the selection contents as HTML to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_selection_file = .{ .action = .paste, .emit = .html, } }, - .title = "Copy Selection as HTML to Temporary File and Paste Path", - .description = "Copy the selection contents as HTML to a temporary file and paste the path to the file.", + .title = i18n.N_("Copy Selection as HTML to Temporary File and Paste Path"), + .description = i18n.N_("Copy the selection contents as HTML to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_selection_file = .{ .action = .open, .emit = .html, } }, - .title = "Copy Selection as HTML to Temporary File and Open", - .description = "Copy the selection contents as HTML to a temporary file and open it.", + .title = i18n.N_("Copy Selection as HTML to Temporary File and Open"), + .description = i18n.N_("Copy the selection contents as HTML to a temporary file and open it."), }, .{ @@ -386,299 +395,318 @@ fn actionCommands(action: Action.Key) []const Command { .action = .copy, .emit = .vt, } }, - .title = "Copy Selection as ANSI Sequences to Temporary File and Copy Path", - .description = "Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard.", + .title = i18n.N_("Copy Selection as ANSI Sequences to Temporary File and Copy Path"), + .description = i18n.N_("Copy the selection contents as ANSI escape sequences to a temporary file and copy the path to the clipboard."), }, .{ .action = .{ .write_selection_file = .{ .action = .paste, .emit = .vt, } }, - .title = "Copy Selection as ANSI Sequences to Temporary File and Paste Path", - .description = "Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file.", + .title = i18n.N_("Copy Selection as ANSI Sequences to Temporary File and Paste Path"), + .description = i18n.N_("Copy the selection contents as ANSI escape sequences to a temporary file and paste the path to the file."), }, .{ .action = .{ .write_selection_file = .{ .action = .open, .emit = .vt, } }, - .title = "Copy Selection as ANSI Sequences to Temporary File and Open", - .description = "Copy the selection contents as ANSI escape sequences to a temporary file and open it.", + .title = i18n.N_("Copy Selection as ANSI Sequences to Temporary File and Open"), + .description = i18n.N_("Copy the selection contents as ANSI escape sequences to a temporary file and open it."), }, }, .new_window => comptime &.{.{ .action = .new_window, - .title = "New Window", - .description = "Open a new window.", + .title = i18n.N_("New Window"), + .description = i18n.N_("Open a new window."), }}, .new_tab => comptime &.{.{ .action = .new_tab, - .title = "New Tab", - .description = "Open a new tab.", + .title = i18n.N_("New Tab"), + .description = i18n.N_("Open a new tab."), }}, .move_tab => comptime &.{ .{ .action = .{ .move_tab = -1 }, - .title = "Move Tab Left", - .description = "Move the current tab to the left.", + .title = i18n.N_("Move Tab Left"), + .description = i18n.N_("Move the current tab to the left."), }, .{ .action = .{ .move_tab = 1 }, - .title = "Move Tab Right", - .description = "Move the current tab to the right.", + .title = i18n.N_("Move Tab Right"), + .description = i18n.N_("Move the current tab to the right."), }, }, + .move_tab_to_new_window => comptime &.{.{ + .action = .move_tab_to_new_window, + .title = i18n.N_("Move Tab to New Window"), + .description = i18n.N_("Move the current tab to a new window."), + }}, + .toggle_tab_overview => comptime &.{.{ .action = .toggle_tab_overview, - .title = "Toggle Tab Overview", - .description = "Toggle the tab overview.", + .title = i18n.N_("Toggle Tab Overview"), + .description = i18n.N_("Toggle the tab overview."), }}, .prompt_surface_title => comptime &.{.{ .action = .prompt_surface_title, - .title = "Change Terminal Title…", - .description = "Prompt for a new title for the current terminal.", + .title = i18n.N_("Change Terminal Title…"), + .description = i18n.N_("Prompt for a new title for the current terminal."), }}, .prompt_tab_title => comptime &.{.{ .action = .prompt_tab_title, - .title = "Change Tab Title…", - .description = "Prompt for a new title for the current tab.", + .title = i18n.N_("Change Tab Title…"), + .description = i18n.N_("Prompt for a new title for the current tab."), + }}, + + .prompt_window_title => comptime &.{.{ + .action = .prompt_window_title, + .title = "Change Window Title…", + .description = "Prompt for a new title for the current window.", }}, .new_split => comptime &.{ .{ .action = .{ .new_split = .left }, - .title = "Split Left", - .description = "Split the terminal to the left.", + .title = i18n.N_("Split Left"), + .description = i18n.N_("Split the terminal to the left."), }, .{ .action = .{ .new_split = .right }, - .title = "Split Right", - .description = "Split the terminal to the right.", + .title = i18n.N_("Split Right"), + .description = i18n.N_("Split the terminal to the right."), }, .{ .action = .{ .new_split = .up }, - .title = "Split Up", - .description = "Split the terminal up.", + .title = i18n.N_("Split Up"), + .description = i18n.N_("Split the terminal up."), }, .{ .action = .{ .new_split = .down }, - .title = "Split Down", - .description = "Split the terminal down.", + .title = i18n.N_("Split Down"), + .description = i18n.N_("Split the terminal down."), }, }, .goto_split => comptime &.{ .{ .action = .{ .goto_split = .previous }, - .title = "Focus Split: Previous", - .description = "Focus the previous split, if any.", + .title = i18n.N_("Focus Split: Previous"), + .description = i18n.N_("Focus the previous split, if any."), }, .{ .action = .{ .goto_split = .next }, - .title = "Focus Split: Next", - .description = "Focus the next split, if any.", + .title = i18n.N_("Focus Split: Next"), + .description = i18n.N_("Focus the next split, if any."), }, .{ .action = .{ .goto_split = .left }, - .title = "Focus Split: Left", - .description = "Focus the split to the left, if it exists.", + .title = i18n.N_("Focus Split: Left"), + .description = i18n.N_("Focus the split to the left, if it exists."), }, .{ .action = .{ .goto_split = .right }, - .title = "Focus Split: Right", - .description = "Focus the split to the right, if it exists.", + .title = i18n.N_("Focus Split: Right"), + .description = i18n.N_("Focus the split to the right, if it exists."), }, .{ .action = .{ .goto_split = .up }, - .title = "Focus Split: Up", - .description = "Focus the split above, if it exists.", + .title = i18n.N_("Focus Split: Up"), + .description = i18n.N_("Focus the split above, if it exists."), }, .{ .action = .{ .goto_split = .down }, - .title = "Focus Split: Down", - .description = "Focus the split below, if it exists.", + .title = i18n.N_("Focus Split: Down"), + .description = i18n.N_("Focus the split below, if it exists."), }, }, .goto_window => comptime &.{ .{ .action = .{ .goto_window = .previous }, - .title = "Focus Window: Previous", - .description = "Focus the previous window, if any.", + .title = i18n.N_("Focus Window: Previous"), + .description = i18n.N_("Focus the previous window, if any."), }, .{ .action = .{ .goto_window = .next }, - .title = "Focus Window: Next", - .description = "Focus the next window, if any.", + .title = i18n.N_("Focus Window: Next"), + .description = i18n.N_("Focus the next window, if any."), }, }, .toggle_split_zoom => comptime &.{.{ .action = .toggle_split_zoom, - .title = "Toggle Split Zoom", - .description = "Toggle the zoom state of the current split.", + .title = i18n.N_("Toggle Split Zoom"), + .description = i18n.N_("Toggle the zoom state of the current split."), }}, .toggle_readonly => comptime &.{.{ .action = .toggle_readonly, - .title = "Toggle Read-Only Mode", - .description = "Toggle read-only mode for the current surface.", + .title = i18n.N_("Toggle Read-Only Mode"), + .description = i18n.N_("Toggle read-only mode for the current surface."), }}, .equalize_splits => comptime &.{.{ .action = .equalize_splits, - .title = "Equalize Splits", - .description = "Equalize the size of all splits.", + .title = i18n.N_("Equalize Splits"), + .description = i18n.N_("Equalize the size of all splits."), }}, .reset_window_size => comptime &.{.{ .action = .reset_window_size, - .title = "Reset Window Size", - .description = "Reset the window size to the default.", + .title = i18n.N_("Reset Window Size"), + .description = i18n.N_("Reset the window size to the default."), }}, .inspector => comptime &.{.{ .action = .{ .inspector = .toggle }, - .title = "Toggle Inspector", - .description = "Toggle the inspector.", + .title = i18n.N_("Toggle Inspector"), + .description = i18n.N_("Toggle the inspector."), }}, .show_gtk_inspector => comptime &.{.{ .action = .show_gtk_inspector, - .title = "Show the GTK Inspector", - .description = "Show the GTK inspector.", + .title = i18n.N_("Show the GTK Inspector"), + .description = i18n.N_("Show the GTK inspector."), }}, .show_on_screen_keyboard => comptime &.{.{ .action = .show_on_screen_keyboard, - .title = "Show On-Screen Keyboard", - .description = "Show the on-screen keyboard if present.", + .title = i18n.N_("Show On-Screen Keyboard"), + .description = i18n.N_("Show the on-screen keyboard if present."), }}, - .open_config => comptime &.{.{ - .action = .open_config, - .title = "Open Config", - .description = "Open the config file.", - }}, + .open_config => comptime &.{ + .{ + .action = .{ .open_config = .os_open }, + .title = i18n.N_("Open Config Using OS editor"), + .description = i18n.N_("Open the config file with the OS's default editor."), + }, + .{ + .action = .{ .open_config = .new_window }, + .title = i18n.N_("Open Config in New Terminal Window"), + .description = i18n.N_("Open the config file in a new window using $EDITOR or $VISUAL."), + }, + }, .reload_config => comptime &.{.{ .action = .reload_config, - .title = "Reload Config", - .description = "Reload the config file.", + .title = i18n.N_("Reload Config"), + .description = i18n.N_("Reload the config file."), }}, .close_surface => comptime &.{.{ .action = .close_surface, - .title = "Close Terminal", - .description = "Close the current terminal.", + .title = i18n.N_("Close Terminal"), + .description = i18n.N_("Close the current terminal."), }}, .close_tab => comptime &.{ .{ .action = .{ .close_tab = .this }, - .title = "Close Tab", - .description = "Close the current tab.", + .title = i18n.N_("Close Tab"), + .description = i18n.N_("Close the current tab."), }, .{ .action = .{ .close_tab = .other }, - .title = "Close Other Tabs", - .description = "Close all tabs in this window except the current one.", + .title = i18n.N_("Close Other Tabs"), + .description = i18n.N_("Close all tabs in this window except the current one."), }, .{ .action = .{ .close_tab = .right }, - .title = "Close Tabs to the Right", - .description = "Close all tabs to the right of the current one.", + .title = i18n.N_("Close Tabs to the Right"), + .description = i18n.N_("Close all tabs to the right of the current one."), }, }, .close_window => comptime &.{.{ .action = .close_window, - .title = "Close Window", - .description = "Close the current window.", + .title = i18n.N_("Close Window"), + .description = i18n.N_("Close the current window."), }}, .close_all_windows => comptime &.{.{ .action = .close_all_windows, - .title = "Close All Windows", - .description = "Close all windows.", + .title = i18n.N_("Close All Windows"), + .description = i18n.N_("Close all windows."), }}, .toggle_maximize => comptime &.{.{ .action = .toggle_maximize, - .title = "Toggle Maximize", - .description = "Toggle the maximized state of the current window.", + .title = i18n.N_("Toggle Maximize"), + .description = i18n.N_("Toggle the maximized state of the current window."), }}, .toggle_fullscreen => comptime &.{.{ .action = .toggle_fullscreen, - .title = "Toggle Fullscreen", - .description = "Toggle the fullscreen state of the current window.", + .title = i18n.N_("Toggle Fullscreen"), + .description = i18n.N_("Toggle the fullscreen state of the current window."), }}, .toggle_window_decorations => comptime &.{.{ .action = .toggle_window_decorations, - .title = "Toggle Window Decorations", - .description = "Toggle the window decorations.", + .title = i18n.N_("Toggle Window Decorations"), + .description = i18n.N_("Toggle the window decorations."), }}, .toggle_window_float_on_top => comptime &.{.{ .action = .toggle_window_float_on_top, - .title = "Toggle Float on Top", - .description = "Toggle the float on top state of the current window.", + .title = i18n.N_("Toggle Float on Top"), + .description = i18n.N_("Toggle the float on top state of the current window."), }}, .toggle_secure_input => comptime &.{.{ .action = .toggle_secure_input, - .title = "Toggle Secure Input", - .description = "Toggle secure input mode.", + .title = i18n.N_("Toggle Secure Input"), + .description = i18n.N_("Toggle secure input mode."), }}, .toggle_mouse_reporting => comptime &.{.{ .action = .toggle_mouse_reporting, - .title = "Toggle Mouse Reporting", - .description = "Toggle whether mouse events are reported to terminal applications.", + .title = i18n.N_("Toggle Mouse Reporting"), + .description = i18n.N_("Toggle whether mouse events are reported to terminal applications."), }}, .toggle_background_opacity => comptime &.{.{ .action = .toggle_background_opacity, - .title = "Toggle Background Opacity", - .description = "Toggle the background opacity of a window that started transparent.", + .title = i18n.N_("Toggle Background Opacity"), + .description = i18n.N_("Toggle the background opacity of a window that started transparent."), }}, .check_for_updates => comptime &.{.{ .action = .check_for_updates, - .title = "Check for Updates", - .description = "Check for updates to the application.", + .title = i18n.N_("Check for Updates"), + .description = i18n.N_("Check for updates to the application."), }}, .undo => comptime &.{.{ .action = .undo, - .title = "Undo", - .description = "Undo the last action.", + .title = i18n.N_("Undo"), + .description = i18n.N_("Undo the last action."), }}, .redo => comptime &.{.{ .action = .redo, - .title = "Redo", - .description = "Redo the last undone action.", + .title = i18n.N_("Redo"), + .description = i18n.N_("Redo the last undone action."), }}, .quit => comptime &.{.{ .action = .quit, - .title = "Quit", - .description = "Quit the application.", + .title = i18n.N_("Quit"), + .description = i18n.N_("Quit the application."), }}, .text => comptime &.{.{ .action = .{ .text = "👻" }, - .title = "Ghostty", - .description = "Put a little Ghostty in your terminal.", + .title = i18n.N_("Ghostty"), + .description = i18n.N_("Put a little Ghostty in your terminal."), }}, // No commands because they're parameterized and there @@ -691,6 +719,7 @@ fn actionCommands(action: Action.Key) []const Command { .set_font_size, .set_surface_title, .set_tab_title, + .set_window_title, .search, .scroll_to_row, .scroll_page_fractional, diff --git a/vendor/libghostty-vt/src/input/function_keys.zig b/vendor/libghostty-vt/src/input/function_keys.zig index 66ab4bc4..995e576f 100644 --- a/vendor/libghostty-vt/src/input/function_keys.zig +++ b/vendor/libghostty-vt/src/input/function_keys.zig @@ -88,8 +88,10 @@ pub const keys = keys: { result.set(.delete, pcStyle("\x1b[3;{}~") ++ .{Entry{ .sequence = "\x1B[3~" }}); result.set(.page_up, pcStyle("\x1b[5;{}~") ++ .{Entry{ .sequence = "\x1B[5~" }}); result.set(.page_down, pcStyle("\x1b[6;{}~") ++ .{Entry{ .sequence = "\x1B[6~" }}); + result.set(.help, pcStyle("\x1b[28;{}~") ++ .{Entry{ .sequence = "\x1B[28~" }}); + result.set(.context_menu, pcStyle("\x1b[29;{}~") ++ .{Entry{ .sequence = "\x1B[29~" }}); - // Function Keys. todo: f13-f35 but we need to add to input.Key + // Function Keys. todo: f26-f35 but we need to add to input.Key result.set(.f1, pcStyle("\x1b[1;{}P") ++ .{Entry{ .sequence = "\x1BOP" }}); result.set(.f2, pcStyle("\x1b[1;{}Q") ++ .{Entry{ .sequence = "\x1BOQ" }}); result.set(.f3, pcStyle("\x1b[13;{}~") ++ .{Entry{ .sequence = "\x1BOR" }}); @@ -102,24 +104,37 @@ pub const keys = keys: { result.set(.f10, pcStyle("\x1b[21;{}~") ++ .{Entry{ .sequence = "\x1B[21~" }}); result.set(.f11, pcStyle("\x1b[23;{}~") ++ .{Entry{ .sequence = "\x1B[23~" }}); result.set(.f12, pcStyle("\x1b[24;{}~") ++ .{Entry{ .sequence = "\x1B[24~" }}); + result.set(.f13, pcStyle("\x1b[25;{}~") ++ .{Entry{ .sequence = "\x1B[25~" }}); + result.set(.f14, pcStyle("\x1b[26;{}~") ++ .{Entry{ .sequence = "\x1B[26~" }}); + result.set(.f15, pcStyle("\x1b[28;{}~") ++ .{Entry{ .sequence = "\x1B[28~" }}); + result.set(.f16, pcStyle("\x1b[29;{}~") ++ .{Entry{ .sequence = "\x1B[29~" }}); + result.set(.f17, pcStyle("\x1b[31;{}~") ++ .{Entry{ .sequence = "\x1B[31~" }}); + result.set(.f18, pcStyle("\x1b[32;{}~") ++ .{Entry{ .sequence = "\x1B[32~" }}); + result.set(.f19, pcStyle("\x1b[33;{}~") ++ .{Entry{ .sequence = "\x1B[33~" }}); + result.set(.f20, pcStyle("\x1b[34;{}~") ++ .{Entry{ .sequence = "\x1B[34~" }}); + result.set(.f21, pcStyle("\x1b[42;{}~") ++ .{Entry{ .sequence = "\x1B[42~" }}); + result.set(.f22, pcStyle("\x1b[43;{}~") ++ .{Entry{ .sequence = "\x1B[43~" }}); + result.set(.f23, pcStyle("\x1b[44;{}~") ++ .{Entry{ .sequence = "\x1B[44~" }}); + result.set(.f24, pcStyle("\x1b[45;{}~") ++ .{Entry{ .sequence = "\x1B[45~" }}); + result.set(.f25, pcStyle("\x1b[46;{}~") ++ .{Entry{ .sequence = "\x1B[46~" }}); // Keypad keys - result.set(.numpad_0, kpKeys("p")); - result.set(.numpad_1, kpKeys("q")); - result.set(.numpad_2, kpKeys("r")); - result.set(.numpad_3, kpKeys("s")); - result.set(.numpad_4, kpKeys("t")); - result.set(.numpad_5, kpKeys("u")); - result.set(.numpad_6, kpKeys("v")); - result.set(.numpad_7, kpKeys("w")); - result.set(.numpad_8, kpKeys("x")); - result.set(.numpad_9, kpKeys("y")); - result.set(.numpad_decimal, kpKeys("n")); - result.set(.numpad_divide, kpKeys("o")); - result.set(.numpad_multiply, kpKeys("j")); - result.set(.numpad_subtract, kpKeys("m")); - result.set(.numpad_add, kpKeys("k")); - result.set(.numpad_enter, kpKeys("M") ++ .{Entry{ .sequence = "\r" }}); + result.set(.numpad_0, kpKeys("p", "0")); + result.set(.numpad_1, kpKeys("q", "1")); + result.set(.numpad_2, kpKeys("r", "2")); + result.set(.numpad_3, kpKeys("s", "3")); + result.set(.numpad_4, kpKeys("t", "4")); + result.set(.numpad_5, kpKeys("u", "5")); + result.set(.numpad_6, kpKeys("v", "6")); + result.set(.numpad_7, kpKeys("w", "7")); + result.set(.numpad_8, kpKeys("x", "8")); + result.set(.numpad_9, kpKeys("y", "9")); + result.set(.numpad_decimal, kpKeys("n", ".")); + result.set(.numpad_divide, kpKeys("o", "/")); + result.set(.numpad_multiply, kpKeys("j", "*")); + result.set(.numpad_subtract, kpKeys("m", "-")); + result.set(.numpad_add, kpKeys("k", "+")); + result.set(.numpad_enter, kpKeys("M", "\r")); result.set(.numpad_up, pcStyle("\x1b[1;{}A") ++ cursorKey("\x1b[A", "\x1bOA")); result.set(.numpad_down, pcStyle("\x1b[1;{}B") ++ cursorKey("\x1b[B", "\x1bOB")); result.set(.numpad_right, pcStyle("\x1b[1;{}C") ++ cursorKey("\x1b[C", "\x1bOC")); @@ -224,7 +239,8 @@ pub const keys = keys: { result.set(.escape, &.{ .{ .mods = .{ .shift = true }, .sequence = "\x1b[27;2;27~" }, - .{ .mods = .{ .alt = true }, .sequence = "\x1b\x1b" }, + .{ .mods = .{ .alt = true }, .modify_other_keys = .set, .sequence = "\x1b\x1b" }, + .{ .mods = .{ .alt = true }, .modify_other_keys = .set_other, .sequence = "\x1b[27;3;27~" }, .{ .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~" }, @@ -257,10 +273,12 @@ fn kpDefault(comptime suffix: []const u8) []const Entry { /// Returns the entries for a keypad key. The suffix is the final character /// of the sent sequence, such as "r" for kp_2. -fn kpKeys(comptime suffix: []const u8) []const Entry { +fn kpKeys(comptime suffix: []const u8, comptime normal: []const u8) []const Entry { const pc = pcStyle("\x1bO{}" ++ suffix); for (pc) |*entry| entry.keypad = .application; - return kpDefault(suffix) ++ pc; + return kpDefault(suffix) ++ pc ++ .{ + Entry{ .keypad = .normal, .sequence = normal }, + }; } /// Returns entries that are dependent on cursor key settings. diff --git a/vendor/libghostty-vt/src/input/key.zig b/vendor/libghostty-vt/src/input/key.zig index 9c04f01e..1bf64919 100644 --- a/vendor/libghostty-vt/src/input/key.zig +++ b/vendor/libghostty-vt/src/input/key.zig @@ -352,8 +352,7 @@ pub const Key = enum(c_int) { )) |key| return key; // We need to convert FooBar to foo_bar - var fbs = std.io.fixedBufferStream(&result); - const w = fbs.writer(); + var w: std.Io.Writer = .fixed(&result); for (code, 0..) |ch, i| switch (ch) { 'a'...'z' => w.writeByte(ch) catch return null, @@ -367,7 +366,7 @@ pub const Key = enum(c_int) { else => return null, }; - return std.meta.stringToEnum(Key, fbs.getWritten()); + return std.meta.stringToEnum(Key, w.buffered()); } /// Converts a Ghostty key enum value to a W3C key code. @@ -379,8 +378,7 @@ pub const Key = enum(c_int) { const name = @tagName(tag); var buf: [128]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const w = fbs.writer(); + var w: std.Io.Writer = .fixed(&buf); var i: usize = 0; while (i < name.len) { if (i == 0) { @@ -396,7 +394,7 @@ pub const Key = enum(c_int) { } const written = buf; - const result = written[0..fbs.getWritten().len]; + const result = written[0..w.end]; break :w3c result; }, }; diff --git a/vendor/libghostty-vt/src/input/key_encode.zig b/vendor/libghostty-vt/src/input/key_encode.zig index 6ab5a4cc..53a7deaa 100644 --- a/vendor/libghostty-vt/src/input/key_encode.zig +++ b/vendor/libghostty-vt/src/input/key_encode.zig @@ -224,9 +224,11 @@ fn kitty( const entry = entry_ orelse { // No entry found. If we have UTF-8 text this is a pure text event // (e.g. composed/IME text), so send it as-is so programs can - // still receive it. - if (event.utf8.len > 0) return try writer.writeAll(event.utf8); - return; + // still receive it. Release events never insert text, same as the + // plain-text path above. + if (event.action == .release) return; + if (event.utf8.len == 0) return; + return try writer.writeAll(event.utf8); }; // If this is just a modifier we require "report all" to send the sequence. @@ -377,42 +379,10 @@ fn legacy( return try writer.writeAll(sequence); } - // If we match a control sequence, we output that directly. For - // ctrlSeq we have to use all mods because we want it to only - // match ctrl+<char>. - if (ctrlSeq( - event.key, - event.utf8, - event.unshifted_codepoint, - all_mods, - )) |char| { - // C0 sequences support alt-as-esc prefixing. - if (binding_mods.alt) { - try writer.writeByte(0x1B); - try writer.writeByte(char); - return; - } - - try writer.writeByte(char); - return; - } - - // If we have no UTF8 text then the only possibility is the - // alt-prefix handling of unshifted codepoints... so we process that. - const utf8 = event.utf8; - if (utf8.len == 0) { - if (try legacyAltPrefix( - event, - binding_mods, - all_mods, - opts, - )) |byte| try writer.print("\x1B{c}", .{byte}); - return; - } - // In modify other keys state 2, we send the CSI 27 sequence - // for any char with a modifier. Ctrl sequences like Ctrl+a - // are already handled above. + // for any char with a modifier. This must happen before converting + // Ctrl+<char> to a C0 byte because mode 2 encodes those keys, too. + const utf8 = event.utf8; if (opts.modify_other_keys_state_2) modify_other: { const view = std.unicode.Utf8View.init(utf8) catch { // Assume invalid UTF-8 means we no UTF-8. @@ -474,6 +444,39 @@ fn legacy( } } + // If we match a control sequence, we output that directly. For + // ctrlSeq we have to use all mods because we want it to only + // match ctrl+<char>. + if (ctrlSeq( + event.key, + event.utf8, + event.unshifted_codepoint, + all_mods, + )) |char| { + // C0 sequences support alt-as-esc prefixing. + if (binding_mods.alt) { + try writer.writeByte(0x1B); + try writer.writeByte(char); + return; + } + + try writer.writeByte(char); + return; + } + + // If we have no UTF8 text then the only possibility is the + // alt-prefix handling of unshifted codepoints... so we process that. + if (utf8.len == 0) { + _ = try legacyAltPrefix( + writer, + event, + binding_mods, + all_mods, + opts, + ); + return; + } + // Let's see if we should apply fixterms to this codepoint. // At this stage of key processing, we only need to apply fixterms // to unicode codepoints if we have ctrl set. @@ -523,13 +526,12 @@ fn legacy( // If we have alt-pressed and alt-esc-prefix is enabled, then // we need to prefix the utf8 sequence with an esc. if (try legacyAltPrefix( + writer, event, binding_mods, all_mods, opts, - )) |byte| { - return try writer.print("\x1B{c}", .{byte}); - } + )) return; // If we are on macOS, command+keys do not encode text. It isn't // typical for command+keys on macOS to ever encode text. They @@ -548,47 +550,71 @@ fn legacy( } fn legacyAltPrefix( + writer: *std.Io.Writer, event: key.KeyEvent, binding_mods: key.Mods, mods: key.Mods, opts: Options, -) !?u8 { +) std.Io.Writer.Error!bool { // This only takes effect with alt pressed - if (!binding_mods.alt or !opts.alt_esc_prefix) return null; + if (!binding_mods.alt or !opts.alt_esc_prefix) return false; // On macOS, we only handle option like alt in certain // circumstances. Otherwise, macOS does a unicode translation // and we allow that to happen. if (comptime builtin.os.tag == .macos) { switch (opts.macos_option_as_alt) { - .false => return null, - .left => if (mods.sides.alt == .right) return null, - .right => if (mods.sides.alt == .left) return null, + .false => return false, + .left => if (mods.sides.alt == .right) return false, + .right => if (mods.sides.alt == .left) return false, .true => {}, } } - // Otherwise, we require utf8 to already have the byte represented. + // A single byte is already the exact text we want to prefix. In + // particular, this preserves shifted ASCII punctuation. const utf8 = event.utf8; if (utf8.len == 1) { - if (std.math.cast(u8, utf8[0])) |byte| { - return byte; - } + try writer.writeByte(0x1B); + try writer.writeAll(utf8); + return true; } - // If UTF8 isn't set, we will allow unshifted codepoints through. - if (event.unshifted_codepoint > 0) { - if (std.math.cast( - u8, - event.unshifted_codepoint, - )) |byte| { - return byte; + var unshifted_buf: [4]u8 = undefined; + const value: []const u8 = value: { + // On macOS, Option may translate the text into a different Unicode + // value. When Option is configured as Alt, use the physical key's + // unshifted codepoint just as the single-byte implementation did. + if (comptime builtin.os.tag == .macos) { + if (event.unshifted_codepoint > 0) { + const len = std.unicode.utf8Encode( + event.unshifted_codepoint, + &unshifted_buf, + ) catch return false; + break :value unshifted_buf[0..len]; + } } - } - // Else, we can't figure out the byte to alt-prefix so we - // exit this handling. - return null; + // Outside of the macOS translation case, prefix the complete UTF-8 + // text rather than truncating it to a single byte. + if (utf8.len > 0) break :value utf8; + + // Frontends may omit UTF-8 while still supplying the physical key's + // unshifted codepoint. Encode the complete scalar in that case. + if (event.unshifted_codepoint > 0) { + const len = std.unicode.utf8Encode( + event.unshifted_codepoint, + &unshifted_buf, + ) catch return false; + break :value unshifted_buf[0..len]; + } + + return false; + }; + + try writer.writeByte(0x1B); + try writer.writeAll(value); + return true; } /// A helper to memcpy a src value to a buffer and return the result. @@ -1357,6 +1383,22 @@ test "kitty: shift+backspace emits CSI u" { try testing.expectEqualStrings("\x1b[127;2u", writer.buffered()); } +test "kitty: alt+backspace emits CSI u" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .key = .backspace, + .mods = .{ .alt = true }, + // macOS may mark Option as consumed while translating the key. With + // no attached control text, all modifiers must remain effective. + .consumed_mods = .{ .alt = true }, + .utf8 = "", + }, .{ + .kitty_flags = .{ .disambiguate = true }, + }); + try testing.expectEqualStrings("\x1b[127;3u", writer.buffered()); +} + test "kitty: shift+enter emits CSI u" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); @@ -1492,6 +1534,50 @@ test "kitty: composed text with report all" { try testing.expectEqualStrings("\xc3\xbb", writer.buffered()); } +// A key whose base (unshifted) keysym is a dead key has no unshifted +// codepoint, so it has no kitty entry and falls back to writing the text +// directly. That must not happen on release or the text is inserted twice. +test "kitty: text fallback on release" { + for ([_]bool{ false, true }) |report_all| { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .release, + .key = .unidentified, + .mods = .{ .shift = true }, + .utf8 = "!", + }, .{ + .kitty_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = report_all, + .report_associated = true, + }, + }); + try testing.expectEqualStrings("", writer.buffered()); + } +} + +test "kitty: text fallback on repeat" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try kitty(&writer, .{ + .action = .repeat, + .key = .unidentified, + .mods = .{ .shift = true }, + .utf8 = "!", + }, .{ + .kitty_flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_associated = true, + }, + }); + try testing.expectEqualStrings("!", writer.buffered()); +} + test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); @@ -1971,6 +2057,28 @@ test "legacy: esc with utf8 (dead key state)" { try testing.expectEqualStrings("A", writer.buffered()); } +test "legacy: alt+escape with modify other state 2" { + var buf: [128]u8 = undefined; + + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .escape, + .mods = .{ .alt = true }, + }, .{}); + try testing.expectEqualStrings("\x1b\x1b", writer.buffered()); + } + + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .escape, + .mods = .{ .alt = true }, + }, .{ .modify_other_keys_state_2 = true }); + try testing.expectEqualStrings("\x1b[27;3;27~", writer.buffered()); + } +} + test "legacy: ctrl+shift+minus (underscore on US)" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); @@ -2021,6 +2129,63 @@ test "legacy: alt+e only unshifted" { try testing.expectEqualStrings("\x1Be", writer.buffered()); } +test "legacy: alt+unicode" { + const Case = struct { + text: []const u8, + codepoint: u21, + expected: []const u8, + }; + const cases = [_]Case{ + .{ .text = "é", .codepoint = 'é', .expected = "\x1B" ++ "é" }, + .{ .text = "ő", .codepoint = 'ő', .expected = "\x1B" ++ "ő" }, + .{ .text = "界", .codepoint = '界', .expected = "\x1B" ++ "界" }, + .{ .text = "😀", .codepoint = '😀', .expected = "\x1B" ++ "😀" }, + }; + + for (cases) |case| { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .unidentified, + .utf8 = case.text, + .unshifted_codepoint = case.codepoint, + .mods = .{ .alt = true }, + }, .{ + .alt_esc_prefix = true, + .macos_option_as_alt = .true, + }); + try testing.expectEqualStrings(case.expected, writer.buffered()); + } +} + +test "legacy: alt+unicode only unshifted" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .unidentified, + .unshifted_codepoint = '界', + .mods = .{ .alt = true }, + }, .{ + .alt_esc_prefix = true, + .macos_option_as_alt = .true, + }); + try testing.expectEqualStrings("\x1B" ++ "界", writer.buffered()); +} + +test "legacy: alt+unicode without unshifted" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .unidentified, + .utf8 = "😀", + .mods = .{ .alt = true }, + }, .{ + .alt_esc_prefix = true, + .macos_option_as_alt = .true, + }); + try testing.expectEqualStrings("\x1B" ++ "😀", writer.buffered()); +} + test "legacy: alt+x macos" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; @@ -2064,8 +2229,9 @@ test "legacy: alt+ф" { .mods = .{ .alt = true }, }, .{ .alt_esc_prefix = true, + .macos_option_as_alt = .true, }); - try testing.expectEqualStrings("ф", writer.buffered()); + try testing.expectEqualStrings("\x1B" ++ "ф", writer.buffered()); } test "legacy: ctrl+c" { @@ -2157,6 +2323,30 @@ test "legacy: ctrl+shift+char with modify other state 2" { try testing.expectEqualStrings("\x1b[27;6;72~", writer.buffered()); } +test "legacy: ctrl+char with modify other state 2" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_p, + .mods = .{ .ctrl = true }, + .utf8 = "p", + }, .{ + .modify_other_keys_state_2 = true, + }); + try testing.expectEqualStrings("\x1b[27;5;112~", writer.buffered()); +} + +test "legacy: ctrl+char without modify other state 2" { + var buf: [128]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .key_p, + .mods = .{ .ctrl = true }, + .utf8 = "p", + }, .{}); + try testing.expectEqualStrings("\x10", writer.buffered()); +} + test "legacy: ctrl+shift+char with modify other state 2 and consumed mods" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); @@ -2293,6 +2483,35 @@ test "legacy: keypad 1" { try testing.expectEqualStrings("1", writer.buffered()); } +test "legacy: keypad 1 with modify other state 2" { + var buf: [128]u8 = undefined; + + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{ .ctrl = true }, + .utf8 = "1", + }, .{ + .modify_other_keys_state_2 = true, + }); + try testing.expectEqualStrings("1", writer.buffered()); + } + + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = .numpad_1, + .mods = .{ .ctrl = true }, + .utf8 = "1", + }, .{ + .keypad_key_application = true, + .modify_other_keys_state_2 = true, + }); + try testing.expectEqualStrings("\x1bO5q", writer.buffered()); + } +} + test "legacy: keypad 1 with application keypad" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); @@ -2395,6 +2614,91 @@ test "legacy: f1" { } } +test "legacy: f13 through f25" { + const Case = struct { key: key.Key, code: u8 }; + const cases = [_]Case{ + .{ .key = .f13, .code = 25 }, + .{ .key = .f14, .code = 26 }, + .{ .key = .f15, .code = 28 }, + .{ .key = .f16, .code = 29 }, + .{ .key = .f17, .code = 31 }, + .{ .key = .f18, .code = 32 }, + .{ .key = .f19, .code = 33 }, + .{ .key = .f20, .code = 34 }, + .{ .key = .f21, .code = 42 }, + .{ .key = .f22, .code = 43 }, + .{ .key = .f23, .code = 44 }, + .{ .key = .f24, .code = 45 }, + .{ .key = .f25, .code = 46 }, + }; + + var buf: [128]u8 = undefined; + var expected_buf: [32]u8 = undefined; + for (cases) |case| { + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = case.key }, .{}); + const expected = try std.fmt.bufPrint( + &expected_buf, + "\x1b[{}~", + .{case.code}, + ); + try testing.expectEqualStrings(expected, writer.buffered()); + } + + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = case.key, + .mods = .{ .ctrl = true }, + }, .{ .modify_other_keys_state_2 = true }); + const expected = try std.fmt.bufPrint( + &expected_buf, + "\x1b[{};5~", + .{case.code}, + ); + try testing.expectEqualStrings(expected, writer.buffered()); + } + } +} + +test "legacy: help and context menu" { + const Case = struct { key: key.Key, code: u8 }; + const cases = [_]Case{ + .{ .key = .help, .code = 28 }, + .{ .key = .context_menu, .code = 29 }, + }; + + var buf: [128]u8 = undefined; + var expected_buf: [32]u8 = undefined; + for (cases) |case| { + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ .key = case.key }, .{}); + const expected = try std.fmt.bufPrint( + &expected_buf, + "\x1b[{}~", + .{case.code}, + ); + try testing.expectEqualStrings(expected, writer.buffered()); + } + + { + var writer: std.Io.Writer = .fixed(&buf); + try legacy(&writer, .{ + .key = case.key, + .mods = .{ .ctrl = true }, + }, .{ .modify_other_keys_state_2 = true }); + const expected = try std.fmt.bufPrint( + &expected_buf, + "\x1b[{};5~", + .{case.code}, + ); + try testing.expectEqualStrings(expected, writer.buffered()); + } + } +} + test "legacy: left_shift+tab" { var buf: [128]u8 = undefined; var writer: std.Io.Writer = .fixed(&buf); diff --git a/vendor/libghostty-vt/src/input/key_mods.zig b/vendor/libghostty-vt/src/input/key_mods.zig index 35e1c103..1a981403 100644 --- a/vendor/libghostty-vt/src/input/key_mods.zig +++ b/vendor/libghostty-vt/src/input/key_mods.zig @@ -368,12 +368,11 @@ pub const RemapSet = struct { const to = entry.value_ptr.*; var buf: [64]u8 = undefined; - var fbs = std.io.fixedBufferStream(&buf); - const writer = fbs.writer(); - formatMod(writer, from) catch return error.OutOfMemory; + var writer: std.Io.Writer = .fixed(&buf); + formatMod(&writer, from) catch return error.OutOfMemory; writer.writeByte('=') catch return error.OutOfMemory; - formatMod(writer, to) catch return error.OutOfMemory; - try formatter.formatEntry([]const u8, fbs.getWritten()); + formatMod(&writer, to) catch return error.OutOfMemory; + try formatter.formatEntry([]const u8, writer.buffered()); } } diff --git a/vendor/libghostty-vt/src/input/paste.zig b/vendor/libghostty-vt/src/input/paste.zig index 16b6266b..60a4fdde 100644 --- a/vendor/libghostty-vt/src/input/paste.zig +++ b/vendor/libghostty-vt/src/input/paste.zig @@ -1,6 +1,14 @@ const std = @import("std"); const Terminal = @import("../terminal/Terminal.zig"); +/// The bracketed paste (mode 2004) frame written around the data. +pub const bracketed_prefix = "\x1b[200~"; +pub const bracketed_suffix = "\x1b[201~"; + +/// The maximum number of bytes `encode` adds around the data, so callers +/// can size a buffer for the full encoded result. +pub const max_frame_size = bracketed_prefix.len + bracketed_suffix.len; + pub const Options = struct { /// True if bracketed paste mode is on. bracketed: bool, @@ -93,8 +101,8 @@ pub fn encode( // Bracketed paste mode (mode 2004) wraps pasted data in // fenceposts so that the terminal can ignore things like newlines. if (opts.bracketed) { - result[0] = "\x1b[200~"; - result[2] = "\x1b[201~"; + result[0] = bracketed_prefix; + result[2] = bracketed_suffix; return result; } @@ -116,6 +124,42 @@ pub const Error = error{ MutableRequired, }; +/// Encode the given data for pasting directly into a writer. This is +/// the same transformation as `encode` (unsafe bytes replaced, bracketed +/// frame or newline conversion per `opts`) but the data is copied +/// exactly once: into the writer's buffer, where it is modified in place. +/// This is the form to use when the data is const and the result is +/// being assembled into a single buffer anyway. +/// +/// The data is copied in chunks sized to the writer's buffer, so any +/// writer works; a writer with less total capacity than the writer +/// needs to hold at once reports `error.WriteFailed` as usual. +/// +/// WARNING: The input data is not checked for safety. See `isSafe` +/// and `isSafeWith` to check if the data is safe to paste. +pub fn encodeWriter( + writer: *std.Io.Writer, + data: []const u8, + opts: Options, +) std.Io.Writer.Error!void { + if (opts.bracketed) try writer.writeAll(bracketed_prefix); + + // The byte transformations are position-independent, so the data + // can be copied and encoded chunk by chunk. The frame returned by + // encode is ignored since it's written around the whole data here. + var remaining = data; + while (remaining.len > 0) { + const dest = try writer.writableSliceGreedy(1); + const n = @min(dest.len, remaining.len); + @memcpy(dest[0..n], remaining[0..n]); + _ = encode(dest[0..n], opts); + writer.advance(n); + remaining = remaining[n..]; + } + + if (opts.bracketed) try writer.writeAll(bracketed_suffix); +} + /// Returns true if the data looks safe to paste. Data is considered /// unsafe if it contains any of the following: /// @@ -133,6 +177,22 @@ pub fn isSafe(data: []const u8) bool { std.mem.indexOf(u8, data, "\x1b[201~") == null; } +/// Returns true if the data looks safe to paste given how it will be +/// encoded. This is the terminal-state-aware counterpart of `isSafe`: +/// +/// - Bracketed (mode 2004 on): the program receives the data as one +/// framed unit, so newlines are fine. The data is unsafe only if it +/// contains the end of the frame (`\x1b[201~`), which would let the +/// rest of the data escape the frame and inject commands. +/// - Unbracketed: the same rule as `isSafe`. +/// +/// Callers wanting the conservative rule regardless of terminal state +/// should use `isSafe` instead. +pub fn isSafeWith(data: []const u8, opts: Options) bool { + if (opts.bracketed) return std.mem.indexOf(u8, data, bracketed_suffix) == null; + return isSafe(data); +} + test isSafe { const testing = std.testing; try testing.expect(isSafe("hello")); @@ -141,6 +201,110 @@ test isSafe { try testing.expect(!isSafe("he\x1b[201~llo")); } +test isSafeWith { + const testing = std.testing; + + // Bracketed: newlines are fine, the frame terminator is not. + try testing.expect(isSafeWith("hello", .{ .bracketed = true })); + try testing.expect(isSafeWith("hello\nworld", .{ .bracketed = true })); + try testing.expect(!isSafeWith("he\x1b[201~llo", .{ .bracketed = true })); + try testing.expect(!isSafeWith("hello\n\x1b[201~", .{ .bracketed = true })); + + // Unbracketed: the conservative rule. + try testing.expect(isSafeWith("hello", .{ .bracketed = false })); + try testing.expect(!isSafeWith("hello\nworld", .{ .bracketed = false })); + try testing.expect(!isSafeWith("he\x1b[201~llo", .{ .bracketed = false })); +} + +test "encodeWriter bracketed" { + const testing = std.testing; + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encodeWriter(&writer, "hel\x1blo\nworld", .{ .bracketed = true }); + try testing.expectEqualStrings("\x1b[200~hel lo\nworld\x1b[201~", writer.buffered()); +} + +test "encodeWriter unbracketed" { + const testing = std.testing; + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encodeWriter(&writer, "hel\x00lo\r\nworld", .{ .bracketed = false }); + try testing.expectEqualStrings("hel lo\r\rworld", writer.buffered()); +} + +test "encodeWriter empty" { + const testing = std.testing; + var buf: [64]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try encodeWriter(&writer, "", .{ .bracketed = true }); + try testing.expectEqualStrings("\x1b[200~\x1b[201~", writer.buffered()); + writer = .fixed(&buf); + try encodeWriter(&writer, "", .{ .bracketed = false }); + try testing.expectEqualStrings("", writer.buffered()); +} + +test "encodeWriter chunks through a small writer buffer" { + const testing = std.testing; + const alloc = testing.allocator; + + // A writer with a 4-byte staging buffer that drains into a list, + // so the data is copied and encoded in several chunks. + const Sink = struct { + list: std.ArrayList(u8) = .empty, + writer: std.Io.Writer, + + fn drain( + w: *std.Io.Writer, + data: []const []const u8, + splat: usize, + ) std.Io.Writer.Error!usize { + const self: *@This() = @alignCast(@fieldParentPtr("writer", w)); + self.list.appendSlice(testing.allocator, w.buffered()) catch return error.WriteFailed; + w.end = 0; + var n: usize = 0; + for (data[0 .. data.len - 1]) |slice| { + self.list.appendSlice(testing.allocator, slice) catch return error.WriteFailed; + n += slice.len; + } + for (0..splat) |_| { + self.list.appendSlice(testing.allocator, data[data.len - 1]) catch return error.WriteFailed; + } + return n + splat * data[data.len - 1].len; + } + }; + + var staging: [4]u8 = undefined; + var sink: Sink = .{ .writer = .{ + .buffer = &staging, + .vtable = &.{ .drain = Sink.drain }, + } }; + defer sink.list.deinit(alloc); + + const data = "line one\nline\x1btwo\nline three\n"; + try encodeWriter(&sink.writer, data, .{ .bracketed = true }); + try sink.writer.flush(); + try testing.expectEqualStrings( + "\x1b[200~line one\nline two\nline three\n\x1b[201~", + sink.list.items, + ); +} + +test "encodeWriter too small" { + const testing = std.testing; + var buf: [4]u8 = undefined; + var writer: std.Io.Writer = .fixed(&buf); + try testing.expectError( + error.WriteFailed, + encodeWriter(&writer, "hello", .{ .bracketed = true }), + ); +} + +test max_frame_size { + const testing = std.testing; + const result = try encode(@as([]const u8, ""), .{ .bracketed = true }); + try testing.expectEqual(max_frame_size, result[0].len + result[2].len); +} + test "encode bracketed" { const testing = std.testing; const result = try encode( |
