aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/input
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-10 21:51:57 +0300
committerGitHub <noreply@github.com>2026-09-10 21:51:57 +0300
commit425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch)
treee52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/input
parent90e947a6e26e02f10ec7f1694c4eb5b78a86406b (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.zig94
-rw-r--r--vendor/libghostty-vt/src/input/command.zig387
-rw-r--r--vendor/libghostty-vt/src/input/function_keys.zig58
-rw-r--r--vendor/libghostty-vt/src/input/key.zig10
-rw-r--r--vendor/libghostty-vt/src/input/key_encode.zig428
-rw-r--r--vendor/libghostty-vt/src/input/key_mods.zig9
-rw-r--r--vendor/libghostty-vt/src/input/paste.zig168
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(