From 142e07c502dfbb274892cd5aa640ade31264183d Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Fri, 13 Jun 2025 16:11:46 -0700 Subject: [PATCH 01/86] feat: add SSH integration wrapper for shell integration - Implements opt-in SSH wrapper following sudo pattern - Supports term_only, basic, and full integration levels - Fixes xterm-ghostty TERM compatibility on remote systems - Propagates shell integration environment variables - Allows for automatic installation of terminfo if desired - Addresses GitHub discussions #5892 and #4156 --- src/Surface.zig | 1 + src/config.zig | 1 + src/config/Config.zig | 357 +++--------------- src/shell-integration/bash/ghostty.bash | 208 +++++++--- .../elvish/lib/ghostty-integration.elv | 101 +++++ .../ghostty-shell-integration.fish | 103 ++++- src/shell-integration/zsh/ghostty-integration | 81 ++++ src/termio/Exec.zig | 2 + src/termio/shell_integration.zig | 14 +- 9 files changed, 496 insertions(+), 372 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 6005635d9..78363e87c 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -545,6 +545,7 @@ pub fn init( .env_override = config.env, .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", + .ssh_integration = config.@"ssh-integration", .working_directory = config.@"working-directory", .resources_dir = global_state.resources_dir.host(), .term = config.term, diff --git a/src/config.zig b/src/config.zig index 7f390fb08..e34819fa1 100644 --- a/src/config.zig +++ b/src/config.zig @@ -33,6 +33,7 @@ pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig"); pub const RepeatablePath = Config.RepeatablePath; pub const Path = Config.Path; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; +pub const SSHIntegration = Config.SSHIntegration; pub const WindowPaddingColor = Config.WindowPaddingColor; pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImageFit = Config.BackgroundImageFit; diff --git a/src/config/Config.zig b/src/config/Config.zig index be59ae94f..5fe950576 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -466,93 +466,6 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, -/// Background image for the terminal. -/// -/// This should be a path to a PNG or JPEG file, other image formats are -/// not yet supported. -/// -/// The background image is currently per-terminal, not per-window. If -/// you are a heavy split user, the background image will be repeated across -/// splits. A future improvement to Ghostty will address this. -/// -/// WARNING: Background images are currently duplicated in VRAM per-terminal. -/// For sufficiently large images, this could lead to a large increase in -/// memory usage (specifically VRAM usage). A future Ghostty improvement -/// will resolve this by sharing image textures across terminals. -@"background-image": ?Path = null, - -/// Background image opacity. -/// -/// This is relative to the value of `background-opacity`. -/// -/// A value of `1.0` (the default) will result in the background image being -/// placed on top of the general background color, and then the combined result -/// will be adjusted to the opacity specified by `background-opacity`. -/// -/// A value less than `1.0` will result in the background image being mixed -/// with the general background color before the combined result is adjusted -/// to the configured `background-opacity`. -/// -/// A value greater than `1.0` will result in the background image having a -/// higher opacity than the general background color. For instance, if the -/// configured `background-opacity` is `0.5` and `background-image-opacity` -/// is set to `1.5`, then the final opacity of the background image will be -/// `0.5 * 1.5 = 0.75`. -@"background-image-opacity": f32 = 1.0, - -/// Background image position. -/// -/// Valid values are: -/// * `top-left` -/// * `top-center` -/// * `top-right` -/// * `center-left` -/// * `center` -/// * `center-right` -/// * `bottom-left` -/// * `bottom-center` -/// * `bottom-right` -/// -/// The default value is `center`. -@"background-image-position": BackgroundImagePosition = .center, - -/// Background image fit. -/// -/// Valid values are: -/// -/// * `contain` -/// -/// Preserving the aspect ratio, scale the background image to the largest -/// size that can still be contained within the terminal, so that the whole -/// image is visible. -/// -/// * `cover` -/// -/// Preserving the aspect ratio, scale the background image to the smallest -/// size that can completely cover the terminal. This may result in one or -/// more edges of the image being clipped by the edge of the terminal. -/// -/// * `stretch` -/// -/// Stretch the background image to the full size of the terminal, without -/// preserving the aspect ratio. -/// -/// * `none` -/// -/// Don't scale the background image. -/// -/// The default value is `contain`. -@"background-image-fit": BackgroundImageFit = .contain, - -/// Whether to repeat the background image or not. -/// -/// If this is set to true, the background image will be repeated if there -/// would otherwise be blank space around it because it doesn't completely -/// fill the terminal area. -/// -/// The default value is `false`. -@"background-image-repeat": bool = false, - /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). @@ -2062,27 +1975,14 @@ keybind: Keybinds = .{}, /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` @"shell-integration-features": ShellIntegrationFeatures = .{}, -/// Custom entries into the command palette. +/// SSH integration level. This controls what level of SSH integration +/// is performed when using the ssh wrapper provided by shell integration. +/// Requires shell integration to be enabled to function. /// -/// Each entry requires the title, the corresponding action, and an optional -/// description. Each field should be prefixed with the field name, a colon -/// (`:`), and then the specified value. The syntax for actions is identical -/// to the one for keybind actions. Whitespace in between fields is ignored. +/// See SSHIntegration for available options. /// -/// ```ini -/// command-palette-entry = title:Reset Font Style, action:csi:0m -/// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main -/// ``` -/// -/// By default, the command palette is preloaded with most actions that might -/// be useful in an interactive setting yet do not have easily accessible or -/// memorizable shortcuts. The default entries can be cleared by setting this -/// setting to an empty value: -/// -/// ```ini -/// command-palette-entry = -/// ``` -@"command-palette-entry": RepeatableCommand = .{}, +/// The default value is `off`. +@"ssh-integration": SSHIntegration = .off, /// Sets the reporting format for OSC sequences that request color information. /// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and @@ -2893,9 +2793,6 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // Add our default keybindings try result.keybind.init(alloc); - // Add our default command palette entries - try result.@"command-palette-entry".init(alloc); - // Add our default link for URL detection try result.link.links.append(alloc, .{ .regex = url.regex, @@ -3410,15 +3307,6 @@ fn expandPaths(self: *Config, base: []const u8) !void { &self._diagnostics, ); }, - ?RepeatablePath, ?Path => { - if (@field(self, field.name)) |*path| { - try path.expand( - arena_alloc, - base, - &self._diagnostics, - ); - } - }, else => {}, } } @@ -5083,29 +4971,25 @@ pub const Keybinds = struct { .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_tab = {} }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .next_tab = {} }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } }, .{ .previous_tab = {} }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, - .{ .performable = true }, ); try self.set.put( alloc, @@ -5117,67 +5001,57 @@ pub const Keybinds = struct { .{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .down }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .up }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .down }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .left }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .right }, - .{ .performable = true }, ); // Resizing splits - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .up, 10 } }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .down, 10 } }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .left, 10 } }, - .{ .performable = true }, ); - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .right, 10 } }, - .{ .performable = true }, ); // Viewport scrolling @@ -5248,24 +5122,22 @@ pub const Keybinds = struct { const end: u21 = '8'; var i: u21 = start; while (i <= end) : (i += 1) { - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .unicode = i }, .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, - .{ .performable = true }, ); } - try self.set.putFlags( + try self.set.put( alloc, .{ .key = .{ .unicode = '9' }, .mods = mods, }, .{ .last_tab = {} }, - .{ .performable = true }, ); } @@ -6235,147 +6107,32 @@ pub const ShellIntegrationFeatures = packed struct { title: bool = true, }; -pub const RepeatableCommand = struct { - value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, +/// SSH integration levels for shell integration. +/// Controls how much SSH integration is performed when connecting to remote hosts. +/// +/// Allowable values are: +/// +/// * `off` - No SSH integration, use standard ssh command +/// +/// * `term_only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color) +/// +/// * `basic` - TERM fix + environment variable propagation +/// +/// * `full` - All features: TERM fix + env vars + terminfo installation + shell integration injection +/// +/// The default value is `off`. +pub const SSHIntegration = enum { + off, + term_only, + basic, + full, - pub fn init(self: *RepeatableCommand, alloc: Allocator) !void { - self.value = .empty; - try self.value.appendSlice(alloc, inputpkg.command.defaults); - } - - pub fn parseCLI( - self: *RepeatableCommand, - alloc: Allocator, - input_: ?[]const u8, + pub fn jsonStringify( + self: SSHIntegration, + options: std.json.StringifyOptions, + writer: anytype, ) !void { - // Unset or empty input clears the list - const input = input_ orelse ""; - if (input.len == 0) { - self.value.clearRetainingCapacity(); - return; - } - - const cmd = try cli.args.parseAutoStruct( - inputpkg.Command, - alloc, - input, - ); - try self.value.append(alloc, cmd); - } - - /// Deep copy of the struct. Required by Config. - pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand { - const value = try self.value.clone(alloc); - for (value.items) |*item| { - item.* = try item.clone(alloc); - } - - return .{ .value = value }; - } - - /// Compare if two of our value are equal. Required by Config. - pub fn equal(self: RepeatableCommand, other: RepeatableCommand) bool { - if (self.value.items.len != other.value.items.len) return false; - for (self.value.items, other.value.items) |a, b| { - if (!a.equal(b)) return false; - } - - return true; - } - - /// Used by Formatter - pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void { - if (self.value.items.len == 0) { - try formatter.formatEntry(void, {}); - return; - } - - var buf: [4096]u8 = undefined; - for (self.value.items) |item| { - const str = if (item.description.len > 0) std.fmt.bufPrint( - &buf, - "title:{s},description:{s},action:{}", - .{ item.title, item.description, item.action }, - ) else std.fmt.bufPrint( - &buf, - "title:{s},action:{}", - .{ item.title, item.action }, - ); - try formatter.formatEntry([]const u8, str catch return error.OutOfMemory); - } - } - - test "RepeatableCommand parseCLI" { - const testing = std.testing; - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: RepeatableCommand = .{}; - try list.parseCLI(alloc, "title:Foo,action:ignore"); - try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle"); - try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5"); - - try testing.expectEqual(@as(usize, 3), list.value.items.len); - - try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action); - try testing.expectEqualStrings("Foo", list.value.items[0].title); - - try testing.expect(list.value.items[1].action == .text); - try testing.expectEqualStrings("ale bydle", list.value.items[1].action.text); - try testing.expectEqualStrings("Bar", list.value.items[1].title); - try testing.expectEqualStrings("bobr", list.value.items[1].description); - - try testing.expectEqual( - inputpkg.Binding.Action{ .increase_font_size = 2.5 }, - list.value.items[2].action, - ); - try testing.expectEqualStrings("Quux", list.value.items[2].title); - try testing.expectEqualStrings("boo", list.value.items[2].description); - - try list.parseCLI(alloc, ""); - try testing.expectEqual(@as(usize, 0), list.value.items.len); - } - - test "RepeatableCommand formatConfig empty" { - const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); - - var list: RepeatableCommand = .{}; - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = \n", buf.items); - } - - test "RepeatableCommand formatConfig single item" { - const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); - - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: RepeatableCommand = .{}; - try list.parseCLI(alloc, "title:Bobr, action:text:Bober"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:Bober\n", buf.items); - } - - test "RepeatableCommand formatConfig multiple items" { - const testing = std.testing; - var buf = std.ArrayList(u8).init(testing.allocator); - defer buf.deinit(); - - var arena = ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var list: RepeatableCommand = .{}; - try list.parseCLI(alloc, "title:Bobr, action:text:kurwa"); - try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle"); - try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); - try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:kurwa\na = title:Ja,description:pierdole,action:text:jakie bydle\n", buf.items); + try std.json.stringify(@tagName(self), options, writer); } }; @@ -6834,28 +6591,6 @@ pub const AlphaBlending = enum { } }; -/// See background-image-position -pub const BackgroundImagePosition = enum { - @"top-left", - @"top-center", - @"top-right", - @"center-left", - @"center-center", - @"center-right", - @"bottom-left", - @"bottom-center", - @"bottom-right", - center, -}; - -/// See background-image-fit -pub const BackgroundImageFit = enum { - contain, - cover, - stretch, - none, -}; - /// See freetype-load-flag pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 0766198f9..f542261d4 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -15,8 +15,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# We need to be in interactive mode to proceed. -if [[ "$-" != *i* ]] ; then builtin return; fi +# We need to be in interactive mode and we need to have the Ghostty +# resources dir set which also tells us we're running in Ghostty. +if [[ "$-" != *i* ]]; then builtin return; fi +if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi # When automatic shell integration is active, we were started in POSIX # mode and need to manually recreate the bash startup sequence. @@ -43,7 +45,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then [ -r /etc/profile ] && builtin source "/etc/profile" for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do - [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; } + [ -r "$__ghostty_rcfile" ] && { + builtin source "$__ghostty_rcfile" + break + } done fi else @@ -55,7 +60,10 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then # Void Linux uses /etc/bash/bashrc # Nixos uses /etc/bashrc for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do - [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; } + [ -r "$__ghostty_rcfile" ] && { + builtin source "$__ghostty_rcfile" + break + } done if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi [ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE" @@ -88,15 +96,105 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then fi done if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then - builtin command sudo "$@"; + builtin command sudo "$@" else - builtin command sudo TERMINFO="$TERMINFO" "$@"; + builtin command sudo TERMINFO="$TERMINFO" "$@" fi } fi +if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; then + # Wrap `ssh` command to provide Ghostty SSH integration. + # + # This approach supports wrapping an `ssh` alias, but the alias definition + # must come _after_ this function is defined. Otherwise, the alias expansion + # will take precedence over this function, and it won't be wrapped. + function ssh { + case "$GHOSTTY_SSH_INTEGRATION" in + "term_only") + _ghostty_ssh_term_only "$@" + ;; + "basic") + _ghostty_ssh_basic "$@" + ;; + "full") + _ghostty_ssh_full "$@" + ;; + *) + # Unknown level, fall back to basic + _ghostty_ssh_basic "$@" + ;; + esac + } + + # Level: term_only - Just fix TERM compatibility + _ghostty_ssh_term_only() { + if [[ "$TERM" == "xterm-ghostty" ]]; then + TERM=xterm-256color command ssh "$@" + else + command ssh "$@" + fi + } + + # Level: basic - TERM fix + environment variable propagation + _ghostty_ssh_basic() { + local env_vars=() + + # Fix TERM compatibility + if [[ "$TERM" == "xterm-ghostty" ]]; then + env_vars+=("TERM=xterm-256color") + fi + + # Propagate Ghostty shell integration environment variables + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") + [[ -n "$GHOSTTY_RESOURCES_DIR" ]] && env_vars+=("GHOSTTY_RESOURCES_DIR=$GHOSTTY_RESOURCES_DIR") + + # Execute with environment variables if any were set + if [[ ${#env_vars[@]} -gt 0 ]]; then + env "${env_vars[@]}" ssh "$@" + else + builtin command ssh "$@" + fi + } + + # Level: full - All features + _ghostty_ssh_full() { + # Full integration: Two-step terminfo installation + if command -v infocmp >/dev/null 2>&1; then + echo "Installing Ghostty terminfo on remote host..." >&2 + + # Step 1: Install terminfo using the same approach that works manually + # This requires authentication but is quick and reliable + if infocmp -x xterm-ghostty 2>/dev/null | command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then + echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + + # Step 2: Connect with xterm-ghostty since we know terminfo is now available + local env_vars=() + + # Use xterm-ghostty since we just installed it + env_vars+=("TERM=xterm-ghostty") + + # Propagate Ghostty shell integration environment variables + [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") + [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO") + [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE") + + # Normal SSH connection with Ghostty terminfo available + env "${env_vars[@]}" ssh "$@" + return 0 + else + echo "Terminfo installation failed. Using basic integration." >&2 + fi + fi + + # Fallback to basic integration + _ghostty_ssh_basic "$@" + } + +fi + # Import bash-preexec, safe to do multiple times -builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh" +builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/bash-preexec.sh" # This is set to 1 when we're executing a command so that we don't # send prompt marks multiple times. @@ -104,68 +202,68 @@ _ghostty_executing="" _ghostty_last_reported_cwd="" function __ghostty_precmd() { - local ret="$?" - if test "$_ghostty_executing" != "0"; then - _GHOSTTY_SAVE_PS0="$PS0" - _GHOSTTY_SAVE_PS1="$PS1" - _GHOSTTY_SAVE_PS2="$PS2" + local ret="$?" + if test "$_ghostty_executing" != "0"; then + _GHOSTTY_SAVE_PS0="$PS0" + _GHOSTTY_SAVE_PS1="$PS1" + _GHOSTTY_SAVE_PS2="$PS2" - # Marks - PS1=$PS1'\[\e]133;B\a\]' - PS2=$PS2'\[\e]133;B\a\]' + # Marks + PS1=$PS1'\[\e]133;B\a\]' + PS2=$PS2'\[\e]133;B\a\]' - # bash doesn't redraw the leading lines in a multiline prompt so - # mark the last line as a secondary prompt (k=s) to prevent the - # preceding lines from being erased by ghostty after a resize. - if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then - PS1=$PS1'\[\e]133;A;k=s\a\]' - fi - - # Cursor - if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then - PS1=$PS1'\[\e[5 q\]' - PS0=$PS0'\[\e[0 q\]' - fi - - # Title (working directory) - if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then - PS1=$PS1'\[\e]2;\w\a\]' - fi + # bash doesn't redraw the leading lines in a multiline prompt so + # mark the last line as a secondary prompt (k=s) to prevent the + # preceding lines from being erased by ghostty after a resize. + if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then + PS1=$PS1'\[\e]133;A;k=s\a\]' fi - if test "$_ghostty_executing" != ""; then - # End of current command. Report its status. - builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" + # Cursor + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then + PS1=$PS1'\[\e[5 q\]' + PS0=$PS0'\[\e[0 q\]' fi - # unfortunately bash provides no hooks to detect cwd changes - # in particular this means cwd reporting will not happen for a - # command like cd /test && cat. PS0 is evaluated before cd is run. - if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then - _ghostty_last_reported_cwd="$PWD" - builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD" + # Title (working directory) + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then + PS1=$PS1'\[\e]2;\w\a\]' fi + fi - # Fresh line and start of prompt. - builtin printf "\e]133;A;aid=%s\a" "$BASHPID" - _ghostty_executing=0 + if test "$_ghostty_executing" != ""; then + # End of current command. Report its status. + builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" + fi + + # unfortunately bash provides no hooks to detect cwd changes + # in particular this means cwd reporting will not happen for a + # command like cd /test && cat. PS0 is evaluated before cd is run. + if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then + _ghostty_last_reported_cwd="$PWD" + builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD" + fi + + # Fresh line and start of prompt. + builtin printf "\e]133;A;aid=%s\a" "$BASHPID" + _ghostty_executing=0 } function __ghostty_preexec() { - builtin local cmd="$1" + builtin local cmd="$1" - PS0="$_GHOSTTY_SAVE_PS0" - PS1="$_GHOSTTY_SAVE_PS1" - PS2="$_GHOSTTY_SAVE_PS2" + PS0="$_GHOSTTY_SAVE_PS0" + PS1="$_GHOSTTY_SAVE_PS1" + PS2="$_GHOSTTY_SAVE_PS2" - # Title (current command) - if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then - builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}" - fi + # Title (current command) + if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then + builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}" + fi - # End of input, start of output. - builtin printf "\e]133;C;\a" - _ghostty_executing=1 + # End of input, start of output. + builtin printf "\e]133;C;\a" + _ghostty_executing=1 } preexec_functions+=(__ghostty_preexec) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index a6d052a72..32f9ecbb6 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -98,6 +98,107 @@ (external sudo) $@args } + fn ssh-with-ghostty-integration {|@args| + if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION)) { + if (eq "term_only" $E:GHOSTTY_SSH_INTEGRATION) { + ssh-term-only $@args + } elif (eq "basic" $E:GHOSTTY_SSH_INTEGRATION) { + ssh-basic $@args + } elif (eq "full" $E:GHOSTTY_SSH_INTEGRATION) { + ssh-full $@args + } else { + # Unknown level, fall back to basic + ssh-basic $@args + } + } else { + (external ssh) $@args + } + } + + fn ssh-term-only {|@args| + # Level: term_only - Just fix TERM compatibility + if (eq "xterm-ghostty" $E:TERM) { + TERM=xterm-256color (external ssh) $@args + } else { + (external ssh) $@args + } + } + + fn ssh-basic {|@args| + # Level: basic - TERM fix + environment variable propagation + var env-vars = [] + + # Fix TERM compatibility + if (eq "xterm-ghostty" $E:TERM) { + set env-vars = [$@env-vars TERM=xterm-256color] + } + + # Propagate Ghostty shell integration environment variables + if (has-env GHOSTTY_SHELL_FEATURES) { + if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { + set env-vars = [$@env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES] + } + } + + if (has-env GHOSTTY_RESOURCES_DIR) { + if (not-eq "" $E:GHOSTTY_RESOURCES_DIR) { + set env-vars = [$@env-vars GHOSTTY_RESOURCES_DIR=$E:GHOSTTY_RESOURCES_DIR] + } + } + + # Execute with environment variables if any were set + if (> (count $env-vars) 0) { + (external env) $@env-vars ssh $@args + } else { + (external ssh) $@args + } + } + + fn ghostty-ssh-full {|@args| + # Full integration: Two-step terminfo installation + if (has-external infocmp) { + echo "Installing Ghostty terminfo on remote host..." >&2 + + # Step 1: Install terminfo using the same approach that works manually + # This requires authentication but is quick and reliable + try { + infocmp -x xterm-ghostty 2>/dev/null | command ssh $@args 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null' + echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + + # Step 2: Connect with xterm-ghostty since we know terminfo is now available + var env-vars = [] + + # Use xterm-ghostty since we just installed it + set env-vars = [$@env-vars TERM=xterm-ghostty] + + # Propagate Ghostty shell integration environment variables + if (has-env GHOSTTY_SHELL_INTEGRATION_NO_CURSOR) { + set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR] + } + if (has-env GHOSTTY_SHELL_INTEGRATION_NO_SUDO) { + set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO] + } + if (has-env GHOSTTY_SHELL_INTEGRATION_NO_TITLE) { + set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE] + } + + # Normal SSH connection with Ghostty terminfo available + env $@env-vars ssh $@args + return + } catch e { + echo "Terminfo installation failed. Using basic integration." >&2 + } + } + + # Fallback to basic integration + ghostty-ssh-basic $@args + } + + # Register SSH integration if enabled + if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { + edit:add-var ssh~ $ssh-with-ghostty-integration~ + } + defer { mark-prompt-start report-pwd diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index e7c264e1f..d03c98c7f 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -63,14 +63,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias - if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") + if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x") # Wrap `sudo` command to ensure Ghostty terminfo is preserved function sudo -d "Wrap sudo to preserve terminfo" - set --function sudo_has_sudoedit_flags "no" + set --function sudo_has_sudoedit_flags no for arg in $argv # Check if argument is '-e' or '--edit' (sudoedit flags) - if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg" - set --function sudo_has_sudoedit_flags "yes" + if string match -q -- -e "$arg"; or string match -q -- --edit "$arg" + set --function sudo_has_sudoedit_flags yes break end # Check if argument is neither an option nor a key-value pair @@ -78,7 +78,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" break end end - if test "$sudo_has_sudoedit_flags" = "yes" + if test "$sudo_has_sudoedit_flags" = yes command sudo $argv else command sudo TERMINFO="$TERMINFO" $argv @@ -86,6 +86,99 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end + # SSH integration wrapper + if test -n "$GHOSTTY_SSH_INTEGRATION"; and test "$GHOSTTY_SSH_INTEGRATION" != off + function ssh -d "Wrap ssh to provide Ghostty SSH integration" + switch "$GHOSTTY_SSH_INTEGRATION" + case term_only + _ghostty_ssh_term_only $argv + case basic + _ghostty_ssh_basic $argv + case full + _ghostty_ssh_full $argv + case "*" + # Unknown level, fall back to basic + _ghostty_ssh_basic $argv + end + end + + # Level: term_only - Just fix TERM compatibility + function _ghostty_ssh_term_only -d "SSH with TERM compatibility fix" + if test "$TERM" = xterm-ghostty + TERM=xterm-256color command ssh $argv + else + command ssh $argv + end + end + + # Level: basic - TERM fix + environment variable propagation + function _ghostty_ssh_basic -d "SSH with TERM fix and environment propagation" + # Build environment variables to propagate + set --local env_vars + + # Fix TERM compatibility + if test "$TERM" = xterm-ghostty + set env_vars $env_vars TERM=xterm-256color + end + + # Propagate Ghostty shell integration environment variables + if test -n "$GHOSTTY_SHELL_FEATURES" + set env_vars $env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" + end + + if test -n "$GHOSTTY_RESOURCES_DIR" + set env_vars $env_vars GHOSTTY_RESOURCES_DIR="$GHOSTTY_RESOURCES_DIR" + end + + # Execute with environment variables if any were set + if test (count $env_vars) -gt 0 + env $env_vars ssh $argv + else + command ssh $argv + end + end + + # Level: full - All features + function _ghostty_ssh_full + # Full integration: Two-step terminfo installation + if command -v infocmp >/dev/null 2>&1 + echo "Installing Ghostty terminfo on remote host..." >&2 + + # Step 1: Install terminfo using the same approach that works manually + # This requires authentication but is quick and reliable + if infocmp -x xterm-ghostty 2>/dev/null | command ssh $argv 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null' + echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + + # Step 2: Connect with xterm-ghostty since we know terminfo is now available + set -l env_vars + + # Use xterm-ghostty since we just installed it + set -a env_vars TERM=xterm-ghostty + + # Propagate Ghostty shell integration environment variables + if set -q GHOSTTY_SHELL_INTEGRATION_NO_CURSOR + set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR + end + if set -q GHOSTTY_SHELL_INTEGRATION_NO_SUDO + set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO + end + if set -q GHOSTTY_SHELL_INTEGRATION_NO_TITLE + set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE + end + + # Normal SSH connection with Ghostty terminfo available + env $env_vars ssh $argv + return 0 + else + echo "Terminfo installation failed. Using basic integration." >&2 + end + end + + # Fallback to basic integration + _ghostty_ssh_basic $argv + end + end + # Setup prompt marking function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror # If we never got the output end event, then we need to send it now. diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index c1329683e..58838d1f0 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -243,6 +243,87 @@ _ghostty_deferred_init() { fi } fi + + # SSH + if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; then + # Wrap `ssh` command to provide Ghostty SSH integration + ssh() { + case "$GHOSTTY_SSH_INTEGRATION" in + "term_only") + _ghostty_ssh_term_only "$@" + ;; + "basic") + _ghostty_ssh_basic "$@" + ;; + "full") + _ghostty_ssh_full "$@" + ;; + *) + # Unknown level, fall back to basic + _ghostty_ssh_basic "$@" + ;; + esac + } + + # Level: term_only - Just fix TERM compatibility + _ghostty_ssh_term_only() { + if [[ "$TERM" == "xterm-ghostty" ]]; then + TERM=xterm-256color builtin command ssh "$@" + else + builtin command ssh "$@" + fi + } + + # Level: basic - TERM fix + environment variable propagation + _ghostty_ssh_basic() { + # Fix TERM compatibility and propagate key environment variables + if [[ "$TERM" == "xterm-ghostty" ]]; then + TERM=xterm-256color \ + GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \ + GHOSTTY_RESOURCES_DIR="${GHOSTTY_RESOURCES_DIR}" \ + builtin command ssh "$@" + else + GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \ + GHOSTTY_RESOURCES_DIR="${GHOSTTY_RESOURCES_DIR}" \ + builtin command ssh "$@" + fi + } + + # Level: full - All features + _ghostty_ssh_full() { + # Full integration: Two-step terminfo installation + if command -v infocmp >/dev/null 2>&1; then + echo "Installing Ghostty terminfo on remote host..." >&2 + + # Step 1: Install terminfo using the same approach that works manually + # This requires authentication but is quick and reliable + if infocmp -x xterm-ghostty 2>/dev/null | command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then + echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + + # Step 2: Connect with xterm-ghostty since we know terminfo is now available + local env_vars=() + + # Use xterm-ghostty since we just installed it + env_vars+=("TERM=xterm-ghostty") + + # Propagate Ghostty shell integration environment variables + [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") + [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO") + [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE") + + # Normal SSH connection with Ghostty terminfo available + env "${env_vars[@]}" ssh "$@" + return 0 + else + echo "Terminfo installation failed. Using basic integration." >&2 + fi + fi + + # Fallback to basic integration + _ghostty_ssh_basic "$@" + } + + fi # Some zsh users manually run `source ~/.zshrc` in order to apply rc file # changes to the current shell. This is a terrible practice that breaks many diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index aed7cefb6..9b4707db5 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -733,6 +733,7 @@ pub const Config = struct { env_override: configpkg.RepeatableStringMap = .{}, shell_integration: configpkg.Config.ShellIntegration = .detect, shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{}, + ssh_integration: configpkg.SSHIntegration, working_directory: ?[]const u8 = null, resources_dir: ?[]const u8, term: []const u8, @@ -937,6 +938,7 @@ const Subprocess = struct { &env, force, cfg.shell_integration_features, + cfg.ssh_integration, ) orelse { log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); break :shell default_shell_command; diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index fb62327d3..fc3fbb63c 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -45,6 +45,7 @@ pub fn setup( env: *EnvMap, force_shell: ?Shell, features: config.ShellIntegrationFeatures, + ssh_integration: config.SSHIntegration, ) !?ShellIntegration { const exe = if (force_shell) |shell| switch (shell) { .bash => "bash", @@ -70,8 +71,9 @@ pub fn setup( exe, ); - // Setup our feature env vars + // Setup our feature env vars and SSH integration try setupFeatures(env, features); + try setupSSHIntegration(env, ssh_integration); return result; } @@ -159,6 +161,7 @@ test "force shell" { &env, shell, .{}, + .off, ); try testing.expectEqual(shell, result.?.shell); } @@ -224,6 +227,15 @@ test "setup features" { } } +pub fn setupSSHIntegration( + env: *EnvMap, + ssh_integration: config.SSHIntegration, +) !void { + if (ssh_integration != .off) { + try env.put("GHOSTTY_SSH_INTEGRATION", @tagName(ssh_integration)); + } +} + /// Setup the bash automatic shell integration. This works by /// starting bash in POSIX mode and using the ENV environment /// variable to load our bash integration script. This prevents From 8f93d8fe030a3c14ec8ab358a974f70beb0995c4 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Fri, 13 Jun 2025 17:03:20 -0700 Subject: [PATCH 02/86] fix: use kebab-case for ssh-integration enum values --- src/config/Config.zig | 4 ++-- src/shell-integration/bash/ghostty.bash | 8 ++++---- .../elvish/lib/ghostty-integration.elv | 4 ++-- .../ghostty-shell-integration.fish | 20 +++++++++---------- src/shell-integration/zsh/ghostty-integration | 8 ++++---- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 5fe950576..7f5e35d0d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6114,7 +6114,7 @@ pub const ShellIntegrationFeatures = packed struct { /// /// * `off` - No SSH integration, use standard ssh command /// -/// * `term_only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color) +/// * `term-only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color) /// /// * `basic` - TERM fix + environment variable propagation /// @@ -6123,7 +6123,7 @@ pub const ShellIntegrationFeatures = packed struct { /// The default value is `off`. pub const SSHIntegration = enum { off, - term_only, + @"term-only", basic, full, diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index f542261d4..349f65f1f 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -111,8 +111,8 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; t # will take precedence over this function, and it won't be wrapped. function ssh { case "$GHOSTTY_SSH_INTEGRATION" in - "term_only") - _ghostty_ssh_term_only "$@" + "term-only") + _ghostty_ssh_term-only "$@" ;; "basic") _ghostty_ssh_basic "$@" @@ -127,8 +127,8 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; t esac } - # Level: term_only - Just fix TERM compatibility - _ghostty_ssh_term_only() { + # Level: term-only - Just fix TERM compatibility + _ghostty_ssh_term-only() { if [[ "$TERM" == "xterm-ghostty" ]]; then TERM=xterm-256color command ssh "$@" else diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 32f9ecbb6..257cd7ba6 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -100,7 +100,7 @@ fn ssh-with-ghostty-integration {|@args| if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION)) { - if (eq "term_only" $E:GHOSTTY_SSH_INTEGRATION) { + if (eq "term-only" $E:GHOSTTY_SSH_INTEGRATION) { ssh-term-only $@args } elif (eq "basic" $E:GHOSTTY_SSH_INTEGRATION) { ssh-basic $@args @@ -116,7 +116,7 @@ } fn ssh-term-only {|@args| - # Level: term_only - Just fix TERM compatibility + # Level: term-only - Just fix TERM compatibility if (eq "xterm-ghostty" $E:TERM) { TERM=xterm-256color (external ssh) $@args } else { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index d03c98c7f..f899764de 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -90,8 +90,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if test -n "$GHOSTTY_SSH_INTEGRATION"; and test "$GHOSTTY_SSH_INTEGRATION" != off function ssh -d "Wrap ssh to provide Ghostty SSH integration" switch "$GHOSTTY_SSH_INTEGRATION" - case term_only - _ghostty_ssh_term_only $argv + case term-only + _ghostty_ssh_term-only $argv case basic _ghostty_ssh_basic $argv case full @@ -102,8 +102,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # Level: term_only - Just fix TERM compatibility - function _ghostty_ssh_term_only -d "SSH with TERM compatibility fix" + # Level: term-only - Just fix TERM compatibility + function _ghostty_ssh_term-only -d "SSH with TERM compatibility fix" if test "$TERM" = xterm-ghostty TERM=xterm-256color command ssh $argv else @@ -143,18 +143,18 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Full integration: Two-step terminfo installation if command -v infocmp >/dev/null 2>&1 echo "Installing Ghostty terminfo on remote host..." >&2 - + # Step 1: Install terminfo using the same approach that works manually # This requires authentication but is quick and reliable if infocmp -x xterm-ghostty 2>/dev/null | command ssh $argv 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null' echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - + # Step 2: Connect with xterm-ghostty since we know terminfo is now available set -l env_vars - + # Use xterm-ghostty since we just installed it set -a env_vars TERM=xterm-ghostty - + # Propagate Ghostty shell integration environment variables if set -q GHOSTTY_SHELL_INTEGRATION_NO_CURSOR set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR @@ -165,7 +165,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if set -q GHOSTTY_SHELL_INTEGRATION_NO_TITLE set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE end - + # Normal SSH connection with Ghostty terminfo available env $env_vars ssh $argv return 0 @@ -173,7 +173,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" echo "Terminfo installation failed. Using basic integration." >&2 end end - + # Fallback to basic integration _ghostty_ssh_basic $argv end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 58838d1f0..903b06dac 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -249,8 +249,8 @@ _ghostty_deferred_init() { # Wrap `ssh` command to provide Ghostty SSH integration ssh() { case "$GHOSTTY_SSH_INTEGRATION" in - "term_only") - _ghostty_ssh_term_only "$@" + "term-only") + _ghostty_ssh_term-only "$@" ;; "basic") _ghostty_ssh_basic "$@" @@ -265,8 +265,8 @@ _ghostty_deferred_init() { esac } - # Level: term_only - Just fix TERM compatibility - _ghostty_ssh_term_only() { + # Level: term-only - Just fix TERM compatibility + _ghostty_ssh_term-only() { if [[ "$TERM" == "xterm-ghostty" ]]; then TERM=xterm-256color builtin command ssh "$@" else From 34af3ffbaf56edef6b5b8b4d1d268f96f44135a5 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Fri, 13 Jun 2025 17:08:44 -0700 Subject: [PATCH 03/86] docs: inline ssh-integration documentation instead of referencing enum --- src/config/Config.zig | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 7f5e35d0d..edf743cce 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1975,11 +1975,18 @@ keybind: Keybinds = .{}, /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` @"shell-integration-features": ShellIntegrationFeatures = .{}, -/// SSH integration level. This controls what level of SSH integration -/// is performed when using the ssh wrapper provided by shell integration. -/// Requires shell integration to be enabled to function. +/// SSH integration levels for shell integration. +/// Controls how much SSH integration is performed when connecting to remote hosts. /// -/// See SSHIntegration for available options. +/// Allowable values are: +/// +/// * `off` - No SSH integration, use standard ssh command +/// +/// * `term-only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color) +/// +/// * `basic` - TERM fix + environment variable propagation +/// +/// * `full` - All features: TERM fix + env vars + terminfo installation /// /// The default value is `off`. @"ssh-integration": SSHIntegration = .off, From 2babdb458fa936908b0bfe1e288aaff5e9892ce5 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 09:53:22 -0700 Subject: [PATCH 04/86] refactor: simplify ssh integration environment variable checks --- src/shell-integration/bash/ghostty.bash | 2 +- src/shell-integration/elvish/lib/ghostty-integration.elv | 4 ++-- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 2 +- src/shell-integration/zsh/ghostty-integration | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 349f65f1f..6915a3f13 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -103,7 +103,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then } fi -if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; then +if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Wrap `ssh` command to provide Ghostty SSH integration. # # This approach supports wrapping an `ssh` alias, but the alias definition diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 257cd7ba6..5c901cbb3 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -99,7 +99,7 @@ } fn ssh-with-ghostty-integration {|@args| - if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION)) { + if (has-env GHOSTTY_SSH_INTEGRATION) { if (eq "term-only" $E:GHOSTTY_SSH_INTEGRATION) { ssh-term-only $@args } elif (eq "basic" $E:GHOSTTY_SSH_INTEGRATION) { @@ -195,7 +195,7 @@ } # Register SSH integration if enabled - if (and (has-env GHOSTTY_SSH_INTEGRATION) (not-eq "" $E:GHOSTTY_SSH_INTEGRATION) (not-eq "off" $E:GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { + if (and (has-env GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { edit:add-var ssh~ $ssh-with-ghostty-integration~ } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index f899764de..482b03cf9 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -87,7 +87,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # SSH integration wrapper - if test -n "$GHOSTTY_SSH_INTEGRATION"; and test "$GHOSTTY_SSH_INTEGRATION" != off + if test -n "$GHOSTTY_SSH_INTEGRATION" function ssh -d "Wrap ssh to provide Ghostty SSH integration" switch "$GHOSTTY_SSH_INTEGRATION" case term-only diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 903b06dac..6bfc6ffd2 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -245,7 +245,7 @@ _ghostty_deferred_init() { fi # SSH - if [[ -n "$GHOSTTY_SSH_INTEGRATION" && "$GHOSTTY_SSH_INTEGRATION" != "off" ]]; then + if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Wrap `ssh` command to provide Ghostty SSH integration ssh() { case "$GHOSTTY_SSH_INTEGRATION" in From 842ced9212e4e5d678e78e8a4c036cf9b4c8c68a Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 13:16:35 -0700 Subject: [PATCH 05/86] bash: preserve mixed indentation in SSH integration changes Preserves existing mixed indentation in ghostty.bash to minimize diff noise per maintainer feedback. --- src/shell-integration/bash/ghostty.bash | 98 ++++++++++++------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 6915a3f13..5c8fd959f 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -103,6 +103,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then } fi +# SSH if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Wrap `ssh` command to provide Ghostty SSH integration. # @@ -190,7 +191,6 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Fallback to basic integration _ghostty_ssh_basic "$@" } - fi # Import bash-preexec, safe to do multiple times @@ -202,68 +202,68 @@ _ghostty_executing="" _ghostty_last_reported_cwd="" function __ghostty_precmd() { - local ret="$?" - if test "$_ghostty_executing" != "0"; then - _GHOSTTY_SAVE_PS0="$PS0" - _GHOSTTY_SAVE_PS1="$PS1" - _GHOSTTY_SAVE_PS2="$PS2" + local ret="$?" + if test "$_ghostty_executing" != "0"; then + _GHOSTTY_SAVE_PS0="$PS0" + _GHOSTTY_SAVE_PS1="$PS1" + _GHOSTTY_SAVE_PS2="$PS2" - # Marks - PS1=$PS1'\[\e]133;B\a\]' - PS2=$PS2'\[\e]133;B\a\]' + # Marks + PS1=$PS1'\[\e]133;B\a\]' + PS2=$PS2'\[\e]133;B\a\]' - # bash doesn't redraw the leading lines in a multiline prompt so - # mark the last line as a secondary prompt (k=s) to prevent the - # preceding lines from being erased by ghostty after a resize. - if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then - PS1=$PS1'\[\e]133;A;k=s\a\]' + # bash doesn't redraw the leading lines in a multiline prompt so + # mark the last line as a secondary prompt (k=s) to prevent the + # preceding lines from being erased by ghostty after a resize. + if [[ "${PS1}" == *"\n"* || "${PS1}" == *$'\n'* ]]; then + PS1=$PS1'\[\e]133;A;k=s\a\]' + fi + + # Cursor + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then + PS1=$PS1'\[\e[5 q\]' + PS0=$PS0'\[\e[0 q\]' + fi + + # Title (working directory) + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then + PS1=$PS1'\[\e]2;\w\a\]' + fi fi - # Cursor - if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then - PS1=$PS1'\[\e[5 q\]' - PS0=$PS0'\[\e[0 q\]' + if test "$_ghostty_executing" != ""; then + # End of current command. Report its status. + builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" fi - # Title (working directory) - if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then - PS1=$PS1'\[\e]2;\w\a\]' + # unfortunately bash provides no hooks to detect cwd changes + # in particular this means cwd reporting will not happen for a + # command like cd /test && cat. PS0 is evaluated before cd is run. + if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then + _ghostty_last_reported_cwd="$PWD" + builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD" fi - fi - if test "$_ghostty_executing" != ""; then - # End of current command. Report its status. - builtin printf "\e]133;D;%s;aid=%s\a" "$ret" "$BASHPID" - fi - - # unfortunately bash provides no hooks to detect cwd changes - # in particular this means cwd reporting will not happen for a - # command like cd /test && cat. PS0 is evaluated before cd is run. - if [[ "$_ghostty_last_reported_cwd" != "$PWD" ]]; then - _ghostty_last_reported_cwd="$PWD" - builtin printf "\e]7;kitty-shell-cwd://%s%s\a" "$HOSTNAME" "$PWD" - fi - - # Fresh line and start of prompt. - builtin printf "\e]133;A;aid=%s\a" "$BASHPID" - _ghostty_executing=0 + # Fresh line and start of prompt. + builtin printf "\e]133;A;aid=%s\a" "$BASHPID" + _ghostty_executing=0 } function __ghostty_preexec() { - builtin local cmd="$1" + builtin local cmd="$1" - PS0="$_GHOSTTY_SAVE_PS0" - PS1="$_GHOSTTY_SAVE_PS1" - PS2="$_GHOSTTY_SAVE_PS2" + PS0="$_GHOSTTY_SAVE_PS0" + PS1="$_GHOSTTY_SAVE_PS1" + PS2="$_GHOSTTY_SAVE_PS2" - # Title (current command) - if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then - builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]/}" - fi + # Title (current command) + if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then + builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}" + fi - # End of input, start of output. - builtin printf "\e]133;C;\a" - _ghostty_executing=1 + # End of input, start of output. + builtin printf "\e]133;C;\a" + _ghostty_executing=1 } preexec_functions+=(__ghostty_preexec) From c70643404cf2da95036700a16da73c57a75006ab Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 13:46:13 -0700 Subject: [PATCH 06/86] bash: revert all formatting changes Keeps only functional additions for SSH integration wrapper, preserving original line breaks and indentation to minimize diff noise per maintainer feedback. --- src/shell-integration/bash/ghostty.bash | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 5c8fd959f..a630b2dec 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -17,7 +17,7 @@ # We need to be in interactive mode and we need to have the Ghostty # resources dir set which also tells us we're running in Ghostty. -if [[ "$-" != *i* ]]; then builtin return; fi +if [[ "$-" != *i* ]] ; then builtin return; fi if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi # When automatic shell integration is active, we were started in POSIX @@ -45,10 +45,7 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then if [[ $__ghostty_bash_flags != *"--noprofile"* ]]; then [ -r /etc/profile ] && builtin source "/etc/profile" for __ghostty_rcfile in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do - [ -r "$__ghostty_rcfile" ] && { - builtin source "$__ghostty_rcfile" - break - } + [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; } done fi else @@ -60,10 +57,7 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then # Void Linux uses /etc/bash/bashrc # Nixos uses /etc/bashrc for __ghostty_rcfile in /etc/bash.bashrc /etc/bash/bashrc /etc/bashrc; do - [ -r "$__ghostty_rcfile" ] && { - builtin source "$__ghostty_rcfile" - break - } + [ -r "$__ghostty_rcfile" ] && { builtin source "$__ghostty_rcfile"; break; } done if [[ -z "$GHOSTTY_BASH_RCFILE" ]]; then GHOSTTY_BASH_RCFILE="$HOME/.bashrc"; fi [ -r "$GHOSTTY_BASH_RCFILE" ] && builtin source "$GHOSTTY_BASH_RCFILE" @@ -96,9 +90,9 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then fi done if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then - builtin command sudo "$@" + builtin command sudo "$@"; else - builtin command sudo TERMINFO="$TERMINFO" "$@" + builtin command sudo TERMINFO="$TERMINFO" "$@"; fi } fi From b07b3e4608cbbdd5a9134e2cbec8c69d1612501c Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 13:51:13 -0700 Subject: [PATCH 07/86] fish: revert all formatting changes Keeps only functional additions for SSH integration wrapper, preserving original line breaks and indentation to minimize diff noise per maintainer feedback. --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 482b03cf9..1f4f9832d 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -63,14 +63,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias - if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x") + if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") # Wrap `sudo` command to ensure Ghostty terminfo is preserved function sudo -d "Wrap sudo to preserve terminfo" - set --function sudo_has_sudoedit_flags no + set --function sudo_has_sudoedit_flags "no" for arg in $argv # Check if argument is '-e' or '--edit' (sudoedit flags) - if string match -q -- -e "$arg"; or string match -q -- --edit "$arg" - set --function sudo_has_sudoedit_flags yes + if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg" + set --function sudo_has_sudoedit_flags "yes" break end # Check if argument is neither an option nor a key-value pair @@ -78,7 +78,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" break end end - if test "$sudo_has_sudoedit_flags" = yes + if test "$sudo_has_sudoedit_flags" = "yes" command sudo $argv else command sudo TERMINFO="$TERMINFO" $argv From 2e9a0e92db644957a95ff5624262d9ed6e360495 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 15:58:48 -0700 Subject: [PATCH 08/86] fix: clean up SSH environment variable propagation --- src/shell-integration/bash/ghostty.bash | 4 +--- .../elvish/lib/ghostty-integration.elv | 12 ++++-------- .../vendor_conf.d/ghostty-shell-integration.fish | 14 ++------------ src/shell-integration/zsh/ghostty-integration | 6 +----- 4 files changed, 8 insertions(+), 28 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index a630b2dec..9200509da 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -170,9 +170,7 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then env_vars+=("TERM=xterm-ghostty") # Propagate Ghostty shell integration environment variables - [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") - [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO") - [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE") + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") # Normal SSH connection with Ghostty terminfo available env "${env_vars[@]}" ssh "$@" diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 5c901cbb3..63c1253bc 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -172,14 +172,10 @@ set env-vars = [$@env-vars TERM=xterm-ghostty] # Propagate Ghostty shell integration environment variables - if (has-env GHOSTTY_SHELL_INTEGRATION_NO_CURSOR) { - set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR] - } - if (has-env GHOSTTY_SHELL_INTEGRATION_NO_SUDO) { - set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO] - } - if (has-env GHOSTTY_SHELL_INTEGRATION_NO_TITLE) { - set env-vars = [$@env-vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE] + if (has-env GHOSTTY_SHELL_FEATURES) { + if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { + set env-vars = [$@env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES] + } } # Normal SSH connection with Ghostty terminfo available diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 1f4f9832d..e0644b8c6 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -126,10 +126,6 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set env_vars $env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" end - if test -n "$GHOSTTY_RESOURCES_DIR" - set env_vars $env_vars GHOSTTY_RESOURCES_DIR="$GHOSTTY_RESOURCES_DIR" - end - # Execute with environment variables if any were set if test (count $env_vars) -gt 0 env $env_vars ssh $argv @@ -156,14 +152,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set -a env_vars TERM=xterm-ghostty # Propagate Ghostty shell integration environment variables - if set -q GHOSTTY_SHELL_INTEGRATION_NO_CURSOR - set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR - end - if set -q GHOSTTY_SHELL_INTEGRATION_NO_SUDO - set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO - end - if set -q GHOSTTY_SHELL_INTEGRATION_NO_TITLE - set -a env_vars GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE + if test -n "$GHOSTTY_SHELL_FEATURES" + set env_vars $env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" end # Normal SSH connection with Ghostty terminfo available diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 6bfc6ffd2..0ffb283b8 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -280,11 +280,9 @@ _ghostty_deferred_init() { if [[ "$TERM" == "xterm-ghostty" ]]; then TERM=xterm-256color \ GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \ - GHOSTTY_RESOURCES_DIR="${GHOSTTY_RESOURCES_DIR}" \ builtin command ssh "$@" else GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \ - GHOSTTY_RESOURCES_DIR="${GHOSTTY_RESOURCES_DIR}" \ builtin command ssh "$@" fi } @@ -307,9 +305,7 @@ _ghostty_deferred_init() { env_vars+=("TERM=xterm-ghostty") # Propagate Ghostty shell integration environment variables - [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR=$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") - [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_SUDO=$GHOSTTY_SHELL_INTEGRATION_NO_SUDO") - [[ -n "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" ]] && env_vars+=("GHOSTTY_SHELL_INTEGRATION_NO_TITLE=$GHOSTTY_SHELL_INTEGRATION_NO_TITLE") + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") # Normal SSH connection with Ghostty terminfo available env "${env_vars[@]}" ssh "$@" From 050cb3bfecbb2dc6c946b39ab670c560b2de3e0b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 15:59:27 -0700 Subject: [PATCH 09/86] fix: remove unnecessary jsonStringify method --- src/config/Config.zig | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index edf743cce..6ca7470d0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -6133,14 +6133,6 @@ pub const SSHIntegration = enum { @"term-only", basic, full, - - pub fn jsonStringify( - self: SSHIntegration, - options: std.json.StringifyOptions, - writer: anytype, - ) !void { - try std.json.stringify(@tagName(self), options, writer); - } }; /// OSC 4, 10, 11, and 12 default color reporting format. From 4206ab121053870c424b69107e4f46b2866c1a3b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 16:04:37 -0700 Subject: [PATCH 10/86] fix: use idiomatic Fish shell syntax in SSH integration - Use `set --append` for array operations - Use `type -q` for command existence checks --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index e0644b8c6..4ceb5324a 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -118,12 +118,12 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Fix TERM compatibility if test "$TERM" = xterm-ghostty - set env_vars $env_vars TERM=xterm-256color + set --append env_vars TERM=xterm-256color end # Propagate Ghostty shell integration environment variables if test -n "$GHOSTTY_SHELL_FEATURES" - set env_vars $env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" + set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" end # Execute with environment variables if any were set @@ -137,7 +137,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Level: full - All features function _ghostty_ssh_full # Full integration: Two-step terminfo installation - if command -v infocmp >/dev/null 2>&1 + if type -q infocmp echo "Installing Ghostty terminfo on remote host..." >&2 # Step 1: Install terminfo using the same approach that works manually @@ -153,7 +153,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Propagate Ghostty shell integration environment variables if test -n "$GHOSTTY_SHELL_FEATURES" - set env_vars $env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" + set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" end # Normal SSH connection with Ghostty terminfo available From 8fafd5ace11411a9a54e0dd13cb0085430a75e1c Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 16:19:14 -0700 Subject: [PATCH 11/86] docs: expand SSH integration configuration documentation Add detailed explanations of shell function behavior, TERM compatibility trade-offs, environment variable propagation, and authentication requirements per maintainer feedback. --- src/config/Config.zig | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 6ca7470d0..589347c8b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1975,18 +1975,32 @@ keybind: Keybinds = .{}, /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` @"shell-integration-features": ShellIntegrationFeatures = .{}, -/// SSH integration levels for shell integration. -/// Controls how much SSH integration is performed when connecting to remote hosts. +/// SSH integration configuration for shell integration. +/// +/// When enabled (any value other than `off`), Ghostty replaces the `ssh` command +/// with a shell function to provide enhanced terminal compatibility and feature +/// propagation when connecting to remote hosts. Users can verify this by running +/// `type ssh` which will show "ssh is a shell function" instead of the binary path. /// /// Allowable values are: /// -/// * `off` - No SSH integration, use standard ssh command +/// * `off` - No SSH integration, use standard ssh binary /// -/// * `term-only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color) +/// * `term-only` - Automatically converts TERM from `xterm-ghostty` to `xterm-256color` +/// when connecting to remote hosts. This prevents "unknown terminal type" errors +/// on systems that lack Ghostty's terminfo entry, but sacrifices Ghostty-specific +/// terminal features like enhanced cursor reporting and shell integration markers. +/// See: https://ghostty.org/docs/help/terminfo /// -/// * `basic` - TERM fix + environment variable propagation +/// * `basic` - TERM compatibility fix plus environment variable propagation. +/// Forwards `GHOSTTY_SHELL_FEATURES` to enable shell integration features on +/// remote systems that have Ghostty installed and configured. /// -/// * `full` - All features: TERM fix + env vars + terminfo installation +/// * `full` - All basic features plus automatic terminfo installation. Attempts +/// to install Ghostty's terminfo entry on the remote host using `infocmp` and `tic`, +/// then connects with full `xterm-ghostty` support. Requires two SSH authentications +/// (one for installation, one for the session) but enables complete Ghostty +/// terminal functionality on the remote system. /// /// The default value is `off`. @"ssh-integration": SSHIntegration = .off, @@ -6114,20 +6128,7 @@ pub const ShellIntegrationFeatures = packed struct { title: bool = true, }; -/// SSH integration levels for shell integration. -/// Controls how much SSH integration is performed when connecting to remote hosts. -/// -/// Allowable values are: -/// -/// * `off` - No SSH integration, use standard ssh command -/// -/// * `term-only` - Only fix TERM compatibility (xterm-ghostty -> xterm-256color) -/// -/// * `basic` - TERM fix + environment variable propagation -/// -/// * `full` - All features: TERM fix + env vars + terminfo installation + shell integration injection -/// -/// The default value is `off`. +/// See ssh-integration pub const SSHIntegration = enum { off, @"term-only", @@ -7678,3 +7679,4 @@ test "theme specifying light/dark sets theme usage in conditional state" { try testing.expect(cfg._conditional_set.contains(.theme)); } } + From af28763a34ae8976b0b294d7c3c7a9cb79a8be14 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 16 Jun 2025 17:11:09 -0700 Subject: [PATCH 12/86] fix: trailing newline in Config.zig --- src/config/Config.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 589347c8b..4cc87ae04 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -7679,4 +7679,3 @@ test "theme specifying light/dark sets theme usage in conditional state" { try testing.expect(cfg._conditional_set.contains(.theme)); } } - From 80475e1d17fdc4e7368899a183c891548c021ece Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 14:52:37 -0700 Subject: [PATCH 13/86] fix: critical elvish syntax errors for environment variables --- src/shell-integration/elvish/lib/ghostty-integration.elv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 63c1253bc..ef34d97b8 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -118,7 +118,7 @@ fn ssh-term-only {|@args| # Level: term-only - Just fix TERM compatibility if (eq "xterm-ghostty" $E:TERM) { - TERM=xterm-256color (external ssh) $@args + (external env) TERM=xterm-256color ssh $@args } else { (external ssh) $@args } @@ -179,7 +179,7 @@ } # Normal SSH connection with Ghostty terminfo available - env $@env-vars ssh $@args + (external env) $@env-vars ssh $@args return } catch e { echo "Terminfo installation failed. Using basic integration." >&2 From fb8f6c77dd49d57ec22e9119ed08ea7e4f974a1c Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 15:26:00 -0700 Subject: [PATCH 14/86] fix: remove dangling resources_dir var --- src/shell-integration/elvish/lib/ghostty-integration.elv | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index ef34d97b8..7611d6745 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -140,12 +140,6 @@ } } - if (has-env GHOSTTY_RESOURCES_DIR) { - if (not-eq "" $E:GHOSTTY_RESOURCES_DIR) { - set env-vars = [$@env-vars GHOSTTY_RESOURCES_DIR=$E:GHOSTTY_RESOURCES_DIR] - } - } - # Execute with environment variables if any were set if (> (count $env-vars) 0) { (external env) $@env-vars ssh $@args From 3319b2b6eda6804e01e0b2d7a1e13028c3e0197a Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 15:30:11 -0700 Subject: [PATCH 15/86] docs: added full stop for consistency --- src/config/Config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 4cc87ae04..0743dc4cf 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1984,7 +1984,7 @@ keybind: Keybinds = .{}, /// /// Allowable values are: /// -/// * `off` - No SSH integration, use standard ssh binary +/// * `off` - No SSH integration, use standard ssh binary. /// /// * `term-only` - Automatically converts TERM from `xterm-ghostty` to `xterm-256color` /// when connecting to remote hosts. This prevents "unknown terminal type" errors From 2ddcf2fffe8e5359b065f7d3d258fe3af7b23015 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 15:32:15 -0700 Subject: [PATCH 16/86] fix: remove resources_dir var, add builtin prefix for consistency --- src/shell-integration/bash/ghostty.bash | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 9200509da..cab4bc01e 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -142,7 +142,6 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Propagate Ghostty shell integration environment variables [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - [[ -n "$GHOSTTY_RESOURCES_DIR" ]] && env_vars+=("GHOSTTY_RESOURCES_DIR=$GHOSTTY_RESOURCES_DIR") # Execute with environment variables if any were set if [[ ${#env_vars[@]} -gt 0 ]]; then @@ -174,7 +173,7 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Normal SSH connection with Ghostty terminfo available env "${env_vars[@]}" ssh "$@" - return 0 + builtin return 0 else echo "Terminfo installation failed. Using basic integration." >&2 fi From 995fb09813ce75809bbd1beff769fd1bfd6501f8 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 15:48:39 -0700 Subject: [PATCH 17/86] fix: add builtin prefix for safety and consistency --- src/shell-integration/bash/ghostty.bash | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index cab4bc01e..b40bf3816 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -125,9 +125,9 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Level: term-only - Just fix TERM compatibility _ghostty_ssh_term-only() { if [[ "$TERM" == "xterm-ghostty" ]]; then - TERM=xterm-256color command ssh "$@" + TERM=xterm-256color builtin command ssh "$@" else - command ssh "$@" + builtin command ssh "$@" fi } @@ -154,12 +154,12 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Level: full - All features _ghostty_ssh_full() { # Full integration: Two-step terminfo installation - if command -v infocmp >/dev/null 2>&1; then + if builtin command -v infocmp >/dev/null 2>&1; then echo "Installing Ghostty terminfo on remote host..." >&2 # Step 1: Install terminfo using the same approach that works manually # This requires authentication but is quick and reliable - if infocmp -x xterm-ghostty 2>/dev/null | command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then + if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 # Step 2: Connect with xterm-ghostty since we know terminfo is now available From b6bb9abfbcbc02212353283c56cf68491eba9266 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 16:37:37 -0700 Subject: [PATCH 18/86] fix: address comprehensive shell integration code review issues - Fix elvish function name mismatch and use conj for list operations - Simplify terminfo installation command per ghostty docs (tic -x -) - Fix conditional structure to ensure error messages always print - Remove redundant checks and optimize array initialization - Use consistent patterns across bash, fish, elvish, and zsh implementations --- src/shell-integration/bash/ghostty.bash | 13 ++-- .../elvish/lib/ghostty-integration.elv | 70 ++++++++----------- .../ghostty-shell-integration.fish | 35 ++++------ src/shell-integration/zsh/ghostty-integration | 67 +++++++++--------- 4 files changed, 82 insertions(+), 103 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index b40bf3816..2ce1f9503 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -157,16 +157,12 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then if builtin command -v infocmp >/dev/null 2>&1; then echo "Installing Ghostty terminfo on remote host..." >&2 - # Step 1: Install terminfo using the same approach that works manually - # This requires authentication but is quick and reliable - if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then + # Step 1: Install terminfo + if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" 'tic -x - 2>/dev/null'; then echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 # Step 2: Connect with xterm-ghostty since we know terminfo is now available - local env_vars=() - - # Use xterm-ghostty since we just installed it - env_vars+=("TERM=xterm-ghostty") + local env_vars=("TERM=xterm-ghostty") # Propagate Ghostty shell integration environment variables [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") @@ -174,9 +170,8 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Normal SSH connection with Ghostty terminfo available env "${env_vars[@]}" ssh "$@" builtin return 0 - else - echo "Terminfo installation failed. Using basic integration." >&2 fi + echo "Terminfo installation failed. Using basic integration." >&2 fi # Fallback to basic integration diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 7611d6745..2f938df97 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -130,14 +130,12 @@ # Fix TERM compatibility if (eq "xterm-ghostty" $E:TERM) { - set env-vars = [$@env-vars TERM=xterm-256color] + set env-vars = (conj $env-vars TERM=xterm-256color) } # Propagate Ghostty shell integration environment variables - if (has-env GHOSTTY_SHELL_FEATURES) { - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = [$@env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES] - } + if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { + set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) } # Execute with environment variables if any were set @@ -148,47 +146,39 @@ } } - fn ghostty-ssh-full {|@args| - # Full integration: Two-step terminfo installation - if (has-external infocmp) { - echo "Installing Ghostty terminfo on remote host..." >&2 - - # Step 1: Install terminfo using the same approach that works manually - # This requires authentication but is quick and reliable - try { - infocmp -x xterm-ghostty 2>/dev/null | command ssh $@args 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null' - echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - - # Step 2: Connect with xterm-ghostty since we know terminfo is now available - var env-vars = [] - - # Use xterm-ghostty since we just installed it - set env-vars = [$@env-vars TERM=xterm-ghostty] - - # Propagate Ghostty shell integration environment variables - if (has-env GHOSTTY_SHELL_FEATURES) { - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = [$@env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES] - } - } - - # Normal SSH connection with Ghostty terminfo available - (external env) $@env-vars ssh $@args - return - } catch e { - echo "Terminfo installation failed. Using basic integration." >&2 - } - } + fn ssh-full {|@args| + # Full integration: Two-step terminfo installation + if (has-external infocmp) { + echo "Installing Ghostty terminfo on remote host..." >&2 - # Fallback to basic integration - ghostty-ssh-basic $@args + try { + infocmp -x xterm-ghostty 2>/dev/null | (external ssh) $@args 'tic -x - 2>/dev/null' + echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + + # Step 2: Connect with xterm-ghostty since we know terminfo is now available + var env-vars = [TERM=xterm-ghostty] + + # Propagate Ghostty shell integration environment variables + if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { + set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) + } + + # Normal SSH connection with Ghostty terminfo available + (external env) $@env-vars ssh $@args + return + } catch e { + echo "Terminfo installation failed. Using basic integration." >&2 + } + } + + # Fallback to basic integration + ssh-basic $@args } - # Register SSH integration if enabled +# Register SSH integration if enabled if (and (has-env GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { edit:add-var ssh~ $ssh-with-ghostty-integration~ } - defer { mark-prompt-start report-pwd diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 4ceb5324a..0fe7155a1 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -104,10 +104,10 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Level: term-only - Just fix TERM compatibility function _ghostty_ssh_term-only -d "SSH with TERM compatibility fix" - if test "$TERM" = xterm-ghostty - TERM=xterm-256color command ssh $argv + if test "$TERM" = "xterm-ghostty" + TERM=xterm-256color builtin command ssh $argv else - command ssh $argv + builtin command ssh $argv end end @@ -117,7 +117,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set --local env_vars # Fix TERM compatibility - if test "$TERM" = xterm-ghostty + if test "$TERM" = "xterm-ghostty" set --append env_vars TERM=xterm-256color end @@ -127,10 +127,10 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # Execute with environment variables if any were set - if test (count $env_vars) -gt 0 + if test "$(count $env_vars)" -gt 0 env $env_vars ssh $argv else - command ssh $argv + builtin command ssh $argv end end @@ -140,35 +140,28 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if type -q infocmp echo "Installing Ghostty terminfo on remote host..." >&2 - # Step 1: Install terminfo using the same approach that works manually - # This requires authentication but is quick and reliable - if infocmp -x xterm-ghostty 2>/dev/null | command ssh $argv 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null' + # Step 1: Install terminfo + if infocmp -x xterm-ghostty 2>/dev/null | ssh $argv 'tic -x - 2>/dev/null' echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - + # Step 2: Connect with xterm-ghostty since we know terminfo is now available - set -l env_vars - - # Use xterm-ghostty since we just installed it - set -a env_vars TERM=xterm-ghostty - + set --local env_vars TERM=xterm-ghostty + # Propagate Ghostty shell integration environment variables if test -n "$GHOSTTY_SHELL_FEATURES" set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" end - - # Normal SSH connection with Ghostty terminfo available + env $env_vars ssh $argv - return 0 - else - echo "Terminfo installation failed. Using basic integration." >&2 + builtin return 0 end + echo "Terminfo installation failed. Using basic integration." >&2 end # Fallback to basic integration _ghostty_ssh_basic $argv end end - # Setup prompt marking function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror # If we never got the output end event, then we need to send it now. diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 0ffb283b8..109c9fd60 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -276,49 +276,50 @@ _ghostty_deferred_init() { # Level: basic - TERM fix + environment variable propagation _ghostty_ssh_basic() { - # Fix TERM compatibility and propagate key environment variables + local env_vars=() + + # Fix TERM compatibility if [[ "$TERM" == "xterm-ghostty" ]]; then - TERM=xterm-256color \ - GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \ - builtin command ssh "$@" + env_vars+=("TERM=xterm-256color") + fi + + # Propagate Ghostty shell integration environment variables + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") + + # Execute with environment variables if any were set + if [[ ${#env_vars[@]} -gt 0 ]]; then + env "${env_vars[@]}" ssh "$@" else - GHOSTTY_SHELL_FEATURES="${GHOSTTY_SHELL_FEATURES}" \ builtin command ssh "$@" fi } # Level: full - All features _ghostty_ssh_full() { - # Full integration: Two-step terminfo installation - if command -v infocmp >/dev/null 2>&1; then - echo "Installing Ghostty terminfo on remote host..." >&2 - - # Step 1: Install terminfo using the same approach that works manually - # This requires authentication but is quick and reliable - if infocmp -x xterm-ghostty 2>/dev/null | command ssh "$@" 'mkdir -p ~/.terminfo/x 2>/dev/null && tic -x -o ~/.terminfo /dev/stdin 2>/dev/null'; then - echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - - # Step 2: Connect with xterm-ghostty since we know terminfo is now available - local env_vars=() - - # Use xterm-ghostty since we just installed it - env_vars+=("TERM=xterm-ghostty") - - # Propagate Ghostty shell integration environment variables - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - - # Normal SSH connection with Ghostty terminfo available - env "${env_vars[@]}" ssh "$@" - return 0 - else - echo "Terminfo installation failed. Using basic integration." >&2 - fi - fi + # Full integration: Two-step terminfo installation + if builtin command -v infocmp >/dev/null 2>&1; then + echo "Installing Ghostty terminfo on remote host..." >&2 - # Fallback to basic integration - _ghostty_ssh_basic "$@" + # Step 1: Install terminfo + if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" 'tic -x - 2>/dev/null'; then + echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + + # Step 2: Connect with xterm-ghostty since we know terminfo is now available + local env_vars=("TERM=xterm-ghostty") + + # Propagate Ghostty shell integration environment variables + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") + + # Normal SSH connection with Ghostty terminfo available + env "${env_vars[@]}" ssh "$@" + builtin return 0 + fi + echo "Terminfo installation failed. Using basic integration." >&2 + fi + + # Fallback to basic integration + _ghostty_ssh_basic "$@" } - fi # Some zsh users manually run `source ~/.zshrc` in order to apply rc file From 4cebee5c8e34a26861fea40034cd78abf679d98f Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 17:43:21 -0700 Subject: [PATCH 19/86] fix: add client-side caching to eliminate redundant terminfo installations - Cache known hosts with terminfo in $GHOSTTY_RESOURCES_DIR/terminfo_hosts - Skip installation step for cached hosts (single connection instead of two) - Use secure file permissions (600) and atomic writes - Extract SSH target safely from command arguments - Maintains full functionality while improving user experience on repeated connections --- src/shell-integration/bash/ghostty.bash | 94 ++++++++++++-- .../elvish/lib/ghostty-integration.elv | 120 +++++++++++++++--- .../ghostty-shell-integration.fish | 101 +++++++++++++-- src/shell-integration/zsh/ghostty-integration | 102 +++++++++++++-- 4 files changed, 366 insertions(+), 51 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 2ce1f9503..51ab6c3e5 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -99,6 +99,66 @@ fi # SSH if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then + # Cache file for tracking hosts with terminfo installed + _ghostty_cache_file="${GHOSTTY_RESOURCES_DIR:-$HOME/.config/ghostty}/terminfo_hosts" + + # Extract target host from SSH arguments + _ghostty_get_ssh_target() { + local target="" + local skip_next=false + + for arg in "$@"; do + if [[ "$skip_next" == "true" ]]; then + skip_next=false + continue + fi + + # Skip flags that take arguments + if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then + skip_next=true + continue + fi + + # Skip other flags + if [[ "$arg" =~ ^- ]]; then + continue + fi + + # This should be the target + target="$arg" + break + done + + echo "$target" + } + + # Check if host has terminfo installed + _ghostty_host_has_terminfo() { + local target="$1" + [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null + } + + # Add host to terminfo cache + _ghostty_cache_host() { + local target="$1" + local cache_dir + cache_dir="$(dirname "$_ghostty_cache_file")" + + # Create cache directory if needed + [[ ! -d "$cache_dir" ]] && mkdir -p "$cache_dir" + + # Atomic write to cache file + { + if [[ -f "$_ghostty_cache_file" ]]; then + cat "$_ghostty_cache_file" + fi + echo "$target" + } | sort -u > "$_ghostty_cache_file.tmp" && mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" + + # Secure permissions + chmod 600 "$_ghostty_cache_file" 2>/dev/null + } + # Wrap `ssh` command to provide Ghostty SSH integration. # # This approach supports wrapping an `ssh` alias, but the alias definition @@ -153,21 +213,35 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Level: full - All features _ghostty_ssh_full() { - # Full integration: Two-step terminfo installation + local target + target="$(_ghostty_get_ssh_target "$@")" + + # Check if we already know this host has terminfo + if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then + # Direct connection with xterm-ghostty + local env_vars=("TERM=xterm-ghostty") + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") + env "${env_vars[@]}" ssh "$@" + return 0 + fi + + # Full integration: Install terminfo if needed if builtin command -v infocmp >/dev/null 2>&1; then - echo "Installing Ghostty terminfo on remote host..." >&2 + # Install terminfo only if needed + if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" ' + if ! infocmp xterm-ghostty >/dev/null 2>&1; then + echo "Installing Ghostty terminfo..." >&2 + tic -x - 2>/dev/null + fi + '; then + echo "Connecting with full Ghostty support..." >&2 - # Step 1: Install terminfo - if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" 'tic -x - 2>/dev/null'; then - echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 + # Cache this host for future connections + [[ -n "$target" ]] && _ghostty_cache_host "$target" - # Step 2: Connect with xterm-ghostty since we know terminfo is now available + # Connect with xterm-ghostty since terminfo is available local env_vars=("TERM=xterm-ghostty") - - # Propagate Ghostty shell integration environment variables [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - - # Normal SSH connection with Ghostty terminfo available env "${env_vars[@]}" ssh "$@" builtin return 0 fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 2f938df97..deb258ae7 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -98,6 +98,70 @@ (external sudo) $@args } + # SSH Integration + # Cache file for tracking hosts with terminfo installed + var ghostty-cache-file = (if (has-env GHOSTTY_RESOURCES_DIR) { put $E:GHOSTTY_RESOURCES_DIR"/terminfo_hosts" } else { put $E:HOME"/.config/ghostty/terminfo_hosts" }) + + # Extract target host from SSH arguments + fn ghostty-get-ssh-target {|@args| + var target = "" + var skip-next = $false + + for arg $args { + if (eq $skip-next $true) { + set skip-next = $false + continue + } + + # Skip flags that take arguments + if (re:match '^-[bcDEeFIiJLlmOopQRSWw]$' $arg) { + set skip-next = $true + continue + } + + # Skip other flags + if (re:match '^-' $arg) { + continue + } + + # This should be the target + set target = $arg + break + } + + put $target + } + + # Check if host has terminfo installed + fn ghostty-host-has-terminfo {|target| + and (path:is-regular $ghostty-cache-file) ?(grep -qFx $target $ghostty-cache-file 2>/dev/null) + } + + # Add host to terminfo cache + fn ghostty-cache-host {|target| + var cache-dir = (path:dir $ghostty-cache-file) + + # Create cache directory if needed + if (not (path:is-dir $cache-dir)) { + mkdir -p $cache-dir + } + + # Atomic write to cache file + var temp-file = $ghostty-cache-file".tmp" + + { + if (path:is-regular $ghostty-cache-file) { + cat $ghostty-cache-file + } + echo $target + } | sort -u > $temp-file + + mv $temp-file $ghostty-cache-file + + # Secure permissions + ?chmod 600 $ghostty-cache-file 2>/dev/null + } + fn ssh-with-ghostty-integration {|@args| if (has-env GHOSTTY_SSH_INTEGRATION) { if (eq "term-only" $E:GHOSTTY_SSH_INTEGRATION) { @@ -127,17 +191,17 @@ fn ssh-basic {|@args| # Level: basic - TERM fix + environment variable propagation var env-vars = [] - + # Fix TERM compatibility if (eq "xterm-ghostty" $E:TERM) { set env-vars = (conj $env-vars TERM=xterm-256color) } - + # Propagate Ghostty shell integration environment variables if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) } - + # Execute with environment variables if any were set if (> (count $env-vars) 0) { (external env) $@env-vars ssh $@args @@ -147,22 +211,47 @@ } fn ssh-full {|@args| - # Full integration: Two-step terminfo installation + var target = (ghostty-get-ssh-target $@args) + + # Check if we already know this host has terminfo + if (and (not-eq "" $target) (ghostty-host-has-terminfo $target)) { + # Direct connection with xterm-ghostty + var env-vars = [TERM=xterm-ghostty] + + # Propagate Ghostty shell integration environment variables + if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { + set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) + } + + (external env) $@env-vars ssh $@args + return + } + + # Full integration: Install terminfo if needed if (has-external infocmp) { - echo "Installing Ghostty terminfo on remote host..." >&2 - try { - infocmp -x xterm-ghostty 2>/dev/null | (external ssh) $@args 'tic -x - 2>/dev/null' - echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - - # Step 2: Connect with xterm-ghostty since we know terminfo is now available + # Install terminfo only if needed + infocmp -x xterm-ghostty 2>/dev/null | (external ssh) $@args ' + if ! infocmp xterm-ghostty >/dev/null 2>&1; then + echo "Installing Ghostty terminfo..." >&2 + tic -x - 2>/dev/null + fi + ' + echo "Connecting with full Ghostty support..." >&2 + + # Cache this host for future connections + if (not-eq "" $target) { + ghostty-cache-host $target + } + + # Connect with xterm-ghostty since terminfo is available var env-vars = [TERM=xterm-ghostty] - + # Propagate Ghostty shell integration environment variables if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) } - + # Normal SSH connection with Ghostty terminfo available (external env) $@env-vars ssh $@args return @@ -170,15 +259,16 @@ echo "Terminfo installation failed. Using basic integration." >&2 } } - + # Fallback to basic integration ssh-basic $@args } -# Register SSH integration if enabled + # Register SSH integration if enabled if (and (has-env GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { edit:add-var ssh~ $ssh-with-ghostty-integration~ - } + } + defer { mark-prompt-start report-pwd diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 0fe7155a1..bcf97cb82 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -86,8 +86,68 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # SSH integration wrapper + # SSH integration if test -n "$GHOSTTY_SSH_INTEGRATION" + # Cache file for tracking hosts with terminfo installed + set -l _ghostty_cache_file (string join / (test -n "$GHOSTTY_RESOURCES_DIR"; and echo "$GHOSTTY_RESOURCES_DIR"; or echo "$HOME/.config/ghostty") "terminfo_hosts") + + # Extract target host from SSH arguments + function _ghostty_get_ssh_target + set -l target "" + set -l skip_next false + + for arg in $argv + if test "$skip_next" = "true" + set skip_next false + continue + end + + # Skip flags that take arguments + if string match -qr '^-[bcDEeFIiJLlmOopQRSWw]$' -- "$arg" + set skip_next true + continue + end + + # Skip other flags + if string match -q -- '-*' "$arg" + continue + end + + # This should be the target + set target "$arg" + break + end + + echo "$target" + end + + # Check if host has terminfo installed + function _ghostty_host_has_terminfo + set -l target $argv[1] + test -f "$_ghostty_cache_file"; and grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null + end + + # Add host to terminfo cache + function _ghostty_cache_host + set -l target $argv[1] + set -l cache_dir (dirname "$_ghostty_cache_file") + + # Create cache directory if needed + test -d "$cache_dir"; or mkdir -p "$cache_dir" + + # Atomic write to cache file + begin + if test -f "$_ghostty_cache_file" + cat "$_ghostty_cache_file" + end + echo "$target" + end | sort -u > "$_ghostty_cache_file.tmp"; and mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" + + # Secure permissions + chmod 600 "$_ghostty_cache_file" 2>/dev/null + end + + # Wrap `ssh` command to provide Ghostty SSH integration. function ssh -d "Wrap ssh to provide Ghostty SSH integration" switch "$GHOSTTY_SSH_INTEGRATION" case term-only @@ -136,22 +196,38 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Level: full - All features function _ghostty_ssh_full - # Full integration: Two-step terminfo installation - if type -q infocmp - echo "Installing Ghostty terminfo on remote host..." >&2 + set -l target (_ghostty_get_ssh_target $argv) - # Step 1: Install terminfo - if infocmp -x xterm-ghostty 2>/dev/null | ssh $argv 'tic -x - 2>/dev/null' - echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - - # Step 2: Connect with xterm-ghostty since we know terminfo is now available + # Check if we already know this host has terminfo + if test -n "$target"; and _ghostty_host_has_terminfo "$target" + # Direct connection with xterm-ghostty + set --local env_vars TERM=xterm-ghostty + if test -n "$GHOSTTY_SHELL_FEATURES" + set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" + end + env $env_vars ssh $argv + return 0 + end + + # Full integration: Install terminfo if needed + if type -q infocmp + # Install terminfo only if needed + if infocmp -x xterm-ghostty 2>/dev/null | ssh $argv ' + if ! infocmp xterm-ghostty >/dev/null 2>&1 + echo "Installing Ghostty terminfo..." >&2 + tic -x - 2>/dev/null + end + ' + echo "Connecting with full Ghostty support..." >&2 + + # Cache this host for future connections + test -n "$target"; and _ghostty_cache_host "$target" + + # Connect with xterm-ghostty since terminfo is available set --local env_vars TERM=xterm-ghostty - - # Propagate Ghostty shell integration environment variables if test -n "$GHOSTTY_SHELL_FEATURES" set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" end - env $env_vars ssh $argv builtin return 0 end @@ -162,6 +238,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" _ghostty_ssh_basic $argv end end + # Setup prompt marking function __ghostty_mark_prompt_start --on-event fish_prompt --on-event fish_cancel --on-event fish_posterror # If we never got the output end event, then we need to send it now. diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 109c9fd60..c172fc02d 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -243,9 +243,69 @@ _ghostty_deferred_init() { fi } fi - + # SSH if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then + # Cache file for tracking hosts with terminfo installed + _ghostty_cache_file="${GHOSTTY_RESOURCES_DIR:-$HOME/.config/ghostty}/terminfo_hosts" + + # Extract target host from SSH arguments + _ghostty_get_ssh_target() { + local target="" + local skip_next=false + + for arg in "$@"; do + if [[ "$skip_next" == "true" ]]; then + skip_next=false + continue + fi + + # Skip flags that take arguments + if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then + skip_next=true + continue + fi + + # Skip other flags + if [[ "$arg" =~ ^- ]]; then + continue + fi + + # This should be the target + target="$arg" + break + done + + echo "$target" + } + + # Check if host has terminfo installed + _ghostty_host_has_terminfo() { + local target="$1" + [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null + } + + # Add host to terminfo cache + _ghostty_cache_host() { + local target="$1" + local cache_dir + cache_dir="$(dirname "$_ghostty_cache_file")" + + # Create cache directory if needed + [[ ! -d "$cache_dir" ]] && mkdir -p "$cache_dir" + + # Atomic write to cache file + { + if [[ -f "$_ghostty_cache_file" ]]; then + cat "$_ghostty_cache_file" + fi + echo "$target" + } | sort -u > "$_ghostty_cache_file.tmp" && mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" + + # Secure permissions + chmod 600 "$_ghostty_cache_file" 2>/dev/null + } + # Wrap `ssh` command to provide Ghostty SSH integration ssh() { case "$GHOSTTY_SSH_INTEGRATION" in @@ -296,27 +356,41 @@ _ghostty_deferred_init() { # Level: full - All features _ghostty_ssh_full() { - # Full integration: Two-step terminfo installation + local target + target="$(_ghostty_get_ssh_target "$@")" + + # Check if we already know this host has terminfo + if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then + # Direct connection with xterm-ghostty + local env_vars=("TERM=xterm-ghostty") + [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") + env "${env_vars[@]}" ssh "$@" + return 0 + fi + + # Full integration: Install terminfo if needed if builtin command -v infocmp >/dev/null 2>&1; then - echo "Installing Ghostty terminfo on remote host..." >&2 - - # Step 1: Install terminfo - if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" 'tic -x - 2>/dev/null'; then - echo "Terminfo installed successfully. Connecting with full Ghostty support..." >&2 - - # Step 2: Connect with xterm-ghostty since we know terminfo is now available + # Install terminfo only if needed + if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" ' + if ! infocmp xterm-ghostty >/dev/null 2>&1; then + echo "Installing Ghostty terminfo..." >&2 + tic -x - 2>/dev/null + fi + '; then + echo "Connecting with full Ghostty support..." >&2 + + # Cache this host for future connections + [[ -n "$target" ]] && _ghostty_cache_host "$target" + + # Connect with xterm-ghostty since terminfo is available local env_vars=("TERM=xterm-ghostty") - - # Propagate Ghostty shell integration environment variables [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - - # Normal SSH connection with Ghostty terminfo available env "${env_vars[@]}" ssh "$@" builtin return 0 fi echo "Terminfo installation failed. Using basic integration." >&2 fi - + # Fallback to basic integration _ghostty_ssh_basic "$@" } From 69f9976394d903b5b5e94ee86e2249331deee104 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 17:56:46 -0700 Subject: [PATCH 20/86] fix: manual formatting pass to ensure consistency with existing patterns --- .../ghostty-shell-integration.fish | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index bcf97cb82..e01eabac3 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -89,22 +89,22 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # SSH integration if test -n "$GHOSTTY_SSH_INTEGRATION" # Cache file for tracking hosts with terminfo installed - set -l _ghostty_cache_file (string join / (test -n "$GHOSTTY_RESOURCES_DIR"; and echo "$GHOSTTY_RESOURCES_DIR"; or echo "$HOME/.config/ghostty") "terminfo_hosts") + set --local _ghostty_cache_file (string join / (test -n "$GHOSTTY_RESOURCES_DIR"; and echo "$GHOSTTY_RESOURCES_DIR"; or echo "$HOME/.config/ghostty") "terminfo_hosts") # Extract target host from SSH arguments function _ghostty_get_ssh_target - set -l target "" - set -l skip_next false + set --local target "" + set --local skip_next "false" for arg in $argv if test "$skip_next" = "true" - set skip_next false + set skip_next "false" continue end # Skip flags that take arguments - if string match -qr '^-[bcDEeFIiJLlmOopQRSWw]$' -- "$arg" - set skip_next true + if string match -qr -- '^-[bcDEeFIiJLlmOopQRSWw]$' "$arg" + set skip_next "true" continue end @@ -123,14 +123,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Check if host has terminfo installed function _ghostty_host_has_terminfo - set -l target $argv[1] + set --local target "$argv[1]" test -f "$_ghostty_cache_file"; and grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null end # Add host to terminfo cache function _ghostty_cache_host - set -l target $argv[1] - set -l cache_dir (dirname "$_ghostty_cache_file") + set --local target "$argv[1]" + set --local cache_dir (dirname "$_ghostty_cache_file") # Create cache directory if needed test -d "$cache_dir"; or mkdir -p "$cache_dir" @@ -147,14 +147,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" chmod 600 "$_ghostty_cache_file" 2>/dev/null end - # Wrap `ssh` command to provide Ghostty SSH integration. + # Wrap `ssh` command to provide Ghostty SSH integration function ssh -d "Wrap ssh to provide Ghostty SSH integration" switch "$GHOSTTY_SSH_INTEGRATION" - case term-only + case "term-only" _ghostty_ssh_term-only $argv - case basic + case "basic" _ghostty_ssh_basic $argv - case full + case "full" _ghostty_ssh_full $argv case "*" # Unknown level, fall back to basic @@ -187,7 +187,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # Execute with environment variables if any were set - if test "$(count $env_vars)" -gt 0 + if test (count $env_vars) -gt 0 env $env_vars ssh $argv else builtin command ssh $argv @@ -196,7 +196,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Level: full - All features function _ghostty_ssh_full - set -l target (_ghostty_get_ssh_target $argv) + set --local target (_ghostty_get_ssh_target $argv) # Check if we already know this host has terminfo if test -n "$target"; and _ghostty_host_has_terminfo "$target" From f206e7684193a11cccd935b25ccea1dbf538a03b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 21:33:29 -0700 Subject: [PATCH 21/86] ssh-integration: improve host caching, new method for "full" integration Need a sanity check on this new approach for "full" to help determine if it's worth additional iteration/refinement. It solves the double auth issue, successfully propagates env vars, and avoids output noise for connections that happen after terminfo is installed. The only issue I don't have time to fix tonight is the fact that it drops the MOTD for cached (re)connections. --- src/shell-integration/bash/ghostty.bash | 282 ++++++++++++++++-------- 1 file changed, 187 insertions(+), 95 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 51ab6c3e5..74f48cf32 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -99,91 +99,90 @@ fi # SSH if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then - # Cache file for tracking hosts with terminfo installed - _ghostty_cache_file="${GHOSTTY_RESOURCES_DIR:-$HOME/.config/ghostty}/terminfo_hosts" + # Cache configuration + _ghostty_cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/ghostty" + _ghostty_cache_file="$_ghostty_cache_dir/terminfo_hosts" - # Extract target host from SSH arguments + # Create cache directory with proper permissions + [[ ! -d "$_ghostty_cache_dir" ]] && mkdir -p "$_ghostty_cache_dir" && chmod 700 "$_ghostty_cache_dir" + + # Extract SSH target from arguments _ghostty_get_ssh_target() { local target="" local skip_next=false + local args=("$@") - for arg in "$@"; do - if [[ "$skip_next" == "true" ]]; then - skip_next=false - continue - fi + for ((i=0; i<${#args[@]}; i++)); do + local arg="${args[i]}" - # Skip flags that take arguments + # Skip if we're processing a flag's argument + [[ "$skip_next" == "true" ]] && { skip_next=false; continue; } + + # Handle flags that take arguments if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then skip_next=true continue fi - # Skip other flags - if [[ "$arg" =~ ^- ]]; then - continue - fi + # Handle combined short flags with values (e.g., -p22) + [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw].+ ]] && continue - # This should be the target + # Skip other flags + [[ "$arg" =~ ^- ]] && continue + + # This should be our target target="$arg" break done - echo "$target" + # Handle user@host format + echo "${target##*@}" } - # Check if host has terminfo installed + # Check if host has terminfo cached _ghostty_host_has_terminfo() { - local target="$1" - [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null + local host="$1" + [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$host" "$_ghostty_cache_file" 2>/dev/null } - # Add host to terminfo cache + # Add host to cache atomically _ghostty_cache_host() { - local target="$1" - local cache_dir - cache_dir="$(dirname "$_ghostty_cache_file")" + local host="$1" + local temp_file + temp_file="$_ghostty_cache_file.$$" - # Create cache directory if needed - [[ ! -d "$cache_dir" ]] && mkdir -p "$cache_dir" - - # Atomic write to cache file + # Merge existing cache with new host { - if [[ -f "$_ghostty_cache_file" ]]; then - cat "$_ghostty_cache_file" - fi - echo "$target" - } | sort -u > "$_ghostty_cache_file.tmp" && mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" + [[ -f "$_ghostty_cache_file" ]] && cat "$_ghostty_cache_file" + echo "$host" + } | sort -u > "$temp_file" - # Secure permissions - chmod 600 "$_ghostty_cache_file" 2>/dev/null + # Atomic replace with proper permissions + mv -f "$temp_file" "$_ghostty_cache_file" && chmod 600 "$_ghostty_cache_file" } - # Wrap `ssh` command to provide Ghostty SSH integration. - # - # This approach supports wrapping an `ssh` alias, but the alias definition - # must come _after_ this function is defined. Otherwise, the alias expansion - # will take precedence over this function, and it won't be wrapped. - function ssh { + # Remove host from cache (for maintenance) + _ghostty_uncache_host() { + local host="$1" + [[ -f "$_ghostty_cache_file" ]] || return 0 + + local temp_file="$_ghostty_cache_file.$$" + grep -vFx "$host" "$_ghostty_cache_file" > "$temp_file" 2>/dev/null || true + mv -f "$temp_file" "$_ghostty_cache_file" + } + + # Main SSH wrapper + ssh() { case "$GHOSTTY_SSH_INTEGRATION" in - "term-only") - _ghostty_ssh_term-only "$@" - ;; - "basic") - _ghostty_ssh_basic "$@" - ;; - "full") - _ghostty_ssh_full "$@" - ;; - *) - # Unknown level, fall back to basic - _ghostty_ssh_basic "$@" - ;; + term-only) _ghostty_ssh_term_only "$@" ;; + basic) _ghostty_ssh_basic "$@" ;; + full) _ghostty_ssh_full "$@" ;; + *) _ghostty_ssh_basic "$@" ;; # Default to basic esac } # Level: term-only - Just fix TERM compatibility - _ghostty_ssh_term-only() { + _ghostty_ssh_term_only() { if [[ "$TERM" == "xterm-ghostty" ]]; then TERM=xterm-256color builtin command ssh "$@" else @@ -191,65 +190,158 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then fi } - # Level: basic - TERM fix + environment variable propagation + # Level: basic - TERM fix + environment propagation _ghostty_ssh_basic() { - local env_vars=() + local term_value + term_value=$([[ "$TERM" == "xterm-ghostty" ]] && echo "xterm-256color" || echo "$TERM") - # Fix TERM compatibility - if [[ "$TERM" == "xterm-ghostty" ]]; then - env_vars+=("TERM=xterm-256color") - fi + builtin command ssh "$@" " + # Set environment for this session + export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' + export TERM='$term_value' - # Propagate Ghostty shell integration environment variables - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - - # Execute with environment variables if any were set - if [[ ${#env_vars[@]} -gt 0 ]]; then - env "${env_vars[@]}" ssh "$@" - else - builtin command ssh "$@" - fi + # Start interactive shell + exec \$SHELL -l + " } - # Level: full - All features + # Level: full - Complete integration with terminfo _ghostty_ssh_full() { local target target="$(_ghostty_get_ssh_target "$@")" - # Check if we already know this host has terminfo + # Quick path for cached hosts if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then - # Direct connection with xterm-ghostty - local env_vars=("TERM=xterm-ghostty") - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - env "${env_vars[@]}" ssh "$@" - return 0 + # Direct connection with full ghostty support + builtin command ssh -t "$@" " + export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' + export TERM='xterm-ghostty' + exec \$SHELL -l + " + return $? fi - # Full integration: Install terminfo if needed - if builtin command -v infocmp >/dev/null 2>&1; then - # Install terminfo only if needed - if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" ' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null - fi - '; then - echo "Connecting with full Ghostty support..." >&2 + # Check if we can export terminfo + if ! builtin command -v infocmp >/dev/null 2>&1; then + echo "Warning: infocmp not found locally. Using basic integration." >&2 + _ghostty_ssh_basic "$@" + return $? + fi - # Cache this host for future connections - [[ -n "$target" ]] && _ghostty_cache_host "$target" + # Generate terminfo data + local terminfo_data + terminfo_data="$(infocmp -x xterm-ghostty 2>/dev/null)" || { + echo "Warning: xterm-ghostty terminfo not found locally. Using basic integration." >&2 + _ghostty_ssh_basic "$@" + return $? + } - # Connect with xterm-ghostty since terminfo is available - local env_vars=("TERM=xterm-ghostty") - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - env "${env_vars[@]}" ssh "$@" - builtin return 0 + echo "Setting up Ghostty terminal support on remote host..." >&2 + + # Create control socket path + local control_path="/tmp/ghostty-ssh-${USER}-$" + trap "rm -f '$control_path'" EXIT + + # Start control master and check/install terminfo + local setup_script=' + if ! infocmp xterm-ghostty >/dev/null 2>&1; then + if command -v tic >/dev/null 2>&1; then + mkdir -p "$HOME/.terminfo" 2>/dev/null + echo "NEEDS_INSTALL" + else + echo "NO_TIC" + fi + else + echo "ALREADY_INSTALLED" fi - echo "Terminfo installation failed. Using basic integration." >&2 + ' + + # First connection: Start control master and check status + local install_status + install_status=$(builtin command ssh -o ControlMaster=yes \ + -o ControlPath="$control_path" \ + -o ControlPersist=30s \ + "$@" "$setup_script") + + case "$install_status" in + "NEEDS_INSTALL") + echo "Installing xterm-ghostty terminfo..." >&2 + # Send terminfo through existing control connection + if echo "$terminfo_data" | builtin command ssh -o ControlPath="$control_path" "$@" \ + 'tic -x - 2>/dev/null && echo "SUCCESS"' | grep -q "SUCCESS"; then + echo "Terminfo installed successfully." >&2 + [[ -n "$target" ]] && _ghostty_cache_host "$target" + else + echo "Warning: Failed to install terminfo. Using basic integration." >&2 + ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true + _ghostty_ssh_basic "$@" + return $? + fi + ;; + "ALREADY_INSTALLED") + [[ -n "$target" ]] && _ghostty_cache_host "$target" + ;; + "NO_TIC") + echo "Warning: tic not found on remote host. Using basic integration." >&2 + ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true + _ghostty_ssh_basic "$@" + return $? + ;; + esac + + # Now use the existing control connection for interactive session + echo "Connecting with full Ghostty support..." >&2 + + # Pass environment through and start login shell to show MOTD + builtin command ssh -t -o ControlPath="$control_path" "$@" " + # Set up Ghostty environment + export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' + export TERM='xterm-ghostty' + + # Display MOTD if this is a fresh connection + if [[ '$install_status' == 'NEEDS_INSTALL' ]]; then + # Try to display MOTD manually + if [[ -f /etc/motd ]]; then + cat /etc/motd 2>/dev/null || true + fi + # Run update-motd if available (Ubuntu/Debian) + if [[ -d /etc/update-motd.d ]]; then + run-parts /etc/update-motd.d 2>/dev/null || true + fi + fi + + # Force a login shell + exec \$SHELL -l + " + + local exit_code=$? + + # Clean up control socket + ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true + + return $exit_code + } + + # Utility function to clear cache for a specific host + ghostty_ssh_reset() { + local host="${1:-}" + if [[ -z "$host" ]]; then + echo "Usage: ghostty_ssh_reset " >&2 + return 1 fi - # Fallback to basic integration - _ghostty_ssh_basic "$@" + _ghostty_uncache_host "$host" + echo "Cleared Ghostty terminfo cache for: $host" + } + + # Utility function to list cached hosts + ghostty_ssh_list_cached() { + if [[ -f "$_ghostty_cache_file" ]]; then + echo "Hosts with cached Ghostty terminfo:" + cat "$_ghostty_cache_file" + else + echo "No hosts cached yet." + fi } fi From 30683979bcb836777ea0166baa75e166b647f850 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 17 Jun 2025 22:07:06 -0700 Subject: [PATCH 22/86] fix: catch up to current state --- src/shell-integration/bash/ghostty.bash | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 74f48cf32..550e8a6c3 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -15,10 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -# We need to be in interactive mode and we need to have the Ghostty -# resources dir set which also tells us we're running in Ghostty. +# We need to be in interactive mode to proceed. if [[ "$-" != *i* ]] ; then builtin return; fi -if [ -z "$GHOSTTY_RESOURCES_DIR" ]; then builtin return; fi # When automatic shell integration is active, we were started in POSIX # mode and need to manually recreate the bash startup sequence. @@ -346,7 +344,7 @@ if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then fi # Import bash-preexec, safe to do multiple times -builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/bash-preexec.sh" +builtin source "$(dirname -- "${BASH_SOURCE[0]}")/bash-preexec.sh" # This is set to 1 when we're executing a command so that we don't # send prompt marks multiple times. From ddd3da487e46e467cb09829b0f41139e3651a29b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 18 Jun 2025 14:59:57 -0700 Subject: [PATCH 23/86] fix: update cache file location --- src/shell-integration/bash/ghostty.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 550e8a6c3..46734525d 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -98,7 +98,7 @@ fi # SSH if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then # Cache configuration - _ghostty_cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/ghostty" + _ghostty_cache_dir="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty" _ghostty_cache_file="$_ghostty_cache_dir/terminfo_hosts" # Create cache directory with proper permissions From e73313ed40bf7da202688709e634c7a0b934c553 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 23 Jun 2025 22:34:32 -0700 Subject: [PATCH 24/86] change: migrate SSH integration from standalone option to shell-integration-features flags - Add ssh_env and ssh_terminfo flags to ShellIntegrationFeatures - Remove SSHIntegration enum and ssh-integration config option - Update setupFeatures to handle new flags via reflection - Remove setupSSHIntegration function and all references Integrates SSH functionality into existing shell-integration-features system for better consistency and user control. --- src/Surface.zig | 1 - src/config.zig | 1 - src/config/Config.zig | 48 +++++++------------------------- src/termio/Exec.zig | 2 -- src/termio/shell_integration.zig | 14 +--------- 5 files changed, 11 insertions(+), 55 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 78363e87c..6005635d9 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -545,7 +545,6 @@ pub fn init( .env_override = config.env, .shell_integration = config.@"shell-integration", .shell_integration_features = config.@"shell-integration-features", - .ssh_integration = config.@"ssh-integration", .working_directory = config.@"working-directory", .resources_dir = global_state.resources_dir.host(), .term = config.term, diff --git a/src/config.zig b/src/config.zig index e34819fa1..7f390fb08 100644 --- a/src/config.zig +++ b/src/config.zig @@ -33,7 +33,6 @@ pub const RepeatableStringMap = @import("config/RepeatableStringMap.zig"); pub const RepeatablePath = Config.RepeatablePath; pub const Path = Config.Path; pub const ShellIntegrationFeatures = Config.ShellIntegrationFeatures; -pub const SSHIntegration = Config.SSHIntegration; pub const WindowPaddingColor = Config.WindowPaddingColor; pub const BackgroundImagePosition = Config.BackgroundImagePosition; pub const BackgroundImageFit = Config.BackgroundImageFit; diff --git a/src/config/Config.zig b/src/config/Config.zig index 0743dc4cf..41171b3be 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1972,39 +1972,17 @@ keybind: Keybinds = .{}, /// /// * `title` - Set the window title via shell integration. /// +/// * `ssh-env` - Enable SSH environment variable compatibility. Automatically +/// converts TERM from `xterm-ghostty` to `xterm-256color` when connecting to +/// remote hosts and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION. +/// +/// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. +/// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when +/// connecting to hosts that lack it. +/// /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` @"shell-integration-features": ShellIntegrationFeatures = .{}, -/// SSH integration configuration for shell integration. -/// -/// When enabled (any value other than `off`), Ghostty replaces the `ssh` command -/// with a shell function to provide enhanced terminal compatibility and feature -/// propagation when connecting to remote hosts. Users can verify this by running -/// `type ssh` which will show "ssh is a shell function" instead of the binary path. -/// -/// Allowable values are: -/// -/// * `off` - No SSH integration, use standard ssh binary. -/// -/// * `term-only` - Automatically converts TERM from `xterm-ghostty` to `xterm-256color` -/// when connecting to remote hosts. This prevents "unknown terminal type" errors -/// on systems that lack Ghostty's terminfo entry, but sacrifices Ghostty-specific -/// terminal features like enhanced cursor reporting and shell integration markers. -/// See: https://ghostty.org/docs/help/terminfo -/// -/// * `basic` - TERM compatibility fix plus environment variable propagation. -/// Forwards `GHOSTTY_SHELL_FEATURES` to enable shell integration features on -/// remote systems that have Ghostty installed and configured. -/// -/// * `full` - All basic features plus automatic terminfo installation. Attempts -/// to install Ghostty's terminfo entry on the remote host using `infocmp` and `tic`, -/// then connects with full `xterm-ghostty` support. Requires two SSH authentications -/// (one for installation, one for the session) but enables complete Ghostty -/// terminal functionality on the remote system. -/// -/// The default value is `off`. -@"ssh-integration": SSHIntegration = .off, - /// Sets the reporting format for OSC sequences that request color information. /// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and /// OSC 4 (256 color palette) queries, and by default the reported values @@ -6126,14 +6104,8 @@ pub const ShellIntegrationFeatures = packed struct { cursor: bool = true, sudo: bool = false, title: bool = true, -}; - -/// See ssh-integration -pub const SSHIntegration = enum { - off, - @"term-only", - basic, - full, + @"ssh-env": bool = false, + @"ssh-terminfo": bool = false, }; /// OSC 4, 10, 11, and 12 default color reporting format. diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 9b4707db5..aed7cefb6 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -733,7 +733,6 @@ pub const Config = struct { env_override: configpkg.RepeatableStringMap = .{}, shell_integration: configpkg.Config.ShellIntegration = .detect, shell_integration_features: configpkg.Config.ShellIntegrationFeatures = .{}, - ssh_integration: configpkg.SSHIntegration, working_directory: ?[]const u8 = null, resources_dir: ?[]const u8, term: []const u8, @@ -938,7 +937,6 @@ const Subprocess = struct { &env, force, cfg.shell_integration_features, - cfg.ssh_integration, ) orelse { log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); break :shell default_shell_command; diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index fc3fbb63c..fb62327d3 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -45,7 +45,6 @@ pub fn setup( env: *EnvMap, force_shell: ?Shell, features: config.ShellIntegrationFeatures, - ssh_integration: config.SSHIntegration, ) !?ShellIntegration { const exe = if (force_shell) |shell| switch (shell) { .bash => "bash", @@ -71,9 +70,8 @@ pub fn setup( exe, ); - // Setup our feature env vars and SSH integration + // Setup our feature env vars try setupFeatures(env, features); - try setupSSHIntegration(env, ssh_integration); return result; } @@ -161,7 +159,6 @@ test "force shell" { &env, shell, .{}, - .off, ); try testing.expectEqual(shell, result.?.shell); } @@ -227,15 +224,6 @@ test "setup features" { } } -pub fn setupSSHIntegration( - env: *EnvMap, - ssh_integration: config.SSHIntegration, -) !void { - if (ssh_integration != .off) { - try env.put("GHOSTTY_SSH_INTEGRATION", @tagName(ssh_integration)); - } -} - /// Setup the bash automatic shell integration. This works by /// starting bash in POSIX mode and using the ENV environment /// variable to load our bash integration script. This prevents From 81641e56b1c471f9e110ebe7ec8372b75f366ed7 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 24 Jun 2025 08:58:00 -0700 Subject: [PATCH 25/86] ssh-integration: replace levels with flags, optimize implementation Rewrote shell functions to support the two new flags for shell-integration-features: - ssh-env: TERM compatibility + best effort environment variable propagation (anything beyond TERM will depend on what the remote host allows) - ssh-terminfo: automatic terminfo installation with control socket orchestration - Flags work independently or combined Implementation optimizations: - ~65% code reduction through unified execution path - Eliminated GHOSTTY_SSH_INTEGRATION environment variable system - Replaced complex function dispatch with direct flag detection - Consolidated 4 cache helper functions into single _ghst_cache() utility - Simplified control socket management (removed multi-step orchestration) - Subsequent connections to cached hosts are now directly executed and more reliable New additions: - If ssh-terminfo is enabled, ghostty will be wrapped to provide users with convenient commands to invoke either of the two utility functions: `ghostty ssh-cache-list` and `ghostty ssh-cache-clear` --- src/shell-integration/bash/ghostty.bash | 315 +++++------------- .../elvish/lib/ghostty-integration.elv | 311 +++++++++-------- .../ghostty-shell-integration.fish | 240 ++++++------- src/shell-integration/zsh/ghostty-integration | 222 +++++------- 4 files changed, 420 insertions(+), 668 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 46734525d..6e09ab193 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -95,252 +95,103 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then } fi -# SSH -if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then - # Cache configuration - _ghostty_cache_dir="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty" - _ghostty_cache_file="$_ghostty_cache_dir/terminfo_hosts" +# SSH Integration +if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + # Only define cache functions and variable if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + _cache="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - # Create cache directory with proper permissions - [[ ! -d "$_ghostty_cache_dir" ]] && mkdir -p "$_ghostty_cache_dir" && chmod 700 "$_ghostty_cache_dir" - - # Extract SSH target from arguments - _ghostty_get_ssh_target() { - local target="" - local skip_next=false - local args=("$@") - - for ((i=0; i<${#args[@]}; i++)); do - local arg="${args[i]}" - - # Skip if we're processing a flag's argument - [[ "$skip_next" == "true" ]] && { skip_next=false; continue; } - - # Handle flags that take arguments - if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then - skip_next=true - continue - fi - - # Handle combined short flags with values (e.g., -p22) - [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw].+ ]] && continue - - # Skip other flags - [[ "$arg" =~ ^- ]] && continue - - # This should be our target - target="$arg" - break - done - - # Handle user@host format - echo "${target##*@}" - } - - # Check if host has terminfo cached - _ghostty_host_has_terminfo() { - local host="$1" - [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$host" "$_ghostty_cache_file" 2>/dev/null - } - - # Add host to cache atomically - _ghostty_cache_host() { - local host="$1" - local temp_file - temp_file="$_ghostty_cache_file.$$" - - # Merge existing cache with new host - { - [[ -f "$_ghostty_cache_file" ]] && cat "$_ghostty_cache_file" - echo "$host" - } | sort -u > "$temp_file" - - # Atomic replace with proper permissions - mv -f "$temp_file" "$_ghostty_cache_file" && chmod 600 "$_ghostty_cache_file" - } - - # Remove host from cache (for maintenance) - _ghostty_uncache_host() { - local host="$1" - [[ -f "$_ghostty_cache_file" ]] || return 0 - - local temp_file="$_ghostty_cache_file.$$" - grep -vFx "$host" "$_ghostty_cache_file" > "$temp_file" 2>/dev/null || true - mv -f "$temp_file" "$_ghostty_cache_file" - } - - # Main SSH wrapper - ssh() { - case "$GHOSTTY_SSH_INTEGRATION" in - term-only) _ghostty_ssh_term_only "$@" ;; - basic) _ghostty_ssh_basic "$@" ;; - full) _ghostty_ssh_full "$@" ;; - *) _ghostty_ssh_basic "$@" ;; # Default to basic - esac - } - - # Level: term-only - Just fix TERM compatibility - _ghostty_ssh_term_only() { - if [[ "$TERM" == "xterm-ghostty" ]]; then - TERM=xterm-256color builtin command ssh "$@" - else - builtin command ssh "$@" - fi - } - - # Level: basic - TERM fix + environment propagation - _ghostty_ssh_basic() { - local term_value - term_value=$([[ "$TERM" == "xterm-ghostty" ]] && echo "xterm-256color" || echo "$TERM") - - builtin command ssh "$@" " - # Set environment for this session - export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' - export TERM='$term_value' - - # Start interactive shell - exec \$SHELL -l - " - } - - # Level: full - Complete integration with terminfo - _ghostty_ssh_full() { - local target - target="$(_ghostty_get_ssh_target "$@")" - - # Quick path for cached hosts - if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then - # Direct connection with full ghostty support - builtin command ssh -t "$@" " - export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' - export TERM='xterm-ghostty' - exec \$SHELL -l - " - return $? - fi - - # Check if we can export terminfo - if ! builtin command -v infocmp >/dev/null 2>&1; then - echo "Warning: infocmp not found locally. Using basic integration." >&2 - _ghostty_ssh_basic "$@" - return $? - fi - - # Generate terminfo data - local terminfo_data - terminfo_data="$(infocmp -x xterm-ghostty 2>/dev/null)" || { - echo "Warning: xterm-ghostty terminfo not found locally. Using basic integration." >&2 - _ghostty_ssh_basic "$@" - return $? + # Cache operations and utilities + _ghst_cache() { + case $2 in + chk) [[ -f $_cache ]] && grep -qFx "$1" "$_cache" 2>/dev/null ;; + add) + mkdir -p "${_cache%/*}" + { + [[ -f $_cache ]] && cat "$_cache" + builtin echo "$1" + } | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" + ;; + esac } - echo "Setting up Ghostty terminal support on remote host..." >&2 + function ghostty_ssh_cache_clear() { + rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." + } - # Create control socket path - local control_path="/tmp/ghostty-ssh-${USER}-$" - trap "rm -f '$control_path'" EXIT + function ghostty_ssh_cache_list() { + [[ -s $_cache ]] && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." + } + fi - # Start control master and check/install terminfo - local setup_script=' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - if command -v tic >/dev/null 2>&1; then - mkdir -p "$HOME/.terminfo" 2>/dev/null - echo "NEEDS_INSTALL" - else - echo "NO_TIC" + # SSH wrapper + ssh() { + local e=() o=() c=() t + + # Get target + t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + + # Set up env vars first so terminfo installation inherits them + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + builtin export COLORTERM=${COLORTERM:-truecolor} TERM_PROGRAM=${TERM_PROGRAM:-ghostty} ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + for v in COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION}; do + o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + done + fi + + # Install terminfo if needed, reuse control connection for main session + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + if [[ -n $t ]] && _ghst_cache "$t" chk; then + e+=(TERM=xterm-ghostty) + elif builtin command -v infocmp >/dev/null 2>&1; then + builtin local ti + ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 + if [[ -n $ti ]]; then + builtin echo "Setting up Ghostty terminfo on remote host..." >&2 + builtin local cp + cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" + case $(builtin echo "$ti" | builtin command ssh "${o[@]}" -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit + command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL + ') in + OK) + builtin echo "Terminfo setup complete." >&2 + [[ -n $t ]] && _ghst_cache "$t" add + e+=(TERM=xterm-ghostty) + c+=(-o "ControlPath=$cp") + ;; + *) builtin echo "Warning: Failed to install terminfo." >&2 ;; + esac fi else - echo "ALREADY_INSTALLED" + builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 fi - ' - - # First connection: Start control master and check status - local install_status - install_status=$(builtin command ssh -o ControlMaster=yes \ - -o ControlPath="$control_path" \ - -o ControlPersist=30s \ - "$@" "$setup_script") - - case "$install_status" in - "NEEDS_INSTALL") - echo "Installing xterm-ghostty terminfo..." >&2 - # Send terminfo through existing control connection - if echo "$terminfo_data" | builtin command ssh -o ControlPath="$control_path" "$@" \ - 'tic -x - 2>/dev/null && echo "SUCCESS"' | grep -q "SUCCESS"; then - echo "Terminfo installed successfully." >&2 - [[ -n "$target" ]] && _ghostty_cache_host "$target" - else - echo "Warning: Failed to install terminfo. Using basic integration." >&2 - ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true - _ghostty_ssh_basic "$@" - return $? - fi - ;; - "ALREADY_INSTALLED") - [[ -n "$target" ]] && _ghostty_cache_host "$target" - ;; - "NO_TIC") - echo "Warning: tic not found on remote host. Using basic integration." >&2 - ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true - _ghostty_ssh_basic "$@" - return $? - ;; - esac - - # Now use the existing control connection for interactive session - echo "Connecting with full Ghostty support..." >&2 - - # Pass environment through and start login shell to show MOTD - builtin command ssh -t -o ControlPath="$control_path" "$@" " - # Set up Ghostty environment - export GHOSTTY_SHELL_FEATURES='$GHOSTTY_SHELL_FEATURES' - export TERM='xterm-ghostty' - - # Display MOTD if this is a fresh connection - if [[ '$install_status' == 'NEEDS_INSTALL' ]]; then - # Try to display MOTD manually - if [[ -f /etc/motd ]]; then - cat /etc/motd 2>/dev/null || true - fi - # Run update-motd if available (Ubuntu/Debian) - if [[ -d /etc/update-motd.d ]]; then - run-parts /etc/update-motd.d 2>/dev/null || true - fi - fi - - # Force a login shell - exec \$SHELL -l - " - - local exit_code=$? - - # Clean up control socket - ssh -O exit -o ControlPath="$control_path" "$@" 2>/dev/null || true - - return $exit_code - } - - # Utility function to clear cache for a specific host - ghostty_ssh_reset() { - local host="${1:-}" - if [[ -z "$host" ]]; then - echo "Usage: ghostty_ssh_reset " >&2 - return 1 fi - _ghostty_uncache_host "$host" - echo "Cleared Ghostty terminfo cache for: $host" - } + # Fallback TERM only if terminfo didn't set it + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + [[ $TERM == xterm-ghostty && ! " ${e[*]} " =~ " TERM=" ]] && e+=(TERM=xterm-256color) + fi - # Utility function to list cached hosts - ghostty_ssh_list_cached() { - if [[ -f "$_ghostty_cache_file" ]]; then - echo "Hosts with cached Ghostty terminfo:" - cat "$_ghostty_cache_file" + # Execute + if [[ ${#e[@]} -gt 0 ]]; then + env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" else - echo "No hosts cached yet." + builtin command ssh "${o[@]}" "${c[@]}" "$@" fi } + + # If 'ssh-terminfo' flag is enabled, wrap ghostty to provide 'ghostty ssh-cache-list' and `ghostty ssh-cache-clear` utility commands + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + ghostty() { + case "$1" in + ssh-cache-list) ghostty_ssh_cache_list ;; + ssh-cache-clear) ghostty_ssh_cache_clear ;; + *) builtin command ghostty "$@" ;; + esac + } + fi fi # Import bash-preexec, safe to do multiple times diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index deb258ae7..1e0c08732 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -99,176 +99,167 @@ } # SSH Integration - # Cache file for tracking hosts with terminfo installed - var ghostty-cache-file = (if (has-env GHOSTTY_RESOURCES_DIR) { put $E:GHOSTTY_RESOURCES_DIR"/terminfo_hosts" } else { put $E:HOME"/.config/ghostty/terminfo_hosts" }) + use str + use path + use re - # Extract target host from SSH arguments - fn ghostty-get-ssh-target {|@args| - var target = "" - var skip-next = $false + if (re:match 'ssh-(env|terminfo)' $E:GHOSTTY_SHELL_FEATURES) { + # Only define cache functions and variable if ssh-terminfo is enabled + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + var _cache = (path:join (or $E:XDG_STATE_HOME $E:HOME/.local/state) ghostty terminfo_hosts) - for arg $args { - if (eq $skip-next $true) { - set skip-next = $false - continue + # Cache operations and utilities + fn _ghst_cache {|target action| + if (eq $action chk) { + if (path:is-regular $_cache) { + try { + grep -qFx $target $_cache 2>/dev/null + } catch e { + fail + } + } else { + fail + } + } elif (eq $action add) { + mkdir -p (path:dir $_cache) + var tmpfile = $_cache.tmp + { + if (path:is-regular $_cache) { + cat $_cache + } + echo $target + } | sort -u > $tmpfile + mv $tmpfile $_cache + chmod 600 $_cache + } + } + + fn ghostty_ssh_cache_clear { + try { + rm -f $_cache 2>/dev/null + echo "Ghostty SSH terminfo cache cleared." + } catch e { + echo "No Ghostty SSH terminfo cache found." + } + } + + fn ghostty_ssh_cache_list { + if (and (path:is-regular $_cache) (> (wc -c < $_cache | str:trim-space) 0)) { + echo "Hosts with Ghostty terminfo installed:" + cat $_cache + } else { + echo "No cached hosts found." + } + } } - # Skip flags that take arguments - if (re:match '^-[bcDEeFIiJLlmOopQRSWw]$' $arg) { - set skip-next = $true - continue + # SSH wrapper + fn ssh {|@args| + var e = [] + var o = [] + var c = [] + var t = "" + + # Get target (only if ssh-terminfo enabled for caching) + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + try { + set t = (e:ssh -G $@args 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@' | str:trim-space) + } catch e { + # Ignore errors + } + } + + # Set up env vars first so terminfo installation inherits them + if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { + set-env COLORTERM (or $E:COLORTERM truecolor) + set-env TERM_PROGRAM (or $E:TERM_PROGRAM ghostty) + if (has-env GHOSTTY_VERSION) { + set-env TERM_PROGRAM_VERSION $E:GHOSTTY_VERSION + } + + var vars = [COLORTERM=truecolor TERM_PROGRAM=ghostty] + if (has-env GHOSTTY_VERSION) { + set vars = [$@vars TERM_PROGRAM_VERSION=$E:GHOSTTY_VERSION] + } + for v $vars { + var varname = (str:split &max=2 '=' $v | take 1) + set o = [$@o -o "SendEnv "$varname -o "SetEnv "$v] + } + } + + # Install terminfo if needed, reuse control connection for main session + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + if (and (not-eq $t "") (try { _ghst_cache $t chk } catch e { put $false })) { + set e = [$@e TERM=xterm-ghostty] + } elif (has-external infocmp) { + var ti = "" + try { + set ti = (infocmp -x xterm-ghostty 2>/dev/null | slurp) + } catch e { + echo "Warning: xterm-ghostty terminfo not found locally." >&2 + } + if (not-eq $ti "") { + echo "Setting up Ghostty terminfo on remote host..." >&2 + var cp = "/tmp/ghostty-ssh-"$E:USER"-"(randint 10000)"-"(date +%s | str:trim-space) + var result = (echo $ti | e:ssh $@o -o ControlMaster=yes -o ControlPath=$cp -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit + command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL + ' | str:trim-space) + if (eq $result OK) { + echo "Terminfo setup complete." >&2 + if (not-eq $t "") { + _ghst_cache $t add + } + set e = [$@e TERM=xterm-ghostty] + set c = [$@c -o ControlPath=$cp] + } else { + echo "Warning: Failed to install terminfo." >&2 + } + } + } else { + echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + } + } + + # Fallback TERM only if terminfo didn't set it + if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { + if (and (eq $E:TERM xterm-ghostty) (not (re:match 'TERM=' (str:join ' ' $e)))) { + set e = [$@e TERM=xterm-256color] + } + } + + # Execute + if (> (count $e) 0) { + e:env $@e e:ssh $@o $@c $@args + } else { + e:ssh $@o $@c $@args + } } - # Skip other flags - if (re:match '^-' $arg) { - continue + # Wrap ghostty command only if ssh-terminfo is enabled + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + fn ghostty {|@args| + if (eq $args[0] ssh-cache-list) { + ghostty_ssh_cache_list + } elif (eq $args[0] ssh-cache-clear) { + ghostty_ssh_cache_clear + } else { + (external ghostty) $@args + } + } + + edit:add-var ghostty~ $ghostty~ + + # Export cache functions for global use + set edit:add-var[ghostty_ssh_cache_clear] = $ghostty_ssh_cache_clear~ + set edit:add-var[ghostty_ssh_cache_list] = $ghostty_ssh_cache_list~ } - # This should be the target - set target = $arg - break - } - - put $target + # Export ssh function for global use + set edit:add-var[ssh] = $ssh~ } - # Check if host has terminfo installed - fn ghostty-host-has-terminfo {|target| - and (path:is-regular $ghostty-cache-file) ?(grep -qFx $target $ghostty-cache-file 2>/dev/null) - } - - # Add host to terminfo cache - fn ghostty-cache-host {|target| - var cache-dir = (path:dir $ghostty-cache-file) - - # Create cache directory if needed - if (not (path:is-dir $cache-dir)) { - mkdir -p $cache-dir - } - - # Atomic write to cache file - var temp-file = $ghostty-cache-file".tmp" - - { - if (path:is-regular $ghostty-cache-file) { - cat $ghostty-cache-file - } - echo $target - } | sort -u > $temp-file - - mv $temp-file $ghostty-cache-file - - # Secure permissions - ?chmod 600 $ghostty-cache-file 2>/dev/null - } - - fn ssh-with-ghostty-integration {|@args| - if (has-env GHOSTTY_SSH_INTEGRATION) { - if (eq "term-only" $E:GHOSTTY_SSH_INTEGRATION) { - ssh-term-only $@args - } elif (eq "basic" $E:GHOSTTY_SSH_INTEGRATION) { - ssh-basic $@args - } elif (eq "full" $E:GHOSTTY_SSH_INTEGRATION) { - ssh-full $@args - } else { - # Unknown level, fall back to basic - ssh-basic $@args - } - } else { - (external ssh) $@args - } - } - - fn ssh-term-only {|@args| - # Level: term-only - Just fix TERM compatibility - if (eq "xterm-ghostty" $E:TERM) { - (external env) TERM=xterm-256color ssh $@args - } else { - (external ssh) $@args - } - } - - fn ssh-basic {|@args| - # Level: basic - TERM fix + environment variable propagation - var env-vars = [] - - # Fix TERM compatibility - if (eq "xterm-ghostty" $E:TERM) { - set env-vars = (conj $env-vars TERM=xterm-256color) - } - - # Propagate Ghostty shell integration environment variables - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) - } - - # Execute with environment variables if any were set - if (> (count $env-vars) 0) { - (external env) $@env-vars ssh $@args - } else { - (external ssh) $@args - } - } - - fn ssh-full {|@args| - var target = (ghostty-get-ssh-target $@args) - - # Check if we already know this host has terminfo - if (and (not-eq "" $target) (ghostty-host-has-terminfo $target)) { - # Direct connection with xterm-ghostty - var env-vars = [TERM=xterm-ghostty] - - # Propagate Ghostty shell integration environment variables - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) - } - - (external env) $@env-vars ssh $@args - return - } - - # Full integration: Install terminfo if needed - if (has-external infocmp) { - try { - # Install terminfo only if needed - infocmp -x xterm-ghostty 2>/dev/null | (external ssh) $@args ' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null - fi - ' - echo "Connecting with full Ghostty support..." >&2 - - # Cache this host for future connections - if (not-eq "" $target) { - ghostty-cache-host $target - } - - # Connect with xterm-ghostty since terminfo is available - var env-vars = [TERM=xterm-ghostty] - - # Propagate Ghostty shell integration environment variables - if (not-eq "" $E:GHOSTTY_SHELL_FEATURES) { - set env-vars = (conj $env-vars GHOSTTY_SHELL_FEATURES=$E:GHOSTTY_SHELL_FEATURES) - } - - # Normal SSH connection with Ghostty terminfo available - (external env) $@env-vars ssh $@args - return - } catch e { - echo "Terminfo installation failed. Using basic integration." >&2 - } - } - - # Fallback to basic integration - ssh-basic $@args - } - - # Register SSH integration if enabled - if (and (has-env GHOSTTY_SSH_INTEGRATION) (has-external ssh)) { - edit:add-var ssh~ $ssh-with-ghostty-integration~ - } - defer { mark-prompt-start report-pwd diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index e01eabac3..73ccf9874 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -86,156 +86,116 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # SSH integration - if test -n "$GHOSTTY_SSH_INTEGRATION" - # Cache file for tracking hosts with terminfo installed - set --local _ghostty_cache_file (string join / (test -n "$GHOSTTY_RESOURCES_DIR"; and echo "$GHOSTTY_RESOURCES_DIR"; or echo "$HOME/.config/ghostty") "terminfo_hosts") + # SSH Integration + if string match -qr 'ssh-(env|terminfo)' "$GHOSTTY_SHELL_FEATURES" + # Only define cache functions and variable if ssh-terminfo is enabled + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + set -g _cache (test -n "$XDG_STATE_HOME" && echo "$XDG_STATE_HOME" || echo "$HOME/.local/state")/ghostty/terminfo_hosts - # Extract target host from SSH arguments - function _ghostty_get_ssh_target - set --local target "" - set --local skip_next "false" - - for arg in $argv - if test "$skip_next" = "true" - set skip_next "false" - continue + # Cache operations and utilities + function _ghst_cache + switch $argv[2] + case chk + test -f $_cache && grep -qFx "$argv[1]" "$_cache" 2>/dev/null + case add + mkdir -p (dirname "$_cache") + begin + test -f $_cache && cat "$_cache" + builtin echo "$argv[1]" + end | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" end + end - # Skip flags that take arguments - if string match -qr -- '^-[bcDEeFIiJLlmOopQRSWw]$' "$arg" - set skip_next "true" - continue + function ghostty_ssh_cache_clear -d "Clear Ghostty SSH terminfo cache" + rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." + end + + function ghostty_ssh_cache_list -d "List hosts with Ghostty terminfo installed" + test -s $_cache && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." + end + end + + # SSH wrapper + function ssh + set -l e + set -l o + set -l c + set -l t + + # Get target (only if ssh-terminfo enabled for caching) + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + set t (builtin command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + end + + # Set up env vars first so terminfo installation inherits them + if string match -qr 'ssh-env' "$GHOSTTY_SHELL_FEATURES" + set -gx COLORTERM (test -n "$COLORTERM" && echo "$COLORTERM" || echo "truecolor") + set -gx TERM_PROGRAM (test -n "$TERM_PROGRAM" && echo "$TERM_PROGRAM" || echo "ghostty") + test -n "$GHOSTTY_VERSION" && set -gx TERM_PROGRAM_VERSION "$GHOSTTY_VERSION" + + for v in COLORTERM=truecolor TERM_PROGRAM=ghostty (test -n "$GHOSTTY_VERSION" && echo "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION") + set -l varname (string split -m1 '=' "$v")[1] + set o $o -o "SendEnv $varname" -o "SetEnv $v" end - - # Skip other flags - if string match -q -- '-*' "$arg" - continue - end - - # This should be the target - set target "$arg" - break end - echo "$target" - end - - # Check if host has terminfo installed - function _ghostty_host_has_terminfo - set --local target "$argv[1]" - test -f "$_ghostty_cache_file"; and grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null - end - - # Add host to terminfo cache - function _ghostty_cache_host - set --local target "$argv[1]" - set --local cache_dir (dirname "$_ghostty_cache_file") - - # Create cache directory if needed - test -d "$cache_dir"; or mkdir -p "$cache_dir" - - # Atomic write to cache file - begin - if test -f "$_ghostty_cache_file" - cat "$_ghostty_cache_file" - end - echo "$target" - end | sort -u > "$_ghostty_cache_file.tmp"; and mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" - - # Secure permissions - chmod 600 "$_ghostty_cache_file" 2>/dev/null - end - - # Wrap `ssh` command to provide Ghostty SSH integration - function ssh -d "Wrap ssh to provide Ghostty SSH integration" - switch "$GHOSTTY_SSH_INTEGRATION" - case "term-only" - _ghostty_ssh_term-only $argv - case "basic" - _ghostty_ssh_basic $argv - case "full" - _ghostty_ssh_full $argv - case "*" - # Unknown level, fall back to basic - _ghostty_ssh_basic $argv - end - end - - # Level: term-only - Just fix TERM compatibility - function _ghostty_ssh_term-only -d "SSH with TERM compatibility fix" - if test "$TERM" = "xterm-ghostty" - TERM=xterm-256color builtin command ssh $argv - else - builtin command ssh $argv - end - end - - # Level: basic - TERM fix + environment variable propagation - function _ghostty_ssh_basic -d "SSH with TERM fix and environment propagation" - # Build environment variables to propagate - set --local env_vars - - # Fix TERM compatibility - if test "$TERM" = "xterm-ghostty" - set --append env_vars TERM=xterm-256color - end - - # Propagate Ghostty shell integration environment variables - if test -n "$GHOSTTY_SHELL_FEATURES" - set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" - end - - # Execute with environment variables if any were set - if test (count $env_vars) -gt 0 - env $env_vars ssh $argv - else - builtin command ssh $argv - end - end - - # Level: full - All features - function _ghostty_ssh_full - set --local target (_ghostty_get_ssh_target $argv) - - # Check if we already know this host has terminfo - if test -n "$target"; and _ghostty_host_has_terminfo "$target" - # Direct connection with xterm-ghostty - set --local env_vars TERM=xterm-ghostty - if test -n "$GHOSTTY_SHELL_FEATURES" - set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" - end - env $env_vars ssh $argv - return 0 - end - - # Full integration: Install terminfo if needed - if type -q infocmp - # Install terminfo only if needed - if infocmp -x xterm-ghostty 2>/dev/null | ssh $argv ' - if ! infocmp xterm-ghostty >/dev/null 2>&1 - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null + # Install terminfo if needed, reuse control connection for main session + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + if test -n "$t" && _ghst_cache "$t" chk + set e $e TERM=xterm-ghostty + else if command -v infocmp >/dev/null 2>&1 + set -l ti + set ti (infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 + if test -n "$ti" + builtin echo "Setting up Ghostty terminfo on remote host..." >&2 + set -l cp "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) + set -l result (builtin echo "$ti" | builtin command ssh $o -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s $argv ' + infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit + command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL + ') + switch $result + case OK + builtin echo "Terminfo setup complete." >&2 + test -n "$t" && _ghst_cache "$t" add + set e $e TERM=xterm-ghostty + set c $c -o "ControlPath=$cp" + case '*' + builtin echo "Warning: Failed to install terminfo." >&2 + end end - ' - echo "Connecting with full Ghostty support..." >&2 - - # Cache this host for future connections - test -n "$target"; and _ghostty_cache_host "$target" - - # Connect with xterm-ghostty since terminfo is available - set --local env_vars TERM=xterm-ghostty - if test -n "$GHOSTTY_SHELL_FEATURES" - set --append env_vars GHOSTTY_SHELL_FEATURES="$GHOSTTY_SHELL_FEATURES" - end - env $env_vars ssh $argv - builtin return 0 + else + builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 end - echo "Terminfo installation failed. Using basic integration." >&2 end - # Fallback to basic integration - _ghostty_ssh_basic $argv + # Fallback TERM only if terminfo didn't set it + if string match -qr 'ssh-env' "$GHOSTTY_SHELL_FEATURES" + if test "$TERM" = xterm-ghostty && not string match -q '*TERM=*' "$e" + set e $e TERM=xterm-256color + end + end + + # Execute + if test (count $e) -gt 0 + env $e ssh $o $c $argv + else + builtin command ssh $o $c $argv + end + end + + # Wrap ghostty command only if ssh-terminfo is enabled + if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" + function ghostty -d "Wrap ghostty to provide cache management commands" + switch "$argv[1]" + case ssh-cache-list + ghostty_ssh_cache_list + case ssh-cache-clear + ghostty_ssh_cache_clear + case "*" + command ghostty $argv + end + end end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index c172fc02d..b3c604c83 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -244,156 +244,106 @@ _ghostty_deferred_init() { } fi - # SSH - if [[ -n "$GHOSTTY_SSH_INTEGRATION" ]]; then - # Cache file for tracking hosts with terminfo installed - _ghostty_cache_file="${GHOSTTY_RESOURCES_DIR:-$HOME/.config/ghostty}/terminfo_hosts" + # SSH Integration + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + # Only define cache functions and variable if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + _cache="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - # Extract target host from SSH arguments - _ghostty_get_ssh_target() { - local target="" - local skip_next=false + # Cache operations and utilities + _ghst_cache() { + case $2 in + chk) [[ -f $_cache ]] && grep -qFx "$1" "$_cache" 2>/dev/null ;; + add) + mkdir -p "${_cache:h}" + { + [[ -f $_cache ]] && cat "$_cache" + builtin echo "$1" + } | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" + ;; + esac + } - for arg in "$@"; do - if [[ "$skip_next" == "true" ]]; then - skip_next=false - continue - fi + ghostty_ssh_cache_clear() { + rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." + } - # Skip flags that take arguments - if [[ "$arg" =~ ^-[bcDEeFIiJLlmOopQRSWw]$ ]]; then - skip_next=true - continue - fi + ghostty_ssh_cache_list() { + [[ -s $_cache ]] && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." + } + fi - # Skip other flags - if [[ "$arg" =~ ^- ]]; then - continue - fi - - # This should be the target - target="$arg" - break - done - - echo "$target" - } - - # Check if host has terminfo installed - _ghostty_host_has_terminfo() { - local target="$1" - [[ -f "$_ghostty_cache_file" ]] && grep -qFx "$target" "$_ghostty_cache_file" 2>/dev/null - } - - # Add host to terminfo cache - _ghostty_cache_host() { - local target="$1" - local cache_dir - cache_dir="$(dirname "$_ghostty_cache_file")" - - # Create cache directory if needed - [[ ! -d "$cache_dir" ]] && mkdir -p "$cache_dir" - - # Atomic write to cache file - { - if [[ -f "$_ghostty_cache_file" ]]; then - cat "$_ghostty_cache_file" - fi - echo "$target" - } | sort -u > "$_ghostty_cache_file.tmp" && mv "$_ghostty_cache_file.tmp" "$_ghostty_cache_file" - - # Secure permissions - chmod 600 "$_ghostty_cache_file" 2>/dev/null - } - - # Wrap `ssh` command to provide Ghostty SSH integration + # SSH wrapper ssh() { - case "$GHOSTTY_SSH_INTEGRATION" in - "term-only") - _ghostty_ssh_term-only "$@" - ;; - "basic") - _ghostty_ssh_basic "$@" - ;; - "full") - _ghostty_ssh_full "$@" - ;; - *) - # Unknown level, fall back to basic - _ghostty_ssh_basic "$@" - ;; - esac - } + local -a e o c + local t - # Level: term-only - Just fix TERM compatibility - _ghostty_ssh_term-only() { - if [[ "$TERM" == "xterm-ghostty" ]]; then - TERM=xterm-256color builtin command ssh "$@" - else - builtin command ssh "$@" - fi - } - - # Level: basic - TERM fix + environment variable propagation - _ghostty_ssh_basic() { - local env_vars=() - - # Fix TERM compatibility - if [[ "$TERM" == "xterm-ghostty" ]]; then - env_vars+=("TERM=xterm-256color") + # Get target (only if ssh-terminfo enabled for caching) + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') fi - # Propagate Ghostty shell integration environment variables - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - - # Execute with environment variables if any were set - if [[ ${#env_vars[@]} -gt 0 ]]; then - env "${env_vars[@]}" ssh "$@" - else - builtin command ssh "$@" - fi - } - - # Level: full - All features - _ghostty_ssh_full() { - local target - target="$(_ghostty_get_ssh_target "$@")" - - # Check if we already know this host has terminfo - if [[ -n "$target" ]] && _ghostty_host_has_terminfo "$target"; then - # Direct connection with xterm-ghostty - local env_vars=("TERM=xterm-ghostty") - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - env "${env_vars[@]}" ssh "$@" - return 0 + # Set up env vars first so terminfo installation inherits them + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + builtin export COLORTERM=${COLORTERM:-truecolor} TERM_PROGRAM=${TERM_PROGRAM:-ghostty} ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + for v in COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION}; do + o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + done fi - # Full integration: Install terminfo if needed - if builtin command -v infocmp >/dev/null 2>&1; then - # Install terminfo only if needed - if infocmp -x xterm-ghostty 2>/dev/null | builtin command ssh "$@" ' - if ! infocmp xterm-ghostty >/dev/null 2>&1; then - echo "Installing Ghostty terminfo..." >&2 - tic -x - 2>/dev/null - fi - '; then - echo "Connecting with full Ghostty support..." >&2 - - # Cache this host for future connections - [[ -n "$target" ]] && _ghostty_cache_host "$target" - - # Connect with xterm-ghostty since terminfo is available - local env_vars=("TERM=xterm-ghostty") - [[ -n "$GHOSTTY_SHELL_FEATURES" ]] && env_vars+=("GHOSTTY_SHELL_FEATURES=$GHOSTTY_SHELL_FEATURES") - env "${env_vars[@]}" ssh "$@" - builtin return 0 + # Install terminfo if needed, reuse control connection for main session + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + if [[ -n $t ]] && _ghst_cache "$t" chk; then + e+=(TERM=xterm-ghostty) + elif builtin command -v infocmp >/dev/null 2>&1; then + local ti + ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 + if [[ -n $ti ]]; then + builtin echo "Setting up Ghostty terminfo on remote host..." >&2 + local cp + cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" + case $(builtin echo "$ti" | builtin command ssh "${o[@]}" -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit + command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL + ') in + OK) + builtin echo "Terminfo setup complete." >&2 + [[ -n $t ]] && _ghst_cache "$t" add + e+=(TERM=xterm-ghostty) + c+=(-o "ControlPath=$cp") + ;; + *) builtin echo "Warning: Failed to install terminfo." >&2 ;; + esac + fi + else + builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 fi - echo "Terminfo installation failed. Using basic integration." >&2 fi - # Fallback to basic integration - _ghostty_ssh_basic "$@" + # Fallback TERM only if terminfo didn't set it + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + [[ $TERM == xterm-ghostty && ! " ${(j: :)e} " =~ " TERM=" ]] && e+=(TERM=xterm-256color) + fi + + # Execute + if (( ${#e} > 0 )); then + env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" + else + builtin command ssh "${o[@]}" "${c[@]}" "$@" + fi } + + # Wrap ghostty command only if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + ghostty() { + case "$1" in + ssh-cache-list) ghostty_ssh_cache_list ;; + ssh-cache-clear) ghostty_ssh_cache_clear ;; + *) builtin command ghostty "$@" ;; + esac + } + fi fi # Some zsh users manually run `source ~/.zshrc` in order to apply rc file From c8d5e603906f5d442e0707c38e2303cc6d744b67 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 24 Jun 2025 11:41:35 -0700 Subject: [PATCH 26/86] docs: expand flag descriptions, usage overview --- src/config/Config.zig | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 41171b3be..b03773295 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1975,10 +1975,19 @@ keybind: Keybinds = .{}, /// * `ssh-env` - Enable SSH environment variable compatibility. Automatically /// converts TERM from `xterm-ghostty` to `xterm-256color` when connecting to /// remote hosts and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION. +/// Whether or not these variables will be accepted by the remote host(s) will +/// depend on whether or not the variables are allowed in their sshd_config. /// /// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. /// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when -/// connecting to hosts that lack it. +/// connecting to hosts that lack it. Requires `infocmp` and `tic` to be available +/// locally. Provides `ghostty ssh-cache-list` and `ghostty ssh-cache-clear` +/// utilities for managing the installation cache. +/// +/// SSH features work independently and can be combined for optimal experience: +/// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its +/// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to +/// `xterm-256color` with environment variables if terminfo installation fails. /// /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` @"shell-integration-features": ShellIntegrationFeatures = .{}, From bbb02a83923c25be263b51c35779b5e1dbff4a44 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 24 Jun 2025 12:00:22 -0700 Subject: [PATCH 27/86] test: update shell integration tests for SSH flags Add ssh-env and ssh-terminfo fields to existing setupFeatures tests. --- src/termio/shell_integration.zig | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index fb62327d3..36ae116d5 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -201,8 +201,8 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true }); - try testing.expectEqualStrings("cursor,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); + try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true, .@"ssh-env" = true, .@"ssh-terminfo" = true }); + try testing.expectEqualStrings("cursor,ssh-env,ssh-terminfo,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); } // Test: all features disabled @@ -210,7 +210,7 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false }); + try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false, .@"ssh-env" = false, .@"ssh-terminfo" = false }); try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } @@ -219,8 +219,8 @@ test "setup features" { var env = EnvMap.init(alloc); defer env.deinit(); - try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false }); - try testing.expectEqualStrings("sudo", env.get("GHOSTTY_SHELL_FEATURES").?); + try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false }); + try testing.expectEqualStrings("sudo,ssh-env", env.get("GHOSTTY_SHELL_FEATURES").?); } } From 8a2fa6485e54483332b6224f04be47c9047f1947 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 24 Jun 2025 17:15:35 -0700 Subject: [PATCH 28/86] refactor: extract SSH cache functionality to shared script Addresses feedback about separation of concerns in shell integration scripts. Extracts host caching logic to `src/shell-integration/shared/ghostty-ssh-cache` and updates all four shell integrations to use the shared script. The `shared/` subdirectory preserves the existing organizational pattern where all shell-specific code lives in subdirectories. This cleanly separates SSH transport logic from cache management while reducing code duplication by ~25%. All existing SSH integration behavior remains identical. --- src/shell-integration/bash/ghostty.bash | 65 ++--- .../elvish/lib/ghostty-integration.elv | 233 +++++++----------- .../ghostty-shell-integration.fish | 81 +++--- .../shared/ghostty-ssh-cache | 11 + src/shell-integration/zsh/ghostty-integration | 66 ++--- 5 files changed, 174 insertions(+), 282 deletions(-) create mode 100755 src/shell-integration/shared/ghostty-ssh-cache diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 6e09ab193..9ba11c845 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -97,56 +97,48 @@ fi # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then - # Only define cache functions and variable if ssh-terminfo is enabled + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - _cache="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - - # Cache operations and utilities - _ghst_cache() { - case $2 in - chk) [[ -f $_cache ]] && grep -qFx "$1" "$_cache" 2>/dev/null ;; - add) - mkdir -p "${_cache%/*}" - { - [[ -f $_cache ]] && cat "$_cache" - builtin echo "$1" - } | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" - ;; + readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" + # If 'ssh-terminfo' flag is enabled, wrap ghostty to provide cache management commands + ghostty() { + case "$1" in + ssh-cache-list) "$_CACHE" list ;; + ssh-cache-clear) "$_CACHE" clear ;; + *) builtin command ghostty "$@" ;; esac } - - function ghostty_ssh_cache_clear() { - rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." - } - - function ghostty_ssh_cache_list() { - [[ -s $_cache ]] && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." - } fi # SSH wrapper ssh() { - local e=() o=() c=() t - - # Get target - t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + local e=() o=() c=() # Removed 't' from here # Set up env vars first so terminfo installation inherits them if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - builtin export COLORTERM=${COLORTERM:-truecolor} TERM_PROGRAM=${TERM_PROGRAM:-ghostty} ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} - for v in COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION}; do + local vars=( + COLORTERM=truecolor + TERM_PROGRAM=ghostty + ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + ) + for v in "${vars[@]}"; do + builtin export "${v?}" o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") done fi # Install terminfo if needed, reuse control connection for main session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - if [[ -n $t ]] && _ghst_cache "$t" chk; then + # Get target (only when needed for terminfo) + builtin local t + t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + + if [[ -n "$t" ]] && "$_CACHE" chk "$t"; then e+=(TERM=xterm-ghostty) elif builtin command -v infocmp >/dev/null 2>&1; then builtin local ti ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n $ti ]]; then + if [[ -n "$ti" ]]; then builtin echo "Setting up Ghostty terminfo on remote host..." >&2 builtin local cp cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" @@ -157,7 +149,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then ') in OK) builtin echo "Terminfo setup complete." >&2 - [[ -n $t ]] && _ghst_cache "$t" add + [[ -n "$t" ]] && "$_CACHE" add "$t" e+=(TERM=xterm-ghostty) c+=(-o "ControlPath=$cp") ;; @@ -181,17 +173,6 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then builtin command ssh "${o[@]}" "${c[@]}" "$@" fi } - - # If 'ssh-terminfo' flag is enabled, wrap ghostty to provide 'ghostty ssh-cache-list' and `ghostty ssh-cache-clear` utility commands - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - ghostty() { - case "$1" in - ssh-cache-list) ghostty_ssh_cache_list ;; - ssh-cache-clear) ghostty_ssh_cache_clear ;; - *) builtin command ghostty "$@" ;; - esac - } - fi fi # Import bash-preexec, safe to do multiple times diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 1e0c08732..09aa09f31 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -104,160 +104,107 @@ use re if (re:match 'ssh-(env|terminfo)' $E:GHOSTTY_SHELL_FEATURES) { - # Only define cache functions and variable if ssh-terminfo is enabled + if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { + var _cache_script = (path:join $E:GHOSTTY_RESOURCES_DIR shell-integration shared ghostty-ssh-cache) + + # Wrap ghostty command to provide cache management commands + fn ghostty {|@args| + if (eq $args[0] ssh-cache-list) { + (external $_cache_script) list + } elif (eq $args[0] ssh-cache-clear) { + (external $_cache_script) clear + } else { + (external ghostty) $@args + } + } + + edit:add-var ghostty~ $ghostty~ + } + + # SSH wrapper + fn ssh {|@args| + var e = [] + var o = [] + var c = [] + + # Set up env vars first so terminfo installation inherits them + if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { + set-env COLORTERM (or $E:COLORTERM truecolor) + set-env TERM_PROGRAM (or $E:TERM_PROGRAM ghostty) + if (has-env GHOSTTY_VERSION) { + set-env TERM_PROGRAM_VERSION $E:GHOSTTY_VERSION + } + + var vars = [COLORTERM=truecolor TERM_PROGRAM=ghostty] + if (has-env GHOSTTY_VERSION) { + set vars = [$@vars TERM_PROGRAM_VERSION=$E:GHOSTTY_VERSION] + } + for v $vars { + var varname = (str:split &max=2 '=' $v | take 1) + set o = [$@o -o "SendEnv "$varname -o "SetEnv "$v] + } + } + + # Install terminfo if needed, reuse control connection for main session if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - var _cache = (path:join (or $E:XDG_STATE_HOME $E:HOME/.local/state) ghostty terminfo_hosts) + # Get target (only when needed for terminfo) + var t = "" + try { + set t = (e:ssh -G $@args 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@' | str:trim-space) + } catch e { + # Ignore errors + } - # Cache operations and utilities - fn _ghst_cache {|target action| - if (eq $action chk) { - if (path:is-regular $_cache) { - try { - grep -qFx $target $_cache 2>/dev/null - } catch e { - fail - } - } else { - fail - } - } elif (eq $action add) { - mkdir -p (path:dir $_cache) - var tmpfile = $_cache.tmp - { - if (path:is-regular $_cache) { - cat $_cache - } - echo $target - } | sort -u > $tmpfile - mv $tmpfile $_cache - chmod 600 $_cache - } + if (and (not-eq $t "") (try { (external $_cache_script) chk $t } catch e { put $false })) { + set e = [$@e TERM=xterm-ghostty] + } elif (has-external infocmp) { + var ti = "" + try { + set ti = (infocmp -x xterm-ghostty 2>/dev/null | slurp) + } catch e { + echo "Warning: xterm-ghostty terminfo not found locally." >&2 } - - fn ghostty_ssh_cache_clear { - try { - rm -f $_cache 2>/dev/null - echo "Ghostty SSH terminfo cache cleared." - } catch e { - echo "No Ghostty SSH terminfo cache found." - } - } - - fn ghostty_ssh_cache_list { - if (and (path:is-regular $_cache) (> (wc -c < $_cache | str:trim-space) 0)) { - echo "Hosts with Ghostty terminfo installed:" - cat $_cache - } else { - echo "No cached hosts found." + if (not-eq $ti "") { + echo "Setting up Ghostty terminfo on remote host..." >&2 + var cp = "/tmp/ghostty-ssh-"$E:USER"-"(randint 10000)"-"(date +%s | str:trim-space) + var result = (echo $ti | e:ssh $@o -o ControlMaster=yes -o ControlPath=$cp -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit + command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL + ' | str:trim-space) + if (eq $result OK) { + echo "Terminfo setup complete." >&2 + if (not-eq $t "") { + (external $_cache_script) add $t } + set e = [$@e TERM=xterm-ghostty] + set c = [$@c -o ControlPath=$cp] + } else { + echo "Warning: Failed to install terminfo." >&2 + } } + } else { + echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + } } - # SSH wrapper - fn ssh {|@args| - var e = [] - var o = [] - var c = [] - var t = "" - - # Get target (only if ssh-terminfo enabled for caching) - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - try { - set t = (e:ssh -G $@args 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@' | str:trim-space) - } catch e { - # Ignore errors - } - } - - # Set up env vars first so terminfo installation inherits them - if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { - set-env COLORTERM (or $E:COLORTERM truecolor) - set-env TERM_PROGRAM (or $E:TERM_PROGRAM ghostty) - if (has-env GHOSTTY_VERSION) { - set-env TERM_PROGRAM_VERSION $E:GHOSTTY_VERSION - } - - var vars = [COLORTERM=truecolor TERM_PROGRAM=ghostty] - if (has-env GHOSTTY_VERSION) { - set vars = [$@vars TERM_PROGRAM_VERSION=$E:GHOSTTY_VERSION] - } - for v $vars { - var varname = (str:split &max=2 '=' $v | take 1) - set o = [$@o -o "SendEnv "$varname -o "SetEnv "$v] - } - } - - # Install terminfo if needed, reuse control connection for main session - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - if (and (not-eq $t "") (try { _ghst_cache $t chk } catch e { put $false })) { - set e = [$@e TERM=xterm-ghostty] - } elif (has-external infocmp) { - var ti = "" - try { - set ti = (infocmp -x xterm-ghostty 2>/dev/null | slurp) - } catch e { - echo "Warning: xterm-ghostty terminfo not found locally." >&2 - } - if (not-eq $ti "") { - echo "Setting up Ghostty terminfo on remote host..." >&2 - var cp = "/tmp/ghostty-ssh-"$E:USER"-"(randint 10000)"-"(date +%s | str:trim-space) - var result = (echo $ti | e:ssh $@o -o ControlMaster=yes -o ControlPath=$cp -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ' | str:trim-space) - if (eq $result OK) { - echo "Terminfo setup complete." >&2 - if (not-eq $t "") { - _ghst_cache $t add - } - set e = [$@e TERM=xterm-ghostty] - set c = [$@c -o ControlPath=$cp] - } else { - echo "Warning: Failed to install terminfo." >&2 - } - } - } else { - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 - } - } - - # Fallback TERM only if terminfo didn't set it - if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { - if (and (eq $E:TERM xterm-ghostty) (not (re:match 'TERM=' (str:join ' ' $e)))) { - set e = [$@e TERM=xterm-256color] - } - } - - # Execute - if (> (count $e) 0) { - e:env $@e e:ssh $@o $@c $@args - } else { - e:ssh $@o $@c $@args - } + # Fallback TERM only if terminfo didn't set it + if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { + if (and (eq $E:TERM xterm-ghostty) (not (re:match 'TERM=' (str:join ' ' $e)))) { + set e = [$@e TERM=xterm-256color] + } } - # Wrap ghostty command only if ssh-terminfo is enabled - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - fn ghostty {|@args| - if (eq $args[0] ssh-cache-list) { - ghostty_ssh_cache_list - } elif (eq $args[0] ssh-cache-clear) { - ghostty_ssh_cache_clear - } else { - (external ghostty) $@args - } - } - - edit:add-var ghostty~ $ghostty~ - - # Export cache functions for global use - set edit:add-var[ghostty_ssh_cache_clear] = $ghostty_ssh_cache_clear~ - set edit:add-var[ghostty_ssh_cache_list] = $ghostty_ssh_cache_list~ + # Execute + if (> (count $e) 0) { + e:env $@e e:ssh $@o $@c $@args + } else { + e:ssh $@o $@c $@args } + } - # Export ssh function for global use - set edit:add-var[ssh] = $ssh~ + # Export ssh function for global use + set edit:add-var[ssh] = $ssh~ } defer { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 73ccf9874..55ce34985 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -63,14 +63,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias - if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") + if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x") # Wrap `sudo` command to ensure Ghostty terminfo is preserved function sudo -d "Wrap sudo to preserve terminfo" - set --function sudo_has_sudoedit_flags "no" + set --function sudo_has_sudoedit_flags no for arg in $argv # Check if argument is '-e' or '--edit' (sudoedit flags) - if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg" - set --function sudo_has_sudoedit_flags "yes" + if string match -q -- -e "$arg"; or string match -q -- --edit "$arg" + set --function sudo_has_sudoedit_flags yes break end # Check if argument is neither an option nor a key-value pair @@ -78,7 +78,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" break end end - if test "$sudo_has_sudoedit_flags" = "yes" + if test "$sudo_has_sudoedit_flags" = yes command sudo $argv else command sudo TERMINFO="$TERMINFO" $argv @@ -88,31 +88,20 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # SSH Integration if string match -qr 'ssh-(env|terminfo)' "$GHOSTTY_SHELL_FEATURES" - # Only define cache functions and variable if ssh-terminfo is enabled - if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" - set -g _cache (test -n "$XDG_STATE_HOME" && echo "$XDG_STATE_HOME" || echo "$HOME/.local/state")/ghostty/terminfo_hosts + if string match -qr ssh-terminfo "$GHOSTTY_SHELL_FEATURES" + set -g _cache_script "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" - # Cache operations and utilities - function _ghst_cache - switch $argv[2] - case chk - test -f $_cache && grep -qFx "$argv[1]" "$_cache" 2>/dev/null - case add - mkdir -p (dirname "$_cache") - begin - test -f $_cache && cat "$_cache" - builtin echo "$argv[1]" - end | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" + # Wrap ghostty command to provide cache management commands + function ghostty -d "Wrap ghostty to provide cache management commands" + switch "$argv[1]" + case ssh-cache-list + command "$_cache_script" list + case ssh-cache-clear + command "$_cache_script" clear + case "*" + command ghostty $argv end end - - function ghostty_ssh_cache_clear -d "Clear Ghostty SSH terminfo cache" - rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." - end - - function ghostty_ssh_cache_list -d "List hosts with Ghostty terminfo installed" - test -s $_cache && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." - end end # SSH wrapper @@ -120,28 +109,28 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set -l e set -l o set -l c - set -l t - - # Get target (only if ssh-terminfo enabled for caching) - if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" - set t (builtin command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - end # Set up env vars first so terminfo installation inherits them - if string match -qr 'ssh-env' "$GHOSTTY_SHELL_FEATURES" + if string match -qr ssh-env "$GHOSTTY_SHELL_FEATURES" set -gx COLORTERM (test -n "$COLORTERM" && echo "$COLORTERM" || echo "truecolor") set -gx TERM_PROGRAM (test -n "$TERM_PROGRAM" && echo "$TERM_PROGRAM" || echo "ghostty") test -n "$GHOSTTY_VERSION" && set -gx TERM_PROGRAM_VERSION "$GHOSTTY_VERSION" - for v in COLORTERM=truecolor TERM_PROGRAM=ghostty (test -n "$GHOSTTY_VERSION" && echo "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION") + set -l vars COLORTERM=truecolor TERM_PROGRAM=ghostty + test -n "$GHOSTTY_VERSION" && set vars $vars "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION" + + for v in $vars set -l varname (string split -m1 '=' "$v")[1] set o $o -o "SendEnv $varname" -o "SetEnv $v" end end # Install terminfo if needed, reuse control connection for main session - if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" - if test -n "$t" && _ghst_cache "$t" chk + if string match -qr ssh-terminfo "$GHOSTTY_SHELL_FEATURES" + # Get target (only when needed for terminfo) + set -l t (builtin command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + + if test -n "$t" && command "$_cache_script" chk "$t" set e $e TERM=xterm-ghostty else if command -v infocmp >/dev/null 2>&1 set -l ti @@ -157,7 +146,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" switch $result case OK builtin echo "Terminfo setup complete." >&2 - test -n "$t" && _ghst_cache "$t" add + test -n "$t" && command "$_cache_script" add "$t" set e $e TERM=xterm-ghostty set c $c -o "ControlPath=$cp" case '*' @@ -170,7 +159,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # Fallback TERM only if terminfo didn't set it - if string match -qr 'ssh-env' "$GHOSTTY_SHELL_FEATURES" + if string match -qr ssh-env "$GHOSTTY_SHELL_FEATURES" if test "$TERM" = xterm-ghostty && not string match -q '*TERM=*' "$e" set e $e TERM=xterm-256color end @@ -183,20 +172,6 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" builtin command ssh $o $c $argv end end - - # Wrap ghostty command only if ssh-terminfo is enabled - if string match -qr 'ssh-terminfo' "$GHOSTTY_SHELL_FEATURES" - function ghostty -d "Wrap ghostty to provide cache management commands" - switch "$argv[1]" - case ssh-cache-list - ghostty_ssh_cache_list - case ssh-cache-clear - ghostty_ssh_cache_clear - case "*" - command ghostty $argv - end - end - end end # Setup prompt marking diff --git a/src/shell-integration/shared/ghostty-ssh-cache b/src/shell-integration/shared/ghostty-ssh-cache new file mode 100755 index 000000000..e0a6d8452 --- /dev/null +++ b/src/shell-integration/shared/ghostty-ssh-cache @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Minimal Ghostty SSH terminfo host cache + +readonly CACHE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" + +case "${1:-}" in + chk) [[ -f "$CACHE_FILE" ]] && grep -qFx "$2" "$CACHE_FILE" 2>/dev/null ;; + add) mkdir -p "${CACHE_FILE%/*}"; { [[ -f "$CACHE_FILE" ]] && cat "$CACHE_FILE"; echo "$2"; } | sort -u > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" && chmod 600 "$CACHE_FILE" ;; + list) [[ -s "$CACHE_FILE" ]] && echo "Hosts with Ghostty terminfo installed:" && cat "$CACHE_FILE" || echo "No cached hosts found." ;; + clear) rm -f "$CACHE_FILE" 2>/dev/null && echo "Ghostty SSH terminfo cache cleared." || echo "No Ghostty SSH terminfo cache found." ;; +esac diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index b3c604c83..4305ab61f 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -246,54 +246,43 @@ _ghostty_deferred_init() { # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then - # Only define cache functions and variable if ssh-terminfo is enabled if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - _cache="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - - # Cache operations and utilities - _ghst_cache() { - case $2 in - chk) [[ -f $_cache ]] && grep -qFx "$1" "$_cache" 2>/dev/null ;; - add) - mkdir -p "${_cache:h}" - { - [[ -f $_cache ]] && cat "$_cache" - builtin echo "$1" - } | sort -u >"$_cache.tmp" && mv "$_cache.tmp" "$_cache" && chmod 600 "$_cache" - ;; + readonly _cache_script="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" + + # Wrap ghostty command to provide cache management commands + ghostty() { + case "$1" in + ssh-cache-list) "$_cache_script" list ;; + ssh-cache-clear) "$_cache_script" clear ;; + *) builtin command ghostty "$@" ;; esac } - - ghostty_ssh_cache_clear() { - rm -f "$_cache" 2>/dev/null && builtin echo "Ghostty SSH terminfo cache cleared." || builtin echo "No Ghostty SSH terminfo cache found." - } - - ghostty_ssh_cache_list() { - [[ -s $_cache ]] && builtin echo "Hosts with Ghostty terminfo installed:" && cat "$_cache" || builtin echo "No cached hosts found." - } fi # SSH wrapper ssh() { local -a e o c - local t - - # Get target (only if ssh-terminfo enabled for caching) - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - fi - + # Set up env vars first so terminfo installation inherits them if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - builtin export COLORTERM=${COLORTERM:-truecolor} TERM_PROGRAM=${TERM_PROGRAM:-ghostty} ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} - for v in COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION}; do + local vars=( + COLORTERM=truecolor + TERM_PROGRAM=ghostty + ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + ) + for v in "${vars[@]}"; do + builtin export "${v?}" o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") done fi # Install terminfo if needed, reuse control connection for main session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - if [[ -n $t ]] && _ghst_cache "$t" chk; then + # Get target (only when needed for terminfo) + builtin local t + t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + + if [[ -n $t ]] && "$_cache_script" chk "$t"; then e+=(TERM=xterm-ghostty) elif builtin command -v infocmp >/dev/null 2>&1; then local ti @@ -309,7 +298,7 @@ _ghostty_deferred_init() { ') in OK) builtin echo "Terminfo setup complete." >&2 - [[ -n $t ]] && _ghst_cache "$t" add + [[ -n $t ]] && "$_cache_script" add "$t" e+=(TERM=xterm-ghostty) c+=(-o "ControlPath=$cp") ;; @@ -333,17 +322,6 @@ _ghostty_deferred_init() { builtin command ssh "${o[@]}" "${c[@]}" "$@" fi } - - # Wrap ghostty command only if ssh-terminfo is enabled - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - ghostty() { - case "$1" in - ssh-cache-list) ghostty_ssh_cache_list ;; - ssh-cache-clear) ghostty_ssh_cache_clear ;; - *) builtin command ghostty "$@" ;; - esac - } - fi fi # Some zsh users manually run `source ~/.zshrc` in order to apply rc file From 6789b7fb6e459b8ca143b946fd0f4f7e6d8fda34 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 12:41:55 -0700 Subject: [PATCH 29/86] docs: add shared directory section to shell-integration README --- src/shell-integration/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 1fd11091d..c09ea34db 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -88,3 +88,17 @@ if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration fi ``` + +## Shared Resources + +The `shared/` directory contains utilities available to all shell integrations: + +- `ghostty-ssh-cache`: A standalone script that manages the SSH terminfo host + cache for the `ssh-terminfo` shell integration feature. This script handles + cache file operations (list, clear, check, add) and is called by all shell + integrations when `ssh-terminfo` is enabled. It is also called by the + `+list-ssh-cache` and `+clear-ssh-cache` CLI actions, providing users with + direct cache management capabilities. + +The shared approach maintains separation of concerns by keeping shell-specific +integration files independent of secondary logic. From 0565ed39546a982b7f47d0dd0ba341bc41cc092f Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 12:47:38 -0700 Subject: [PATCH 30/86] refactor: replace ghostty wrapper with proper CLI actions for terminfo cache management - Add +list-ssh-cache and +clear-ssh-cache CLI actions - Remove ghostty() wrapper functions from all shell integrations - Improve variable naming in shell scripts for readability Addresses @00-kat's feedback about CLI discoverability and naming consistency. The new CLI actions follow established Ghostty patterns and are discoverable via `ghostty --help`, while maintaining clean separation of concerns between shell logic and cache management. --- src/cli/action.zig | 12 +++ src/cli/clear_ssh_cache.zig | 40 +++++++ src/cli/list_ssh_cache.zig | 38 +++++++ src/cli/ssh_cache.zig | 71 ++++++++++++ src/shell-integration/bash/ghostty.bash | 46 ++++---- .../elvish/lib/ghostty-integration.elv | 102 +++++++----------- .../ghostty-shell-integration.fish | 93 ++++++++-------- src/shell-integration/zsh/ghostty-integration | 70 ++++++------ 8 files changed, 299 insertions(+), 173 deletions(-) create mode 100644 src/cli/clear_ssh_cache.zig create mode 100644 src/cli/list_ssh_cache.zig create mode 100644 src/cli/ssh_cache.zig diff --git a/src/cli/action.zig b/src/cli/action.zig index 009afb4c9..1d1c3bfa0 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -9,6 +9,8 @@ const list_keybinds = @import("list_keybinds.zig"); const list_themes = @import("list_themes.zig"); const list_colors = @import("list_colors.zig"); const list_actions = @import("list_actions.zig"); +const list_ssh_cache = @import("list_ssh_cache.zig"); +const clear_ssh_cache = @import("clear_ssh_cache.zig"); const edit_config = @import("edit_config.zig"); const show_config = @import("show_config.zig"); const validate_config = @import("validate_config.zig"); @@ -41,6 +43,12 @@ pub const Action = enum { /// List keybind actions @"list-actions", + /// List hosts with Ghostty SSH terminfo installed + @"list-ssh-cache", + + /// Clear Ghostty SSH terminfo cache + @"clear-ssh-cache", + /// Edit the config file in the configured terminal editor. @"edit-config", @@ -155,6 +163,8 @@ pub const Action = enum { .@"list-themes" => try list_themes.run(alloc), .@"list-colors" => try list_colors.run(alloc), .@"list-actions" => try list_actions.run(alloc), + .@"list-ssh-cache" => @import("list_ssh_cache.zig").run(alloc), + .@"clear-ssh-cache" => @import("clear_ssh_cache.zig").run(alloc), .@"edit-config" => try edit_config.run(alloc), .@"show-config" => try show_config.run(alloc), .@"validate-config" => try validate_config.run(alloc), @@ -192,6 +202,8 @@ pub const Action = enum { .@"list-themes" => list_themes.Options, .@"list-colors" => list_colors.Options, .@"list-actions" => list_actions.Options, + .@"list-ssh-cache" => list_ssh_cache.Options, + .@"clear-ssh-cache" => clear_ssh_cache.Options, .@"edit-config" => edit_config.Options, .@"show-config" => show_config.Options, .@"validate-config" => validate_config.Options, diff --git a/src/cli/clear_ssh_cache.zig b/src/cli/clear_ssh_cache.zig new file mode 100644 index 000000000..062af4221 --- /dev/null +++ b/src/cli/clear_ssh_cache.zig @@ -0,0 +1,40 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const args = @import("args.zig"); +const Action = @import("action.zig").Action; +const ssh_cache = @import("ssh_cache.zig"); + +pub const Options = struct { + pub fn deinit(self: Options) void { + _ = self; + } + + /// Enables `-h` and `--help` to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// Clear the Ghostty SSH terminfo cache. +/// +/// This command removes the cache of hosts where Ghostty's terminfo has been installed +/// via the ssh-terminfo shell integration feature. After clearing, terminfo will be +/// reinstalled on the next SSH connection to previously cached hosts. +/// +/// Use this if you need to force reinstallation of terminfo or clean up old entries. +pub fn run(alloc: Allocator) !u8 { + var opts: Options = .{}; + defer opts.deinit(); + + { + var iter = try args.argsIterator(alloc); + defer iter.deinit(); + try args.parse(Options, alloc, &opts, &iter); + } + + const stdout = std.io.getStdOut().writer(); + try ssh_cache.clearCache(alloc, stdout); + + return 0; +} diff --git a/src/cli/list_ssh_cache.zig b/src/cli/list_ssh_cache.zig new file mode 100644 index 000000000..a799d95bd --- /dev/null +++ b/src/cli/list_ssh_cache.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const args = @import("args.zig"); +const Action = @import("action.zig").Action; +const ssh_cache = @import("ssh_cache.zig"); + +pub const Options = struct { + pub fn deinit(self: Options) void { + _ = self; + } + + /// Enables `-h` and `--help` to work. + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +/// List hosts with Ghostty SSH terminfo installed via the ssh-terminfo shell integration feature. +/// +/// This command shows all remote hosts where Ghostty's terminfo has been successfully +/// installed through the SSH integration. The cache is automatically maintained when +/// connecting to remote hosts with `shell-integration-features = ssh-terminfo` enabled. +pub fn run(alloc: Allocator) !u8 { + var opts: Options = .{}; + defer opts.deinit(); + + { + var iter = try args.argsIterator(alloc); + defer iter.deinit(); + try args.parse(Options, alloc, &opts, &iter); + } + + const stdout = std.io.getStdOut().writer(); + try ssh_cache.listCachedHosts(alloc, stdout); + + return 0; +} diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig new file mode 100644 index 000000000..c3484735a --- /dev/null +++ b/src/cli/ssh_cache.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const Child = std.process.Child; + +/// Get the path to the shared cache script +fn getCacheScriptPath(alloc: Allocator) ![]u8 { + // Use GHOSTTY_RESOURCES_DIR if available, otherwise assume relative path + const resources_dir = std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR") catch { + // Fallback: assume we're running from build directory + return try alloc.dupe(u8, "src/shell-integration/shared/ghostty-ssh-cache"); + }; + defer alloc.free(resources_dir); + + return try std.fs.path.join(alloc, &[_][]const u8{ resources_dir, "shell-integration", "shared", "ghostty-ssh-cache" }); +} + +/// List cached hosts by calling the external script +pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { + const script_path = try getCacheScriptPath(alloc); + defer alloc.free(script_path); + + var child = Child.init(&[_][]const u8{ script_path, "list" }, alloc); + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; + + try child.spawn(); + + const stdout = try child.stdout.?.readToEndAlloc(alloc, std.math.maxInt(usize)); + defer alloc.free(stdout); + + const stderr = try child.stderr.?.readToEndAlloc(alloc, std.math.maxInt(usize)); + defer alloc.free(stderr); + + _ = try child.wait(); + + // Output the results regardless of exit code + try writer.writeAll(stdout); + if (stderr.len > 0) { + try writer.writeAll(stderr); + } + + // Script handles its own success/error messaging, so we don't need to check exit code +} + +/// Clear cache by calling the external script +pub fn clearCache(alloc: Allocator, writer: anytype) !void { + const script_path = try getCacheScriptPath(alloc); + defer alloc.free(script_path); + + var child = Child.init(&[_][]const u8{ script_path, "clear" }, alloc); + child.stdout_behavior = .Pipe; + child.stderr_behavior = .Pipe; + + try child.spawn(); + + const stdout = try child.stdout.?.readToEndAlloc(alloc, std.math.maxInt(usize)); + defer alloc.free(stdout); + + const stderr = try child.stderr.?.readToEndAlloc(alloc, std.math.maxInt(usize)); + defer alloc.free(stderr); + + _ = try child.wait(); + + // Output the results regardless of exit code + try writer.writeAll(stdout); + if (stderr.len > 0) { + try writer.writeAll(stderr); + } + + // Script handles its own success/error messaging, so we don't need to check exit code +} diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 9ba11c845..f07df125e 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -100,19 +100,11 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - # If 'ssh-terminfo' flag is enabled, wrap ghostty to provide cache management commands - ghostty() { - case "$1" in - ssh-cache-list) "$_CACHE" list ;; - ssh-cache-clear) "$_CACHE" clear ;; - *) builtin command ghostty "$@" ;; - esac - } fi # SSH wrapper ssh() { - local e=() o=() c=() # Removed 't' from here + local env=() opts=() ctrl=() # Set up env vars first so terminfo installation inherits them if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then @@ -123,35 +115,35 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then ) for v in "${vars[@]}"; do builtin export "${v?}" - o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") done fi # Install terminfo if needed, reuse control connection for main session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then # Get target (only when needed for terminfo) - builtin local t - t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + builtin local target + target=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - if [[ -n "$t" ]] && "$_CACHE" chk "$t"; then - e+=(TERM=xterm-ghostty) + if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then + env+=(TERM=xterm-ghostty) elif builtin command -v infocmp >/dev/null 2>&1; then - builtin local ti - ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n "$ti" ]]; then + builtin local tinfo + tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 + if [[ -n "$tinfo" ]]; then builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - builtin local cp - cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(builtin echo "$ti" | builtin command ssh "${o[@]}" -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s "$@" ' + builtin local cpath + cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" + case $(builtin echo "$tinfo" | builtin command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL ') in OK) builtin echo "Terminfo setup complete." >&2 - [[ -n "$t" ]] && "$_CACHE" add "$t" - e+=(TERM=xterm-ghostty) - c+=(-o "ControlPath=$cp") + [[ -n "$target" ]] && "$_CACHE" add "$target" + env+=(TERM=xterm-ghostty) + ctrl+=(-o "ControlPath=$cpath") ;; *) builtin echo "Warning: Failed to install terminfo." >&2 ;; esac @@ -163,14 +155,14 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then # Fallback TERM only if terminfo didn't set it if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${e[*]} " =~ " TERM=" ]] && e+=(TERM=xterm-256color) + [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) fi # Execute - if [[ ${#e[@]} -gt 0 ]]; then - env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" + if [[ ${#env[@]} -gt 0 ]]; then + env "${env[@]}" ssh "${opts[@]}" "${ctrl[@]}" "$@" else - builtin command ssh "${o[@]}" "${c[@]}" "$@" + builtin command ssh "${opts[@]}" "${ctrl[@]}" "$@" fi } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 09aa09f31..7048d56b3 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -100,85 +100,68 @@ # SSH Integration use str - use path - use re - if (re:match 'ssh-(env|terminfo)' $E:GHOSTTY_SHELL_FEATURES) { - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - var _cache_script = (path:join $E:GHOSTTY_RESOURCES_DIR shell-integration shared ghostty-ssh-cache) - - # Wrap ghostty command to provide cache management commands - fn ghostty {|@args| - if (eq $args[0] ssh-cache-list) { - (external $_cache_script) list - } elif (eq $args[0] ssh-cache-clear) { - (external $_cache_script) clear - } else { - (external ghostty) $@args - } - } + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - edit:add-var ghostty~ $ghostty~ + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + var _CACHE = $E:GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache } # SSH wrapper fn ssh {|@args| - var e = [] - var o = [] - var c = [] + var env = [] + var opts = [] + var ctrl = [] # Set up env vars first so terminfo installation inherits them - if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { - set-env COLORTERM (or $E:COLORTERM truecolor) - set-env TERM_PROGRAM (or $E:TERM_PROGRAM ghostty) - if (has-env GHOSTTY_VERSION) { - set-env TERM_PROGRAM_VERSION $E:GHOSTTY_VERSION - } - - var vars = [COLORTERM=truecolor TERM_PROGRAM=ghostty] - if (has-env GHOSTTY_VERSION) { + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + var vars = [ + COLORTERM=truecolor + TERM_PROGRAM=ghostty + ] + if (not-eq $E:GHOSTTY_VERSION '') { set vars = [$@vars TERM_PROGRAM_VERSION=$E:GHOSTTY_VERSION] } + for v $vars { - var varname = (str:split &max=2 '=' $v | take 1) - set o = [$@o -o "SendEnv "$varname -o "SetEnv "$v] + set-env (str:split = $v | take 1) (str:split = $v | drop 1 | str:join =) + var varname = (str:split = $v | take 1) + set opts = [$@opts -o 'SendEnv '$varname -o 'SetEnv '$v] } } # Install terminfo if needed, reuse control connection for main session - if (re:match 'ssh-terminfo' $E:GHOSTTY_SHELL_FEATURES) { - # Get target (only when needed for terminfo) - var t = "" + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + # Get target + var target = '' try { - set t = (e:ssh -G $@args 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@' | str:trim-space) - } catch e { - # Ignore errors - } + set target = (e:ssh -G $@args 2>/dev/null | e:awk '/^(user|hostname) /{print $2}' | e:paste -sd'@') + } catch { } - if (and (not-eq $t "") (try { (external $_cache_script) chk $t } catch e { put $false })) { - set e = [$@e TERM=xterm-ghostty] + if (and (not-eq $target '') ($_CACHE chk $target)) { + set env = [$@env TERM=xterm-ghostty] } elif (has-external infocmp) { - var ti = "" + var tinfo = '' try { - set ti = (infocmp -x xterm-ghostty 2>/dev/null | slurp) - } catch e { + set tinfo = (e:infocmp -x xterm-ghostty 2>/dev/null) + } catch { echo "Warning: xterm-ghostty terminfo not found locally." >&2 } - if (not-eq $ti "") { + + if (not-eq $tinfo '') { echo "Setting up Ghostty terminfo on remote host..." >&2 - var cp = "/tmp/ghostty-ssh-"$E:USER"-"(randint 10000)"-"(date +%s | str:trim-space) - var result = (echo $ti | e:ssh $@o -o ControlMaster=yes -o ControlPath=$cp -o ControlPersist=60s $@args ' + var cpath = '/tmp/ghostty-ssh-'$E:USER'-'(randint 0 32767)'-'(date +%s) + var result = (echo $tinfo | e:ssh $@opts -o ControlMaster=yes -o ControlPath=$cpath -o ControlPersist=60s $@args ' infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ' | str:trim-space) + ') + if (eq $result OK) { echo "Terminfo setup complete." >&2 - if (not-eq $t "") { - (external $_cache_script) add $t - } - set e = [$@e TERM=xterm-ghostty] - set c = [$@c -o ControlPath=$cp] + if (not-eq $target '') { $_CACHE add $target } + set env = [$@env TERM=xterm-ghostty] + set ctrl = [$@ctrl -o ControlPath=$cpath] } else { echo "Warning: Failed to install terminfo." >&2 } @@ -189,22 +172,19 @@ } # Fallback TERM only if terminfo didn't set it - if (re:match 'ssh-env' $E:GHOSTTY_SHELL_FEATURES) { - if (and (eq $E:TERM xterm-ghostty) (not (re:match 'TERM=' (str:join ' ' $e)))) { - set e = [$@e TERM=xterm-256color] + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + if (and (eq $E:TERM xterm-ghostty) (not (str:contains (str:join ' ' $env) 'TERM='))) { + set env = [$@env TERM=xterm-256color] } } # Execute - if (> (count $e) 0) { - e:env $@e e:ssh $@o $@c $@args + if (> (count $env) 0) { + e:env $@env e:ssh $@opts $@ctrl $@args } else { - e:ssh $@o $@c $@args + e:ssh $@opts $@ctrl $@args } } - - # Export ssh function for global use - set edit:add-var[ssh] = $ssh~ } defer { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 55ce34985..9b82f5fa2 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -87,89 +87,86 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # SSH Integration - if string match -qr 'ssh-(env|terminfo)' "$GHOSTTY_SHELL_FEATURES" - if string match -qr ssh-terminfo "$GHOSTTY_SHELL_FEATURES" - set -g _cache_script "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" + if string match -qr 'ssh-(env|terminfo)' $GHOSTTY_SHELL_FEATURES - # Wrap ghostty command to provide cache management commands - function ghostty -d "Wrap ghostty to provide cache management commands" - switch "$argv[1]" - case ssh-cache-list - command "$_cache_script" list - case ssh-cache-clear - command "$_cache_script" clear - case "*" - command ghostty $argv - end - end + if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES + set -g _CACHE "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" end # SSH wrapper function ssh - set -l e - set -l o - set -l c + set -l env + set -l opts + set -l ctrl # Set up env vars first so terminfo installation inherits them - if string match -qr ssh-env "$GHOSTTY_SHELL_FEATURES" - set -gx COLORTERM (test -n "$COLORTERM" && echo "$COLORTERM" || echo "truecolor") - set -gx TERM_PROGRAM (test -n "$TERM_PROGRAM" && echo "$TERM_PROGRAM" || echo "ghostty") - test -n "$GHOSTTY_VERSION" && set -gx TERM_PROGRAM_VERSION "$GHOSTTY_VERSION" + if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES + set -l vars \ + COLORTERM=truecolor \ + TERM_PROGRAM=ghostty - set -l vars COLORTERM=truecolor TERM_PROGRAM=ghostty - test -n "$GHOSTTY_VERSION" && set vars $vars "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION" + if test -n "$GHOSTTY_VERSION" + set -a vars "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION" + end for v in $vars - set -l varname (string split -m1 '=' "$v")[1] - set o $o -o "SendEnv $varname" -o "SetEnv $v" + set -l parts (string split = $v) + set -gx $parts[1] $parts[2] + set -a opts -o "SendEnv $parts[1]" -o "SetEnv $v" end end # Install terminfo if needed, reuse control connection for main session - if string match -qr ssh-terminfo "$GHOSTTY_SHELL_FEATURES" - # Get target (only when needed for terminfo) - set -l t (builtin command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES + # Get target + set -l target (command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - if test -n "$t" && command "$_cache_script" chk "$t" - set e $e TERM=xterm-ghostty + if test -n "$target" -a ("$_CACHE" chk "$target") + set -a env TERM=xterm-ghostty else if command -v infocmp >/dev/null 2>&1 - set -l ti - set ti (infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if test -n "$ti" - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - set -l cp "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) - set -l result (builtin echo "$ti" | builtin command ssh $o -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s $argv ' + set -l tinfo (infocmp -x xterm-ghostty 2>/dev/null) + set -l status_code $status + + if test $status_code -ne 0 + echo "Warning: xterm-ghostty terminfo not found locally." >&2 + end + + if test -n "$tinfo" + echo "Setting up Ghostty terminfo on remote host..." >&2 + set -l cpath "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) + set -l result (echo "$tinfo" | command ssh $opts -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s $argv ' infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL ') + switch $result case OK - builtin echo "Terminfo setup complete." >&2 - test -n "$t" && command "$_cache_script" add "$t" - set e $e TERM=xterm-ghostty - set c $c -o "ControlPath=$cp" + echo "Terminfo setup complete." >&2 + test -n "$target" && "$_CACHE" add "$target" + set -a env TERM=xterm-ghostty + set -a ctrl -o "ControlPath=$cpath" case '*' - builtin echo "Warning: Failed to install terminfo." >&2 + echo "Warning: Failed to install terminfo." >&2 end end else - builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 end end # Fallback TERM only if terminfo didn't set it - if string match -qr ssh-env "$GHOSTTY_SHELL_FEATURES" - if test "$TERM" = xterm-ghostty && not string match -q '*TERM=*' "$e" - set e $e TERM=xterm-256color + if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES + if test "$TERM" = xterm-ghostty -a ! (string join ' ' $env | string match -q '*TERM=*') + set -a env TERM=xterm-256color end end # Execute - if test (count $e) -gt 0 - env $e ssh $o $c $argv + if test (count $env) -gt 0 + env $env command ssh $opts $ctrl $argv else - builtin command ssh $o $c $argv + command ssh $opts $ctrl $argv end end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 4305ab61f..4d4461775 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -246,80 +246,76 @@ _ghostty_deferred_init() { # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - readonly _cache_script="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - - # Wrap ghostty command to provide cache management commands - ghostty() { - case "$1" in - ssh-cache-list) "$_cache_script" list ;; - ssh-cache-clear) "$_cache_script" clear ;; - *) builtin command ghostty "$@" ;; - esac - } + readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" fi # SSH wrapper ssh() { - local -a e o c - + local -a env opts ctrl + env=() + opts=() + ctrl=() + # Set up env vars first so terminfo installation inherits them if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local vars=( + local -a vars + vars=( COLORTERM=truecolor TERM_PROGRAM=ghostty ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} ) for v in "${vars[@]}"; do - builtin export "${v?}" - o+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + export "${v?}" + opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") done fi # Install terminfo if needed, reuse control connection for main session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then # Get target (only when needed for terminfo) - builtin local t - t=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + local target + target=$(command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - if [[ -n $t ]] && "$_cache_script" chk "$t"; then - e+=(TERM=xterm-ghostty) - elif builtin command -v infocmp >/dev/null 2>&1; then - local ti - ti=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n $ti ]]; then - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - local cp - cp="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(builtin echo "$ti" | builtin command ssh "${o[@]}" -o ControlMaster=yes -o ControlPath="$cp" -o ControlPersist=60s "$@" ' + if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then + env+=(TERM=xterm-ghostty) + elif command -v infocmp >/dev/null 2>&1; then + local tinfo + tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || echo "Warning: xterm-ghostty terminfo not found locally." >&2 + if [[ -n "$tinfo" ]]; then + echo "Setting up Ghostty terminfo on remote host..." >&2 + local cpath + cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" + case $(echo "$tinfo" | command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL ') in OK) - builtin echo "Terminfo setup complete." >&2 - [[ -n $t ]] && "$_cache_script" add "$t" - e+=(TERM=xterm-ghostty) - c+=(-o "ControlPath=$cp") + echo "Terminfo setup complete." >&2 + [[ -n "$target" ]] && "$_CACHE" add "$target" + env+=(TERM=xterm-ghostty) + ctrl+=(-o "ControlPath=$cpath") ;; - *) builtin echo "Warning: Failed to install terminfo." >&2 ;; + *) echo "Warning: Failed to install terminfo." >&2 ;; esac fi else - builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 fi fi # Fallback TERM only if terminfo didn't set it if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${(j: :)e} " =~ " TERM=" ]] && e+=(TERM=xterm-256color) + [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) fi # Execute - if (( ${#e} > 0 )); then - env "${e[@]}" ssh "${o[@]}" "${c[@]}" "$@" + if [[ ${#env[@]} -gt 0 ]]; then + env "${env[@]}" command ssh "${opts[@]}" "${ctrl[@]}" "$@" else - builtin command ssh "${o[@]}" "${c[@]}" "$@" + command ssh "${opts[@]}" "${ctrl[@]}" "$@" fi } fi From f617c9b3b7125ecad6d4be2341658a180b7a9d65 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 13:09:31 -0700 Subject: [PATCH 31/86] docs: update ssh-terminfo description to reference new CLI actions Updates Config.zig documentation to reflect that SSH cache management is now handled by proper CLI actions (+list-ssh-cache and +clear-ssh-cache) rather than shell wrapper commands. Fixes documentation missed in e8c8a51. --- src/config/Config.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index b03773295..a9df83e0a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1981,8 +1981,9 @@ keybind: Keybinds = .{}, /// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. /// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when /// connecting to hosts that lack it. Requires `infocmp` and `tic` to be available -/// locally. Provides `ghostty ssh-cache-list` and `ghostty ssh-cache-clear` -/// utilities for managing the installation cache. +/// locally. Provides `+list-ssh-cache` and `+clear-ssh-cache` CLI actions for +/// managing the installation cache (caching is otherwise automatic and requires +/// no user intervention). /// /// SSH features work independently and can be combined for optimal experience: /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its From 0ccb7cf3538e189ea25a518c48c721eee243243a Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 14:46:59 -0700 Subject: [PATCH 32/86] docs: improve SSH cache CLI action descriptions - Clarify that +list-ssh-cache shows shell integration cached hosts - Add note about +clear-ssh-cache command and when to use it Addresses mitchellh's feedback on action descriptions. --- src/cli/action.zig | 2 +- src/cli/list_ssh_cache.zig | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/cli/action.zig b/src/cli/action.zig index 1d1c3bfa0..443fafb8a 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -43,7 +43,7 @@ pub const Action = enum { /// List keybind actions @"list-actions", - /// List hosts with Ghostty SSH terminfo installed + /// List hosts cached by SSH shell integration for terminfo installation @"list-ssh-cache", /// Clear Ghostty SSH terminfo cache diff --git a/src/cli/list_ssh_cache.zig b/src/cli/list_ssh_cache.zig index a799d95bd..b4328201d 100644 --- a/src/cli/list_ssh_cache.zig +++ b/src/cli/list_ssh_cache.zig @@ -21,6 +21,9 @@ pub const Options = struct { /// This command shows all remote hosts where Ghostty's terminfo has been successfully /// installed through the SSH integration. The cache is automatically maintained when /// connecting to remote hosts with `shell-integration-features = ssh-terminfo` enabled. +/// +/// Use `+clear-ssh-cache` to remove cached entries if you need to force terminfo +/// reinstallation or clean up stale host entries. pub fn run(alloc: Allocator) !u8 { var opts: Options = .{}; defer opts.deinit(); From 21d95c42c6dc7c94347de91aacae5dece67fb202 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 15:58:13 -0700 Subject: [PATCH 33/86] docs: improve clear-ssh-cache description (missed in previous commit) Clarifies this clears hosts cached by SSH shell integration, completing mitchellh's feedback on both action descriptions. --- src/cli/action.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/action.zig b/src/cli/action.zig index 443fafb8a..db9b54cd3 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -46,7 +46,7 @@ pub const Action = enum { /// List hosts cached by SSH shell integration for terminfo installation @"list-ssh-cache", - /// Clear Ghostty SSH terminfo cache + /// Clear hosts cached by SSH shell integration for terminfo installation @"clear-ssh-cache", /// Edit the config file in the configured terminal editor. From 931efcd1e384df62766957ca9a7609337bd0092e Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 16:15:41 -0700 Subject: [PATCH 34/86] fix: restore background-image config accidentally removed during rebase --- src/config/Config.zig | 339 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 321 insertions(+), 18 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index a9df83e0a..44e7ab3b6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -466,6 +466,93 @@ background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, +/// Background image for the terminal. +/// +/// This should be a path to a PNG or JPEG file, other image formats are +/// not yet supported. +/// +/// The background image is currently per-terminal, not per-window. If +/// you are a heavy split user, the background image will be repeated across +/// splits. A future improvement to Ghostty will address this. +/// +/// WARNING: Background images are currently duplicated in VRAM per-terminal. +/// For sufficiently large images, this could lead to a large increase in +/// memory usage (specifically VRAM usage). A future Ghostty improvement +/// will resolve this by sharing image textures across terminals. +@"background-image": ?Path = null, + +/// Background image opacity. +/// +/// This is relative to the value of `background-opacity`. +/// +/// A value of `1.0` (the default) will result in the background image being +/// placed on top of the general background color, and then the combined result +/// will be adjusted to the opacity specified by `background-opacity`. +/// +/// A value less than `1.0` will result in the background image being mixed +/// with the general background color before the combined result is adjusted +/// to the configured `background-opacity`. +/// +/// A value greater than `1.0` will result in the background image having a +/// higher opacity than the general background color. For instance, if the +/// configured `background-opacity` is `0.5` and `background-image-opacity` +/// is set to `1.5`, then the final opacity of the background image will be +/// `0.5 * 1.5 = 0.75`. +@"background-image-opacity": f32 = 1.0, + +/// Background image position. +/// +/// Valid values are: +/// * `top-left` +/// * `top-center` +/// * `top-right` +/// * `center-left` +/// * `center` +/// * `center-right` +/// * `bottom-left` +/// * `bottom-center` +/// * `bottom-right` +/// +/// The default value is `center`. +@"background-image-position": BackgroundImagePosition = .center, + +/// Background image fit. +/// +/// Valid values are: +/// +/// * `contain` +/// +/// Preserving the aspect ratio, scale the background image to the largest +/// size that can still be contained within the terminal, so that the whole +/// image is visible. +/// +/// * `cover` +/// +/// Preserving the aspect ratio, scale the background image to the smallest +/// size that can completely cover the terminal. This may result in one or +/// more edges of the image being clipped by the edge of the terminal. +/// +/// * `stretch` +/// +/// Stretch the background image to the full size of the terminal, without +/// preserving the aspect ratio. +/// +/// * `none` +/// +/// Don't scale the background image. +/// +/// The default value is `contain`. +@"background-image-fit": BackgroundImageFit = .contain, + +/// Whether to repeat the background image or not. +/// +/// If this is set to true, the background image will be repeated if there +/// would otherwise be blank space around it because it doesn't completely +/// fill the terminal area. +/// +/// The default value is `false`. +@"background-image-repeat": bool = false, + /// The foreground and background color for selection. If this is not set, then /// the selection color is just the inverted window background and foreground /// (note: not to be confused with the cell bg/fg). @@ -1964,6 +2051,8 @@ keybind: Keybinds = .{}, /// its default value is used, so you must explicitly disable features you don't /// want. You can also use `true` or `false` to turn all features on or off. /// +/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` +/// /// Available features: /// /// * `cursor` - Set the cursor to a blinking bar at the prompt. @@ -1989,10 +2078,30 @@ keybind: Keybinds = .{}, /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its /// terminfo on remote hosts and use `xterm-ghostty` as TERM, falling back to /// `xterm-256color` with environment variables if terminfo installation fails. -/// -/// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` @"shell-integration-features": ShellIntegrationFeatures = .{}, +/// Custom entries into the command palette. +/// +/// Each entry requires the title, the corresponding action, and an optional +/// description. Each field should be prefixed with the field name, a colon +/// (`:`), and then the specified value. The syntax for actions is identical +/// to the one for keybind actions. Whitespace in between fields is ignored. +/// +/// ```ini +/// command-palette-entry = title:Reset Font Style, action:csi:0m +/// command-palette-entry = title:Crash on Main Thread,description:Causes a crash on the main (UI) thread.,action:crash:main +/// ``` +/// +/// By default, the command palette is preloaded with most actions that might +/// be useful in an interactive setting yet do not have easily accessible or +/// memorizable shortcuts. The default entries can be cleared by setting this +/// setting to an empty value: +/// +/// ```ini +/// command-palette-entry = +/// ``` +@"command-palette-entry": RepeatableCommand = .{}, + /// Sets the reporting format for OSC sequences that request color information. /// Ghostty currently supports OSC 10 (foreground), OSC 11 (background), and /// OSC 4 (256 color palette) queries, and by default the reported values @@ -2802,6 +2911,9 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // Add our default keybindings try result.keybind.init(alloc); + // Add our default command palette entries + try result.@"command-palette-entry".init(alloc); + // Add our default link for URL detection try result.link.links.append(alloc, .{ .regex = url.regex, @@ -3316,6 +3428,15 @@ fn expandPaths(self: *Config, base: []const u8) !void { &self._diagnostics, ); }, + ?RepeatablePath, ?Path => { + if (@field(self, field.name)) |*path| { + try path.expand( + arena_alloc, + base, + &self._diagnostics, + ); + } + }, else => {}, } } @@ -4980,25 +5101,29 @@ pub const Keybinds = struct { .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_tab = {} }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .next_tab = {} }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } }, .{ .previous_tab = {} }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, + .{ .performable = true }, ); try self.set.put( alloc, @@ -5010,57 +5135,67 @@ pub const Keybinds = struct { .{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .down }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .up }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .down }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .left }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .right }, + .{ .performable = true }, ); // Resizing splits - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .up, 10 } }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .down, 10 } }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .left, 10 } }, + .{ .performable = true }, ); - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .right, 10 } }, + .{ .performable = true }, ); // Viewport scrolling @@ -5131,22 +5266,24 @@ pub const Keybinds = struct { const end: u21 = '8'; var i: u21 = start; while (i <= end) : (i += 1) { - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .unicode = i }, .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, + .{ .performable = true }, ); } - try self.set.put( + try self.set.putFlags( alloc, .{ .key = .{ .unicode = '9' }, .mods = mods, }, .{ .last_tab = {} }, + .{ .performable = true }, ); } @@ -6118,6 +6255,150 @@ pub const ShellIntegrationFeatures = packed struct { @"ssh-terminfo": bool = false, }; +pub const RepeatableCommand = struct { + value: std.ArrayListUnmanaged(inputpkg.Command) = .empty, + + pub fn init(self: *RepeatableCommand, alloc: Allocator) !void { + self.value = .empty; + try self.value.appendSlice(alloc, inputpkg.command.defaults); + } + + pub fn parseCLI( + self: *RepeatableCommand, + alloc: Allocator, + input_: ?[]const u8, + ) !void { + // Unset or empty input clears the list + const input = input_ orelse ""; + if (input.len == 0) { + self.value.clearRetainingCapacity(); + return; + } + + const cmd = try cli.args.parseAutoStruct( + inputpkg.Command, + alloc, + input, + ); + try self.value.append(alloc, cmd); + } + + /// Deep copy of the struct. Required by Config. + pub fn clone(self: *const RepeatableCommand, alloc: Allocator) Allocator.Error!RepeatableCommand { + const value = try self.value.clone(alloc); + for (value.items) |*item| { + item.* = try item.clone(alloc); + } + + return .{ .value = value }; + } + + /// Compare if two of our value are equal. Required by Config. + pub fn equal(self: RepeatableCommand, other: RepeatableCommand) bool { + if (self.value.items.len != other.value.items.len) return false; + for (self.value.items, other.value.items) |a, b| { + if (!a.equal(b)) return false; + } + + return true; + } + + /// Used by Formatter + pub fn formatEntry(self: RepeatableCommand, formatter: anytype) !void { + if (self.value.items.len == 0) { + try formatter.formatEntry(void, {}); + return; + } + + var buf: [4096]u8 = undefined; + for (self.value.items) |item| { + const str = if (item.description.len > 0) std.fmt.bufPrint( + &buf, + "title:{s},description:{s},action:{}", + .{ item.title, item.description, item.action }, + ) else std.fmt.bufPrint( + &buf, + "title:{s},action:{}", + .{ item.title, item.action }, + ); + try formatter.formatEntry([]const u8, str catch return error.OutOfMemory); + } + } + + test "RepeatableCommand parseCLI" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Foo,action:ignore"); + try list.parseCLI(alloc, "title:Bar,description:bobr,action:text:ale bydle"); + try list.parseCLI(alloc, "title:Quux,description:boo,action:increase_font_size:2.5"); + + try testing.expectEqual(@as(usize, 3), list.value.items.len); + + try testing.expectEqual(inputpkg.Binding.Action.ignore, list.value.items[0].action); + try testing.expectEqualStrings("Foo", list.value.items[0].title); + + try testing.expect(list.value.items[1].action == .text); + try testing.expectEqualStrings("ale bydle", list.value.items[1].action.text); + try testing.expectEqualStrings("Bar", list.value.items[1].title); + try testing.expectEqualStrings("bobr", list.value.items[1].description); + + try testing.expectEqual( + inputpkg.Binding.Action{ .increase_font_size = 2.5 }, + list.value.items[2].action, + ); + try testing.expectEqualStrings("Quux", list.value.items[2].title); + try testing.expectEqualStrings("boo", list.value.items[2].description); + + try list.parseCLI(alloc, ""); + try testing.expectEqual(@as(usize, 0), list.value.items.len); + } + + test "RepeatableCommand formatConfig empty" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var list: RepeatableCommand = .{}; + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = \n", buf.items); + } + + test "RepeatableCommand formatConfig single item" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Bobr, action:text:Bober"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:Bober\n", buf.items); + } + + test "RepeatableCommand formatConfig multiple items" { + const testing = std.testing; + var buf = std.ArrayList(u8).init(testing.allocator); + defer buf.deinit(); + + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: RepeatableCommand = .{}; + try list.parseCLI(alloc, "title:Bobr, action:text:kurwa"); + try list.parseCLI(alloc, "title:Ja, description: pierdole, action:text:jakie bydle"); + try list.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = title:Bobr,action:text:kurwa\na = title:Ja,description:pierdole,action:text:jakie bydle\n", buf.items); + } +}; + /// OSC 4, 10, 11, and 12 default color reporting format. pub const OSCColorReportFormat = enum { none, @@ -6573,6 +6854,28 @@ pub const AlphaBlending = enum { } }; +/// See background-image-position +pub const BackgroundImagePosition = enum { + @"top-left", + @"top-center", + @"top-right", + @"center-left", + @"center-center", + @"center-right", + @"bottom-left", + @"bottom-center", + @"bottom-right", + center, +}; + +/// See background-image-fit +pub const BackgroundImageFit = enum { + contain, + cover, + stretch, + none, +}; + /// See freetype-load-flag pub const FreetypeLoadFlags = packed struct { // The defaults here at the time of writing this match the defaults From 59229d76816d11d2e651b55401504fad7d26e4f5 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 16:36:08 -0700 Subject: [PATCH 35/86] style: revert fish_indent quote removal forgot to disable autoformat for this buffer (again) --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 9b82f5fa2..083087bd5 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -63,14 +63,14 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias - if contains sudo $features; and test -n "$TERMINFO"; and test file = (type -t sudo 2> /dev/null; or echo "x") + if contains sudo $features; and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") # Wrap `sudo` command to ensure Ghostty terminfo is preserved function sudo -d "Wrap sudo to preserve terminfo" - set --function sudo_has_sudoedit_flags no + set --function sudo_has_sudoedit_flags "no" for arg in $argv # Check if argument is '-e' or '--edit' (sudoedit flags) - if string match -q -- -e "$arg"; or string match -q -- --edit "$arg" - set --function sudo_has_sudoedit_flags yes + if string match -q -- "-e" "$arg"; or string match -q -- "--edit" "$arg" + set --function sudo_has_sudoedit_flags "yes" break end # Check if argument is neither an option nor a key-value pair @@ -78,7 +78,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" break end end - if test "$sudo_has_sudoedit_flags" = yes + if test "$sudo_has_sudoedit_flags" = "yes" command sudo $argv else command sudo TERMINFO="$TERMINFO" $argv From e5e2a56c983d676291a37c65dc9d82484d9b1b1b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 16:47:17 -0700 Subject: [PATCH 36/86] fix: use imported modules consistently in action dispatch --- src/cli/action.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/action.zig b/src/cli/action.zig index db9b54cd3..9d1fad027 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -163,8 +163,8 @@ pub const Action = enum { .@"list-themes" => try list_themes.run(alloc), .@"list-colors" => try list_colors.run(alloc), .@"list-actions" => try list_actions.run(alloc), - .@"list-ssh-cache" => @import("list_ssh_cache.zig").run(alloc), - .@"clear-ssh-cache" => @import("clear_ssh_cache.zig").run(alloc), + .@"list-ssh-cache" => try list_ssh_cache.run(alloc), + .@"clear-ssh-cache" => try clear_ssh_cache.run(alloc), .@"edit-config" => try edit_config.run(alloc), .@"show-config" => try show_config.run(alloc), .@"validate-config" => try validate_config.run(alloc), From 1873add697ac9a4b9b5b73d1d6225689eebfa071 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 17:00:17 -0700 Subject: [PATCH 37/86] docs: call out bash dependency --- src/shell-integration/README.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index c09ea34db..36a6f9de1 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -93,12 +93,18 @@ fi The `shared/` directory contains utilities available to all shell integrations: -- `ghostty-ssh-cache`: A standalone script that manages the SSH terminfo host - cache for the `ssh-terminfo` shell integration feature. This script handles - cache file operations (list, clear, check, add) and is called by all shell - integrations when `ssh-terminfo` is enabled. It is also called by the - `+list-ssh-cache` and `+clear-ssh-cache` CLI actions, providing users with - direct cache management capabilities. +### ghostty-ssh-cache + +> [!NOTE] +> +> This script requires `bash` to be available in the system PATH. + +This is a standalone script that manages the SSH terminfo host cache for the +`ssh-terminfo` shell integration feature. This script handles cache file +operations (list, clear, check, add) and is called by all shell integrations +when `ssh-terminfo` is enabled. It is also called by the `+list-ssh-cache` +and `+clear-ssh-cache` CLI actions, providing users with direct cache +management capabilities. The shared approach maintains separation of concerns by keeping shell-specific integration files independent of secondary logic. From eed2006b4d7110b935c14f3fcb7ba5679aa010e5 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 17:47:01 -0700 Subject: [PATCH 38/86] fix: correct resources directory fallback path and eliminate code duplication in ssh_cache - Fix fallback path from full path to "src" since full path is built later - Extract duplicate code from listCachedHosts and clearCache into runCacheCommand helper - Addresses feedback from @00-kat --- src/cli/ssh_cache.zig | 38 +++++++++----------------------------- 1 file changed, 9 insertions(+), 29 deletions(-) diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index c3484735a..02462816c 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -7,19 +7,19 @@ fn getCacheScriptPath(alloc: Allocator) ![]u8 { // Use GHOSTTY_RESOURCES_DIR if available, otherwise assume relative path const resources_dir = std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR") catch { // Fallback: assume we're running from build directory - return try alloc.dupe(u8, "src/shell-integration/shared/ghostty-ssh-cache"); + return try alloc.dupe(u8, "src"); }; defer alloc.free(resources_dir); return try std.fs.path.join(alloc, &[_][]const u8{ resources_dir, "shell-integration", "shared", "ghostty-ssh-cache" }); } -/// List cached hosts by calling the external script -pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { +/// Generic function to run cache script commands +fn runCacheCommand(alloc: Allocator, writer: anytype, command: []const u8) !void { const script_path = try getCacheScriptPath(alloc); defer alloc.free(script_path); - var child = Child.init(&[_][]const u8{ script_path, "list" }, alloc); + var child = Child.init(&[_][]const u8{ script_path, command }, alloc); child.stdout_behavior = .Pipe; child.stderr_behavior = .Pipe; @@ -38,34 +38,14 @@ pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { if (stderr.len > 0) { try writer.writeAll(stderr); } +} - // Script handles its own success/error messaging, so we don't need to check exit code +/// List cached hosts by calling the external script +pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { + try runCacheCommand(alloc, writer, "list"); } /// Clear cache by calling the external script pub fn clearCache(alloc: Allocator, writer: anytype) !void { - const script_path = try getCacheScriptPath(alloc); - defer alloc.free(script_path); - - var child = Child.init(&[_][]const u8{ script_path, "clear" }, alloc); - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; - - try child.spawn(); - - const stdout = try child.stdout.?.readToEndAlloc(alloc, std.math.maxInt(usize)); - defer alloc.free(stdout); - - const stderr = try child.stderr.?.readToEndAlloc(alloc, std.math.maxInt(usize)); - defer alloc.free(stderr); - - _ = try child.wait(); - - // Output the results regardless of exit code - try writer.writeAll(stdout); - if (stderr.len > 0) { - try writer.writeAll(stderr); - } - - // Script handles its own success/error messaging, so we don't need to check exit code + try runCacheCommand(alloc, writer, "clear"); } From b5372468e46d0aee5ec42c61c567d67c1a0c5ab6 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 17:47:26 -0700 Subject: [PATCH 39/86] docs: clarify infocmp/tic requirements for ssh-terminfo feature infocmp is required locally to extract terminfo, tic is required on remote hosts to install it --- src/config/Config.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 44e7ab3b6..0f9efd34d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2069,10 +2069,10 @@ keybind: Keybinds = .{}, /// /// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. /// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when -/// connecting to hosts that lack it. Requires `infocmp` and `tic` to be available -/// locally. Provides `+list-ssh-cache` and `+clear-ssh-cache` CLI actions for -/// managing the installation cache (caching is otherwise automatic and requires -/// no user intervention). +/// connecting to hosts that lack it. Requires `infocmp` to be available locally +/// and `tic` to be available on remote hosts. Provides `+list-ssh-cache` and +/// `+clear-ssh-cache` CLI actions for managing the installation cache (caching +/// is otherwise automatic and requires no user intervention). /// /// SSH features work independently and can be combined for optimal experience: /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its From 076f742dd4da1b1a90c98d237977c356a2c7f079 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Wed, 25 Jun 2025 17:50:15 -0700 Subject: [PATCH 40/86] fix: replace non-existent GHOSTTY_VERSION with TERM_PROGRAM_VERSION in shell integration GHOSTTY_VERSION was mistakenly referenced but is never set. Use TERM_PROGRAM_VERSION which is actually provided by Exec.zig from build_config.version_string. --- src/shell-integration/bash/ghostty.bash | 2 +- .../elvish/lib/ghostty-integration.elv | 10 +++++----- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 4 ++-- src/shell-integration/zsh/ghostty-integration | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index f07df125e..b51dae9c7 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -111,7 +111,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then local vars=( COLORTERM=truecolor TERM_PROGRAM=ghostty - ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + ${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} ) for v in "${vars[@]}"; do builtin export "${v?}" diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 7048d56b3..084861434 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -119,10 +119,10 @@ COLORTERM=truecolor TERM_PROGRAM=ghostty ] - if (not-eq $E:GHOSTTY_VERSION '') { - set vars = [$@vars TERM_PROGRAM_VERSION=$E:GHOSTTY_VERSION] + if (not-eq $E:TERM_PROGRAM_VERSION '') { + set vars = [$@vars TERM_PROGRAM_VERSION=$E:TERM_PROGRAM_VERSION] } - + for v $vars { set-env (str:split = $v | take 1) (str:split = $v | drop 1 | str:join =) var varname = (str:split = $v | take 1) @@ -147,7 +147,7 @@ } catch { echo "Warning: xterm-ghostty terminfo not found locally." >&2 } - + if (not-eq $tinfo '') { echo "Setting up Ghostty terminfo on remote host..." >&2 var cpath = '/tmp/ghostty-ssh-'$E:USER'-'(randint 0 32767)'-'(date +%s) @@ -156,7 +156,7 @@ command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL ') - + if (eq $result OK) { echo "Terminfo setup complete." >&2 if (not-eq $target '') { $_CACHE add $target } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 083087bd5..4c780b5a7 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -105,8 +105,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" COLORTERM=truecolor \ TERM_PROGRAM=ghostty - if test -n "$GHOSTTY_VERSION" - set -a vars "TERM_PROGRAM_VERSION=$GHOSTTY_VERSION" + if test -n "$TERM_PROGRAM_VERSION" + set -a vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" end for v in $vars diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 4d4461775..48f8cc934 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -264,7 +264,7 @@ _ghostty_deferred_init() { vars=( COLORTERM=truecolor TERM_PROGRAM=ghostty - ${GHOSTTY_VERSION:+TERM_PROGRAM_VERSION=$GHOSTTY_VERSION} + ${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} ) for v in "${vars[@]}"; do export "${v?}" @@ -277,7 +277,7 @@ _ghostty_deferred_init() { # Get target (only when needed for terminfo) local target target=$(command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - + if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then env+=(TERM=xterm-ghostty) elif command -v infocmp >/dev/null 2>&1; then From e25aa9f424097938362a9d6b02fa32c1714ab97f Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 1 Jul 2025 17:52:36 -0700 Subject: [PATCH 41/86] docs: update to reflect changes after porting terminfo host caching to Zig - Minor tweak to Config.zig to show the new action. - Rolled back README.md to remove reference to the now non-existent 'shared' subdir and bash-based cache script. --- src/config/Config.zig | 7 ++++--- src/shell-integration/README.md | 20 -------------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 0f9efd34d..dee2fe10a 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2070,9 +2070,10 @@ keybind: Keybinds = .{}, /// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. /// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when /// connecting to hosts that lack it. Requires `infocmp` to be available locally -/// and `tic` to be available on remote hosts. Provides `+list-ssh-cache` and -/// `+clear-ssh-cache` CLI actions for managing the installation cache (caching -/// is otherwise automatic and requires no user intervention). +/// and `tic` to be available on remote hosts. Once terminfo is installed on a +/// remote host, it will be automatically "cached" to avoid repeat installations. +/// If desired, the `+ssh-cache` CLI action can be used to manage the installation +/// cache manually using various arguments. /// /// SSH features work independently and can be combined for optimal experience: /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its diff --git a/src/shell-integration/README.md b/src/shell-integration/README.md index 36a6f9de1..1fd11091d 100644 --- a/src/shell-integration/README.md +++ b/src/shell-integration/README.md @@ -88,23 +88,3 @@ if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration fi ``` - -## Shared Resources - -The `shared/` directory contains utilities available to all shell integrations: - -### ghostty-ssh-cache - -> [!NOTE] -> -> This script requires `bash` to be available in the system PATH. - -This is a standalone script that manages the SSH terminfo host cache for the -`ssh-terminfo` shell integration feature. This script handles cache file -operations (list, clear, check, add) and is called by all shell integrations -when `ssh-terminfo` is enabled. It is also called by the `+list-ssh-cache` -and `+clear-ssh-cache` CLI actions, providing users with direct cache -management capabilities. - -The shared approach maintains separation of concerns by keeping shell-specific -integration files independent of secondary logic. From 75c703071a6ab176f2d7982a17cde1e593e14737 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Thu, 3 Jul 2025 20:11:45 -0700 Subject: [PATCH 42/86] feat(ssh): rewrite SSH cache system in native Zig - Eliminates standalone bash dependency - Consolidates `+list-ssh-cache` and `+clear-ssh-cache` actions into single `+ssh-cache` action with args - Structured cache format with timestamps and expiration support - Memory-safe entry handling with proper file locking - Comprehensive hostname validation (IPv4/IPv6/domains) - Atomic updates via temp file + rename - Updated shell integrations for improved cross-platform support and reliability - Cache operations are now unit-testable --- src/cli/action.zig | 16 +- src/cli/clear_ssh_cache.zig | 40 - src/cli/list_ssh_cache.zig | 41 - src/cli/ssh_cache.zig | 769 +++++++++++++++++- src/shell-integration/bash/ghostty.bash | 197 +++-- .../elvish/lib/ghostty-integration.elv | 297 +++++-- .../ghostty-shell-integration.fish | 213 +++-- .../shared/ghostty-ssh-cache | 11 - src/shell-integration/zsh/ghostty-integration | 194 +++-- 9 files changed, 1401 insertions(+), 377 deletions(-) delete mode 100644 src/cli/clear_ssh_cache.zig delete mode 100644 src/cli/list_ssh_cache.zig delete mode 100755 src/shell-integration/shared/ghostty-ssh-cache diff --git a/src/cli/action.zig b/src/cli/action.zig index 9d1fad027..728f36efe 100644 --- a/src/cli/action.zig +++ b/src/cli/action.zig @@ -9,8 +9,7 @@ const list_keybinds = @import("list_keybinds.zig"); const list_themes = @import("list_themes.zig"); const list_colors = @import("list_colors.zig"); const list_actions = @import("list_actions.zig"); -const list_ssh_cache = @import("list_ssh_cache.zig"); -const clear_ssh_cache = @import("clear_ssh_cache.zig"); +const ssh_cache = @import("ssh_cache.zig"); const edit_config = @import("edit_config.zig"); const show_config = @import("show_config.zig"); const validate_config = @import("validate_config.zig"); @@ -43,11 +42,8 @@ pub const Action = enum { /// List keybind actions @"list-actions", - /// List hosts cached by SSH shell integration for terminfo installation - @"list-ssh-cache", - - /// Clear hosts cached by SSH shell integration for terminfo installation - @"clear-ssh-cache", + /// Manage SSH terminfo cache for automatic remote host setup + @"ssh-cache", /// Edit the config file in the configured terminal editor. @"edit-config", @@ -163,8 +159,7 @@ pub const Action = enum { .@"list-themes" => try list_themes.run(alloc), .@"list-colors" => try list_colors.run(alloc), .@"list-actions" => try list_actions.run(alloc), - .@"list-ssh-cache" => try list_ssh_cache.run(alloc), - .@"clear-ssh-cache" => try clear_ssh_cache.run(alloc), + .@"ssh-cache" => try ssh_cache.run(alloc), .@"edit-config" => try edit_config.run(alloc), .@"show-config" => try show_config.run(alloc), .@"validate-config" => try validate_config.run(alloc), @@ -202,8 +197,7 @@ pub const Action = enum { .@"list-themes" => list_themes.Options, .@"list-colors" => list_colors.Options, .@"list-actions" => list_actions.Options, - .@"list-ssh-cache" => list_ssh_cache.Options, - .@"clear-ssh-cache" => clear_ssh_cache.Options, + .@"ssh-cache" => ssh_cache.Options, .@"edit-config" => edit_config.Options, .@"show-config" => show_config.Options, .@"validate-config" => validate_config.Options, diff --git a/src/cli/clear_ssh_cache.zig b/src/cli/clear_ssh_cache.zig deleted file mode 100644 index 062af4221..000000000 --- a/src/cli/clear_ssh_cache.zig +++ /dev/null @@ -1,40 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const args = @import("args.zig"); -const Action = @import("action.zig").Action; -const ssh_cache = @import("ssh_cache.zig"); - -pub const Options = struct { - pub fn deinit(self: Options) void { - _ = self; - } - - /// Enables `-h` and `--help` to work. - pub fn help(self: Options) !void { - _ = self; - return Action.help_error; - } -}; - -/// Clear the Ghostty SSH terminfo cache. -/// -/// This command removes the cache of hosts where Ghostty's terminfo has been installed -/// via the ssh-terminfo shell integration feature. After clearing, terminfo will be -/// reinstalled on the next SSH connection to previously cached hosts. -/// -/// Use this if you need to force reinstallation of terminfo or clean up old entries. -pub fn run(alloc: Allocator) !u8 { - var opts: Options = .{}; - defer opts.deinit(); - - { - var iter = try args.argsIterator(alloc); - defer iter.deinit(); - try args.parse(Options, alloc, &opts, &iter); - } - - const stdout = std.io.getStdOut().writer(); - try ssh_cache.clearCache(alloc, stdout); - - return 0; -} diff --git a/src/cli/list_ssh_cache.zig b/src/cli/list_ssh_cache.zig deleted file mode 100644 index b4328201d..000000000 --- a/src/cli/list_ssh_cache.zig +++ /dev/null @@ -1,41 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const args = @import("args.zig"); -const Action = @import("action.zig").Action; -const ssh_cache = @import("ssh_cache.zig"); - -pub const Options = struct { - pub fn deinit(self: Options) void { - _ = self; - } - - /// Enables `-h` and `--help` to work. - pub fn help(self: Options) !void { - _ = self; - return Action.help_error; - } -}; - -/// List hosts with Ghostty SSH terminfo installed via the ssh-terminfo shell integration feature. -/// -/// This command shows all remote hosts where Ghostty's terminfo has been successfully -/// installed through the SSH integration. The cache is automatically maintained when -/// connecting to remote hosts with `shell-integration-features = ssh-terminfo` enabled. -/// -/// Use `+clear-ssh-cache` to remove cached entries if you need to force terminfo -/// reinstallation or clean up stale host entries. -pub fn run(alloc: Allocator) !u8 { - var opts: Options = .{}; - defer opts.deinit(); - - { - var iter = try args.argsIterator(alloc); - defer iter.deinit(); - try args.parse(Options, alloc, &opts, &iter); - } - - const stdout = std.io.getStdOut().writer(); - try ssh_cache.listCachedHosts(alloc, stdout); - - return 0; -} diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 02462816c..71c47a7a7 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -1,51 +1,754 @@ const std = @import("std"); +const fs = std.fs; const Allocator = std.mem.Allocator; -const Child = std.process.Child; +const xdg = @import("../os/xdg.zig"); +const args = @import("args.zig"); +const Action = @import("action.zig").Action; -/// Get the path to the shared cache script -fn getCacheScriptPath(alloc: Allocator) ![]u8 { - // Use GHOSTTY_RESOURCES_DIR if available, otherwise assume relative path - const resources_dir = std.process.getEnvVarOwned(alloc, "GHOSTTY_RESOURCES_DIR") catch { - // Fallback: assume we're running from build directory - return try alloc.dupe(u8, "src"); - }; - defer alloc.free(resources_dir); +pub const CacheError = error{ + InvalidCacheKey, + CacheLocked, +} || fs.File.OpenError || fs.File.WriteError || Allocator.Error; - return try std.fs.path.join(alloc, &[_][]const u8{ resources_dir, "shell-integration", "shared", "ghostty-ssh-cache" }); +const MAX_CACHE_SIZE = 512 * 1024; // 512KB - sufficient for approximately 10k entries +const NEVER_EXPIRE = 0; +const SECONDS_PER_DAY = 86400; + +pub const Options = struct { + clear: bool = false, + add: ?[]const u8 = null, + remove: ?[]const u8 = null, + host: ?[]const u8 = null, + @"expire-days": u32 = NEVER_EXPIRE, + + pub fn deinit(self: *Options) void { + _ = self; + } + + pub fn help(self: Options) !void { + _ = self; + return Action.help_error; + } +}; + +const CacheEntry = struct { + hostname: []const u8, + timestamp: i64, + terminfo_version: []const u8, + + fn parse(line: []const u8) ?CacheEntry { + const trimmed = std.mem.trim(u8, line, " \t\r\n"); + if (trimmed.len == 0) return null; + + // Parse format: hostname|timestamp|terminfo_version + var iter = std.mem.tokenizeScalar(u8, trimmed, '|'); + const hostname = iter.next() orelse return null; + const timestamp_str = iter.next() orelse return null; + const terminfo_version = iter.next() orelse "xterm-ghostty"; + + const timestamp = std.fmt.parseInt(i64, timestamp_str, 10) catch |err| { + std.log.warn("Invalid timestamp in cache entry: {s} err={}", .{ timestamp_str, err }); + return null; + }; + + return CacheEntry{ + .hostname = hostname, + .timestamp = timestamp, + .terminfo_version = terminfo_version, + }; + } + + fn format(self: CacheEntry, writer: anytype) !void { + try writer.print("{s}|{d}|{s}\n", .{ self.hostname, self.timestamp, self.terminfo_version }); + } + + fn isExpired(self: CacheEntry, expire_days: u32) bool { + if (expire_days == NEVER_EXPIRE) return false; + const now = std.time.timestamp(); + const age_days = @divTrunc(now - self.timestamp, SECONDS_PER_DAY); + return age_days > expire_days; + } +}; + +const AddResult = enum { + added, + updated, +}; + +fn getCachePath(allocator: Allocator) ![]const u8 { + const state_dir = try xdg.state(allocator, .{ .subdir = "ghostty" }); + defer allocator.free(state_dir); + return try std.fs.path.join(allocator, &.{ state_dir, "ssh_cache" }); } -/// Generic function to run cache script commands -fn runCacheCommand(alloc: Allocator, writer: anytype, command: []const u8) !void { - const script_path = try getCacheScriptPath(alloc); - defer alloc.free(script_path); +// Supports both standalone hostnames and user@hostname format +fn isValidCacheKey(key: []const u8) bool { + if (key.len == 0 or key.len > 320) return false; // 253 + 1 + 64 for user@hostname - var child = Child.init(&[_][]const u8{ script_path, command }, alloc); - child.stdout_behavior = .Pipe; - child.stderr_behavior = .Pipe; + // Check for user@hostname format + if (std.mem.indexOf(u8, key, "@")) |at_pos| { + const user = key[0..at_pos]; + const hostname = key[at_pos + 1 ..]; + return isValidUser(user) and isValidHostname(hostname); + } - try child.spawn(); + return isValidHostname(key); +} - const stdout = try child.stdout.?.readToEndAlloc(alloc, std.math.maxInt(usize)); - defer alloc.free(stdout); +// Basic hostname validation - accepts domains and IPs (including IPv6 in brackets) +fn isValidHostname(host: []const u8) bool { + if (host.len == 0 or host.len > 253) return false; - const stderr = try child.stderr.?.readToEndAlloc(alloc, std.math.maxInt(usize)); - defer alloc.free(stderr); + // Handle IPv6 addresses in brackets + if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') { + const ipv6_part = host[1 .. host.len - 1]; + if (ipv6_part.len == 0) return false; + var has_colon = false; + for (ipv6_part) |c| { + switch (c) { + 'a'...'f', 'A'...'F', '0'...'9', ':' => { + if (c == ':') has_colon = true; + }, + else => return false, + } + } + return has_colon; + } - _ = try child.wait(); + // Standard hostname/domain validation + for (host) |c| { + switch (c) { + 'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {}, + else => return false, + } + } - // Output the results regardless of exit code - try writer.writeAll(stdout); - if (stderr.len > 0) { - try writer.writeAll(stderr); + // No leading/trailing dots or hyphens, no consecutive dots + if (host[0] == '.' or host[0] == '-' or + host[host.len - 1] == '.' or host[host.len - 1] == '-') + { + return false; + } + + return std.mem.indexOf(u8, host, "..") == null; +} + +fn isValidUser(user: []const u8) bool { + if (user.len == 0 or user.len > 64) return false; + for (user) |c| { + switch (c) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-', '.' => {}, + else => return false, + } + } + return true; +} + +fn acquireFileLock(file: fs.File) CacheError!void { + _ = file.tryLock(.exclusive) catch { + return CacheError.CacheLocked; + }; +} + +fn readCacheFile( + alloc: Allocator, + path: []const u8, + entries: *std.StringHashMap(CacheEntry), +) !void { + const file = fs.openFileAbsolute(path, .{}) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; + defer file.close(); + + const content = try file.readToEndAlloc(alloc, MAX_CACHE_SIZE); + defer alloc.free(content); + + var lines = std.mem.tokenizeScalar(u8, content, '\n'); + + while (lines.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r"); + + if (CacheEntry.parse(trimmed)) |entry| { + // Always allocate hostname first to avoid key pointer confusion + const hostname_copy = try alloc.dupe(u8, entry.hostname); + errdefer alloc.free(hostname_copy); + + const gop = try entries.getOrPut(hostname_copy); + if (!gop.found_existing) { + const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); + gop.value_ptr.* = CacheEntry{ + .hostname = hostname_copy, + .timestamp = entry.timestamp, + .terminfo_version = terminfo_copy, + }; + } else { + // Don't need the copy since entry already exists + alloc.free(hostname_copy); + + // Handle duplicate entries - keep newer timestamp + if (entry.timestamp > gop.value_ptr.timestamp) { + gop.value_ptr.timestamp = entry.timestamp; + if (!std.mem.eql(u8, gop.value_ptr.terminfo_version, entry.terminfo_version)) { + alloc.free(gop.value_ptr.terminfo_version); + const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); + gop.value_ptr.terminfo_version = terminfo_copy; + } + } + } + } } } -/// List cached hosts by calling the external script -pub fn listCachedHosts(alloc: Allocator, writer: anytype) !void { - try runCacheCommand(alloc, writer, "list"); +// Atomic write via temp file + rename, filters out expired entries +fn writeCacheFile( + alloc: Allocator, + path: []const u8, + entries: *const std.StringHashMap(CacheEntry), + expire_days: u32, +) !void { + // Ensure parent directory exists + const dir = std.fs.path.dirname(path).?; + fs.makeDirAbsolute(dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + // Write to temp file first + const tmp_path = try std.fmt.allocPrint(alloc, "{s}.tmp", .{path}); + defer alloc.free(tmp_path); + + const tmp_file = try fs.createFileAbsolute(tmp_path, .{ .mode = 0o600 }); + defer tmp_file.close(); + errdefer fs.deleteFileAbsolute(tmp_path) catch {}; + + const writer = tmp_file.writer(); + + // Only write non-expired entries + var iter = entries.iterator(); + while (iter.next()) |kv| { + if (!kv.value_ptr.isExpired(expire_days)) { + try kv.value_ptr.format(writer); + } + } + + // Atomic replace + try fs.renameAbsolute(tmp_path, path); } -/// Clear cache by calling the external script -pub fn clearCache(alloc: Allocator, writer: anytype) !void { - try runCacheCommand(alloc, writer, "clear"); +fn checkHost(alloc: Allocator, host: []const u8) !bool { + if (!isValidCacheKey(host)) return CacheError.InvalidCacheKey; + + const path = try getCachePath(alloc); + + var entries = std.StringHashMap(CacheEntry).init(alloc); + + try readCacheFile(alloc, path, &entries); + return entries.contains(host); } + +fn addHost(alloc: Allocator, host: []const u8) !AddResult { + if (!isValidCacheKey(host)) return CacheError.InvalidCacheKey; + + const path = try getCachePath(alloc); + + // Create cache directory if needed + const dir = std.fs.path.dirname(path).?; + fs.makeDirAbsolute(dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + // Open or create cache file with secure permissions + const file = fs.createFileAbsolute(path, .{ + .read = true, + .truncate = false, + .mode = 0o600, + }) catch |err| switch (err) { + error.PathAlreadyExists => blk: { + const existing_file = fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |open_err| { + return open_err; + }; + + // Verify and fix permissions on existing file + const stat = existing_file.stat() catch |stat_err| { + existing_file.close(); + return stat_err; + }; + + // Ensure file has correct permissions (readable/writable by owner only) + if (stat.mode & 0o777 != 0o600) { + existing_file.chmod(0o600) catch |chmod_err| { + existing_file.close(); + return chmod_err; + }; + } + + break :blk existing_file; + }, + else => return err, + }; + defer file.close(); + + try acquireFileLock(file); + defer file.unlock(); + + var entries = std.StringHashMap(CacheEntry).init(alloc); + + try readCacheFile(alloc, path, &entries); + + // Add or update entry + const gop = try entries.getOrPut(host); + const result = if (!gop.found_existing) blk: { + gop.key_ptr.* = try alloc.dupe(u8, host); + gop.value_ptr.* = .{ + .hostname = gop.key_ptr.*, + .timestamp = std.time.timestamp(), + .terminfo_version = "xterm-ghostty", + }; + break :blk AddResult.added; + } else blk: { + // Update timestamp for existing entry + gop.value_ptr.timestamp = std.time.timestamp(); + break :blk AddResult.updated; + }; + + try writeCacheFile(alloc, path, &entries, NEVER_EXPIRE); + return result; +} + +fn removeHost(alloc: Allocator, host: []const u8) !void { + if (!isValidCacheKey(host)) return CacheError.InvalidCacheKey; + + const path = try getCachePath(alloc); + + const file = fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; + defer file.close(); + + try acquireFileLock(file); + defer file.unlock(); + + var entries = std.StringHashMap(CacheEntry).init(alloc); + + try readCacheFile(alloc, path, &entries); + + _ = entries.fetchRemove(host); + + try writeCacheFile(alloc, path, &entries, NEVER_EXPIRE); +} + +fn listHosts(alloc: Allocator, writer: anytype) !void { + const path = try getCachePath(alloc); + + var entries = std.StringHashMap(CacheEntry).init(alloc); + + readCacheFile(alloc, path, &entries) catch |err| switch (err) { + error.FileNotFound => { + try writer.print("No hosts in cache.\n", .{}); + return; + }, + else => return err, + }; + + if (entries.count() == 0) { + try writer.print("No hosts in cache.\n", .{}); + return; + } + + // Sort entries by hostname for consistent output + var items = std.ArrayList(CacheEntry).init(alloc); + defer items.deinit(); + + var iter = entries.iterator(); + while (iter.next()) |kv| { + try items.append(kv.value_ptr.*); + } + + std.mem.sort(CacheEntry, items.items, {}, struct { + fn lessThan(_: void, a: CacheEntry, b: CacheEntry) bool { + return std.mem.lessThan(u8, a.hostname, b.hostname); + } + }.lessThan); + + try writer.print("Cached hosts ({d}):\n", .{items.items.len}); + const now = std.time.timestamp(); + + for (items.items) |entry| { + const age_days = @divTrunc(now - entry.timestamp, SECONDS_PER_DAY); + if (age_days == 0) { + try writer.print(" {s} (today)\n", .{entry.hostname}); + } else if (age_days == 1) { + try writer.print(" {s} (yesterday)\n", .{entry.hostname}); + } else { + try writer.print(" {s} ({d} days ago)\n", .{ entry.hostname, age_days }); + } + } +} + +fn clearCache(alloc: Allocator) !void { + const path = try getCachePath(alloc); + + fs.deleteFileAbsolute(path) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; +} + +/// Manage the SSH terminfo cache for automatic remote host setup. +/// +/// When SSH integration is enabled with `shell-integration-features = ssh-terminfo`, +/// Ghostty automatically installs its terminfo on remote hosts. This command +/// manages the cache of successful installations to avoid redundant uploads. +/// +/// The cache stores hostnames (or user@hostname combinations) along with timestamps. +/// Entries older than the expiration period are automatically removed during cache +/// operations. By default, entries never expire. +/// +/// Examples: +/// ghostty +ssh-cache # List all cached hosts +/// ghostty +ssh-cache --host=example.com # Check if host is cached +/// ghostty +ssh-cache --add=example.com # Manually add host to cache +/// ghostty +ssh-cache --add=user@example.com # Add user@host combination +/// ghostty +ssh-cache --remove=example.com # Remove host from cache +/// ghostty +ssh-cache --clear # Clear entire cache +/// ghostty +ssh-cache --expire-days=30 # Set custom expiration period +pub fn run(alloc_gpa: Allocator) !u8 { + var arena = std.heap.ArenaAllocator.init(alloc_gpa); + defer arena.deinit(); + const alloc = arena.allocator(); + + var opts: Options = .{}; + defer opts.deinit(); + + { + var iter = try args.argsIterator(alloc_gpa); + defer iter.deinit(); + try args.parse(Options, alloc_gpa, &opts, &iter); + } + + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + if (opts.clear) { + try clearCache(alloc); + try stdout.print("Cache cleared.\n", .{}); + return 0; + } + + if (opts.add) |host| { + const result = addHost(alloc, host) catch |err| switch (err) { + CacheError.InvalidCacheKey => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + CacheError.CacheLocked => { + try stderr.print("Error: Cache is busy, try again\n", .{}); + return 1; + }, + error.AccessDenied, error.PermissionDenied => { + try stderr.print("Error: Permission denied\n", .{}); + return 1; + }, + else => { + try stderr.print("Error: Unable to add '{s}' to cache\n", .{host}); + return 1; + }, + }; + + switch (result) { + .added => try stdout.print("Added '{s}' to cache.\n", .{host}), + .updated => try stdout.print("Updated '{s}' cache entry.\n", .{host}), + } + return 0; + } + + if (opts.remove) |host| { + removeHost(alloc, host) catch |err| switch (err) { + CacheError.InvalidCacheKey => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + CacheError.CacheLocked => { + try stderr.print("Error: Cache is busy, try again\n", .{}); + return 1; + }, + error.AccessDenied, error.PermissionDenied => { + try stderr.print("Error: Permission denied\n", .{}); + return 1; + }, + else => { + try stderr.print("Error: Unable to remove '{s}' from cache\n", .{host}); + return 1; + }, + }; + try stdout.print("Removed '{s}' from cache.\n", .{host}); + return 0; + } + + if (opts.host) |host| { + const cached = checkHost(alloc, host) catch |err| switch (err) { + CacheError.InvalidCacheKey => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + error.AccessDenied, error.PermissionDenied => { + try stderr.print("Error: Permission denied\n", .{}); + return 1; + }, + else => { + try stderr.print("Error: Unable to check host '{s}' in cache\n", .{host}); + return 1; + }, + }; + + if (cached) { + try stdout.print("'{s}' has Ghostty terminfo installed.\n", .{host}); + return 0; + } else { + try stdout.print("'{s}' does not have Ghostty terminfo installed.\n", .{host}); + return 1; + } + } + + // Default action: list all hosts + try listHosts(alloc, stdout); + return 0; +} + +// Tests +test "hostname validation - valid cases" { + const testing = std.testing; + try testing.expect(isValidHostname("example.com")); + try testing.expect(isValidHostname("sub.example.com")); + try testing.expect(isValidHostname("host-name.domain.org")); + try testing.expect(isValidHostname("192.168.1.1")); + try testing.expect(isValidHostname("a")); + try testing.expect(isValidHostname("1")); +} + +test "hostname validation - IPv6 addresses" { + const testing = std.testing; + try testing.expect(isValidHostname("[::1]")); + try testing.expect(isValidHostname("[2001:db8::1]")); + try testing.expect(isValidHostname("[fe80::1%eth0]") == false); // Interface notation not supported + try testing.expect(isValidHostname("[]") == false); // Empty IPv6 + try testing.expect(isValidHostname("[invalid]") == false); // No colons +} + +test "hostname validation - invalid cases" { + const testing = std.testing; + try testing.expect(!isValidHostname("")); + try testing.expect(!isValidHostname("host\nname")); + try testing.expect(!isValidHostname(".example.com")); + try testing.expect(!isValidHostname("example.com.")); + try testing.expect(!isValidHostname("host..domain")); + try testing.expect(!isValidHostname("-hostname")); + try testing.expect(!isValidHostname("hostname-")); + try testing.expect(!isValidHostname("host name")); + try testing.expect(!isValidHostname("host_name")); + try testing.expect(!isValidHostname("host@domain")); + try testing.expect(!isValidHostname("host:port")); + + // Too long + const long_host = "a" ** 254; + try testing.expect(!isValidHostname(long_host)); +} + +test "user validation - valid cases" { + const testing = std.testing; + try testing.expect(isValidUser("user")); + try testing.expect(isValidUser("deploy")); + try testing.expect(isValidUser("test-user")); + try testing.expect(isValidUser("user_name")); + try testing.expect(isValidUser("user.name")); + try testing.expect(isValidUser("user123")); + try testing.expect(isValidUser("a")); +} + +test "user validation - complex realistic cases" { + const testing = std.testing; + try testing.expect(isValidUser("git")); + try testing.expect(isValidUser("ubuntu")); + try testing.expect(isValidUser("root")); + try testing.expect(isValidUser("service.account")); + try testing.expect(isValidUser("user-with-dashes")); +} + +test "user validation - invalid cases" { + const testing = std.testing; + try testing.expect(!isValidUser("")); + try testing.expect(!isValidUser("user name")); + try testing.expect(!isValidUser("user@domain")); + try testing.expect(!isValidUser("user:group")); + try testing.expect(!isValidUser("user\nname")); + + // Too long + const long_user = "a" ** 65; + try testing.expect(!isValidUser(long_user)); +} + +test "cache key validation - hostname format" { + const testing = std.testing; + try testing.expect(isValidCacheKey("example.com")); + try testing.expect(isValidCacheKey("sub.example.com")); + try testing.expect(isValidCacheKey("192.168.1.1")); + try testing.expect(isValidCacheKey("[::1]")); + try testing.expect(!isValidCacheKey("")); + try testing.expect(!isValidCacheKey(".invalid.com")); +} + +test "cache key validation - user@hostname format" { + const testing = std.testing; + try testing.expect(isValidCacheKey("user@example.com")); + try testing.expect(isValidCacheKey("deploy@prod.server.com")); + try testing.expect(isValidCacheKey("test-user@192.168.1.1")); + try testing.expect(isValidCacheKey("user_name@host.domain.org")); + try testing.expect(isValidCacheKey("git@github.com")); + try testing.expect(isValidCacheKey("ubuntu@[::1]")); + try testing.expect(!isValidCacheKey("@example.com")); + try testing.expect(!isValidCacheKey("user@")); + try testing.expect(!isValidCacheKey("user@@host")); + try testing.expect(!isValidCacheKey("user@.invalid.com")); +} + +test "cache entry expiration" { + const testing = std.testing; + const now = std.time.timestamp(); + + const fresh_entry = CacheEntry{ + .hostname = "test.com", + .timestamp = now - SECONDS_PER_DAY, // 1 day old + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(!fresh_entry.isExpired(90)); + + const old_entry = CacheEntry{ + .hostname = "old.com", + .timestamp = now - (SECONDS_PER_DAY * 100), // 100 days old + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(old_entry.isExpired(90)); + + // Test never-expire case + try testing.expect(!old_entry.isExpired(NEVER_EXPIRE)); +} + +test "cache entry expiration - boundary cases" { + const testing = std.testing; + const now = std.time.timestamp(); + + // Exactly at expiration boundary + const boundary_entry = CacheEntry{ + .hostname = "boundary.com", + .timestamp = now - (SECONDS_PER_DAY * 30), // Exactly 30 days old + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(!boundary_entry.isExpired(30)); // Should not be expired + try testing.expect(boundary_entry.isExpired(29)); // Should be expired +} + +test "cache entry parsing - valid formats" { + const testing = std.testing; + + const entry = CacheEntry.parse("example.com|1640995200|xterm-ghostty").?; + try testing.expectEqualStrings("example.com", entry.hostname); + try testing.expectEqual(@as(i64, 1640995200), entry.timestamp); + try testing.expectEqualStrings("xterm-ghostty", entry.terminfo_version); + + // Test default terminfo version + const entry_no_version = CacheEntry.parse("test.com|1640995200").?; + try testing.expectEqualStrings("xterm-ghostty", entry_no_version.terminfo_version); + + // Test complex hostnames + const complex_entry = CacheEntry.parse("user@server.example.com|1640995200|xterm-ghostty").?; + try testing.expectEqualStrings("user@server.example.com", complex_entry.hostname); +} + +test "cache entry parsing - invalid formats" { + const testing = std.testing; + + try testing.expect(CacheEntry.parse("") == null); + try testing.expect(CacheEntry.parse("v1") == null); // Invalid format (no pipe) + try testing.expect(CacheEntry.parse("example.com") == null); // Missing timestamp + try testing.expect(CacheEntry.parse("example.com|invalid") == null); // Invalid timestamp + try testing.expect(CacheEntry.parse("example.com|1640995200|") != null); // Empty terminfo should default +} + +test "cache entry parsing - malformed data resilience" { + const testing = std.testing; + + // Extra pipes should not break parsing + try testing.expect(CacheEntry.parse("host|123|term|extra") != null); + + // Whitespace handling + try testing.expect(CacheEntry.parse(" host|123|term ") != null); + try testing.expect(CacheEntry.parse("\n") == null); + try testing.expect(CacheEntry.parse(" \t \n") == null); +} + +test "duplicate cache entries - memory management" { + const testing = std.testing; + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var entries = std.StringHashMap(CacheEntry).init(alloc); + defer entries.deinit(); + + // Simulate reading a cache file with duplicate hostnames + const cache_content = "example.com|1640995200|xterm-ghostty\nexample.com|1640995300|xterm-ghostty-v2\n"; + + var lines = std.mem.tokenizeScalar(u8, cache_content, '\n'); + while (lines.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r"); + if (CacheEntry.parse(trimmed)) |entry| { + const gop = try entries.getOrPut(entry.hostname); + if (!gop.found_existing) { + const hostname_copy = try alloc.dupe(u8, entry.hostname); + const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); + gop.key_ptr.* = hostname_copy; + gop.value_ptr.* = CacheEntry{ + .hostname = hostname_copy, + .timestamp = entry.timestamp, + .terminfo_version = terminfo_copy, + }; + } else { + // Test the duplicate handling logic + if (entry.timestamp > gop.value_ptr.timestamp) { + gop.value_ptr.timestamp = entry.timestamp; + if (!std.mem.eql(u8, gop.value_ptr.terminfo_version, entry.terminfo_version)) { + alloc.free(gop.value_ptr.terminfo_version); + const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); + gop.value_ptr.terminfo_version = terminfo_copy; + } + } + } + } + } + + // Verify only one entry exists with the newer timestamp + try testing.expect(entries.count() == 1); + const entry = entries.get("example.com").?; + try testing.expectEqual(@as(i64, 1640995300), entry.timestamp); + try testing.expectEqualStrings("xterm-ghostty-v2", entry.terminfo_version); +} + +test "concurrent access simulation - file locking" { + const testing = std.testing; + + // This test simulates the file locking mechanism + // In practice, this would require actual file operations + // but we can test the error handling logic + + const TestError = error{CacheLocked}; + + const result = TestError.CacheLocked; + try testing.expectError(TestError.CacheLocked, result); +} + diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index b51dae9c7..8c4cd9e12 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -97,73 +97,172 @@ fi # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + : "${GHOSTTY_SSH_CACHE_TIMEOUT:=5}" + : "${GHOSTTY_SSH_CHECK_TIMEOUT:=3}" - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - fi - - # SSH wrapper + # SSH wrapper that preserves Ghostty features across remote connections ssh() { - local env=() opts=() ctrl=() + local ssh_env=() ssh_opts=() - # Set up env vars first so terminfo installation inherits them + # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local vars=( - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} + local -a ssh_env_vars=( + "COLORTERM=truecolor" + "TERM_PROGRAM=ghostty" ) - for v in "${vars[@]}"; do - builtin export "${v?}" - opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + if [[ -n "$TERM_PROGRAM_VERSION" ]]; then + ssh_env_vars+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") + fi + + # Temporarily export variables for SSH transmission + local -a ssh_exported_vars=() + for ssh_v in "${ssh_env_vars[@]}"; do + local ssh_var_name="${ssh_v%%=*}" + + if [[ -n "${!ssh_var_name+x}" ]]; then + ssh_exported_vars+=("$ssh_var_name=${!ssh_var_name}") + else + ssh_exported_vars+=("$ssh_var_name") + fi + + builtin export "${ssh_v?}" + + # Use both SendEnv and SetEnv for maximum compatibility + ssh_opts+=(-o "SendEnv $ssh_var_name") + ssh_opts+=(-o "SetEnv $ssh_v") done + + ssh_env+=("${ssh_env_vars[@]}") fi - # Install terminfo if needed, reuse control connection for main session + # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - # Get target (only when needed for terminfo) - builtin local target - target=$(builtin command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - - if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then - env+=(TERM=xterm-ghostty) - elif builtin command -v infocmp >/dev/null 2>&1; then - builtin local tinfo - tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || builtin echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n "$tinfo" ]]; then - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - builtin local cpath - cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(builtin echo "$tinfo" | builtin command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') in - OK) - builtin echo "Terminfo setup complete." >&2 - [[ -n "$target" ]] && "$_CACHE" add "$target" - env+=(TERM=xterm-ghostty) - ctrl+=(-o "ControlPath=$cpath") - ;; - *) builtin echo "Warning: Failed to install terminfo." >&2 ;; - esac + builtin local ssh_config ssh_user ssh_hostname + ssh_config=$(builtin command ssh -G "$@" 2>/dev/null) + ssh_user=$(echo "$ssh_config" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "ssh_user" ]] && echo "$ssh_value" && break + done) + ssh_hostname=$(echo "$ssh_config" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "hostname" ]] && echo "$ssh_value" && break + done) + ssh_target="${ssh_user}@${ssh_hostname}" + + if [[ -n "$ssh_hostname" ]]; then + # Detect timeout command (BSD compatibility) + local ssh_timeout_cmd="" + if command -v timeout >/dev/null 2>&1; then + ssh_timeout_cmd="timeout" + elif command -v gtimeout >/dev/null 2>&1; then + ssh_timeout_cmd="gtimeout" + fi + + # Check if terminfo is already cached + local ssh_cache_check_success=false + if command -v ghostty >/dev/null 2>&1; then + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CHECK_TIMEOUT}s" ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + else + ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + fi + fi + + if [[ "$ssh_cache_check_success" == "true" ]]; then + ssh_env+=(TERM=xterm-ghostty) + elif builtin command -v infocmp >/dev/null 2>&1; then + builtin local ssh_terminfo + + # Generate terminfo data (BSD base64 compatibility) + if base64 --help 2>&1 | grep -q GNU; then + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + else + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') + fi + + if [[ -n "$ssh_terminfo" ]]; then + builtin echo "Setting up Ghostty terminfo on remote host..." >&2 + builtin local ssh_cpath_dir ssh_cpath + + ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" + ssh_cpath="$ssh_cpath_dir/socket" + + local ssh_base64_decode_cmd + if base64 --help 2>&1 | grep -q GNU; then + ssh_base64_decode_cmd="base64 -d" + else + ssh_base64_decode_cmd="base64 -D" + fi + + if builtin echo "$ssh_terminfo" | $ssh_base64_decode_cmd | builtin command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null; then + builtin echo "Terminfo setup complete." >&2 + ssh_env+=(TERM=xterm-ghostty) + ssh_opts+=(-o "ControlPath=$ssh_cpath") + + # Cache successful installation + if [[ -n "$ssh_target" ]] && command -v ghostty >/dev/null 2>&1; then + ( + set +m + { + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CACHE_TIMEOUT}s" ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + else + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + fi + } & + ) + fi + else + builtin echo "Warning: Failed to install terminfo." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + builtin echo "Warning: Could not generate terminfo data." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + builtin echo "Warning: ghostty command not available for cache management." >&2 + ssh_env+=(TERM=xterm-256color) fi else - builtin echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + ssh_env+=(TERM=xterm-256color) + fi fi fi - # Fallback TERM only if terminfo didn't set it + # Ensure TERM is set when using ssh-env feature if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) + local ssh_term_set=false + for ssh_v in "${ssh_env[@]}"; do + if [[ "$ssh_v" =~ ^TERM= ]]; then + ssh_term_set=true + break + fi + done + if [[ "$ssh_term_set" == "false" && "$TERM" == "xterm-ghostty" ]]; then + ssh_env+=(TERM=xterm-256color) + fi fi - # Execute - if [[ ${#env[@]} -gt 0 ]]; then - env "${env[@]}" ssh "${opts[@]}" "${ctrl[@]}" "$@" - else - builtin command ssh "${opts[@]}" "${ctrl[@]}" "$@" + builtin command ssh "${ssh_opts[@]}" "$@" + local ssh_ret=$? + + # Restore original environment variables + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + for ssh_v in "${ssh_exported_vars[@]}"; do + if [[ "$ssh_v" == *=* ]]; then + builtin export "${ssh_v?}" + else + builtin unset "${ssh_v}" + fi + done fi + + return $ssh_ret } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 084861434..76fa7bafa 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -100,91 +100,238 @@ # SSH Integration use str + use re - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + if (or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo)) { + var GHOSTTY_SSH_CACHE_TIMEOUT = (if (has-env GHOSTTY_SSH_CACHE_TIMEOUT) { echo $E:GHOSTTY_SSH_CACHE_TIMEOUT } else { echo 5 }) + var GHOSTTY_SSH_CHECK_TIMEOUT = (if (has-env GHOSTTY_SSH_CHECK_TIMEOUT) { echo $E:GHOSTTY_SSH_CHECK_TIMEOUT } else { echo 3 }) - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - var _CACHE = $E:GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache - } + # SSH wrapper that preserves Ghostty features across remote connections + fn ssh {|@args| + var ssh-env = [] + var ssh-opts = [] - # SSH wrapper - fn ssh {|@args| - var env = [] - var opts = [] - var ctrl = [] + # Configure environment variables for remote session + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + var ssh-env-vars = [ + COLORTERM=truecolor + TERM_PROGRAM=ghostty + ] - # Set up env vars first so terminfo installation inherits them - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - var vars = [ - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ] - if (not-eq $E:TERM_PROGRAM_VERSION '') { - set vars = [$@vars TERM_PROGRAM_VERSION=$E:TERM_PROGRAM_VERSION] - } + if (has-env TERM_PROGRAM_VERSION) { + set ssh-env-vars = [$@ssh-env-vars TERM_PROGRAM_VERSION=$E:TERM_PROGRAM_VERSION] + } - for v $vars { - set-env (str:split = $v | take 1) (str:split = $v | drop 1 | str:join =) - var varname = (str:split = $v | take 1) - set opts = [$@opts -o 'SendEnv '$varname -o 'SetEnv '$v] - } - } - - # Install terminfo if needed, reuse control connection for main session - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - # Get target - var target = '' - try { - set target = (e:ssh -G $@args 2>/dev/null | e:awk '/^(user|hostname) /{print $2}' | e:paste -sd'@') - } catch { } - - if (and (not-eq $target '') ($_CACHE chk $target)) { - set env = [$@env TERM=xterm-ghostty] - } elif (has-external infocmp) { - var tinfo = '' - try { - set tinfo = (e:infocmp -x xterm-ghostty 2>/dev/null) - } catch { - echo "Warning: xterm-ghostty terminfo not found locally." >&2 - } - - if (not-eq $tinfo '') { - echo "Setting up Ghostty terminfo on remote host..." >&2 - var cpath = '/tmp/ghostty-ssh-'$E:USER'-'(randint 0 32767)'-'(date +%s) - var result = (echo $tinfo | e:ssh $@opts -o ControlMaster=yes -o ControlPath=$cpath -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') + # Store original values for restoration + var ssh-exported-vars = [] + for ssh-v $ssh-env-vars { + var ssh-var-name = (str:split &max=2 = $ssh-v)[0] - if (eq $result OK) { - echo "Terminfo setup complete." >&2 - if (not-eq $target '') { $_CACHE add $target } - set env = [$@env TERM=xterm-ghostty] - set ctrl = [$@ctrl -o ControlPath=$cpath] - } else { - echo "Warning: Failed to install terminfo." >&2 - } + if (has-env $ssh-var-name) { + var original-value = (get-env $ssh-var-name) + set ssh-exported-vars = [$@ssh-exported-vars $ssh-var-name=$original-value] + } else { + set ssh-exported-vars = [$@ssh-exported-vars $ssh-var-name] + } + + # Export the variable + var ssh-var-parts = (str:split &max=2 = $ssh-v) + set-env $ssh-var-parts[0] $ssh-var-parts[1] + + # Use both SendEnv and SetEnv for maximum compatibility + set ssh-opts = [$@ssh-opts -o "SendEnv "$ssh-var-name] + set ssh-opts = [$@ssh-opts -o "SetEnv "$ssh-v] + } + + set ssh-env = [$@ssh-env $@ssh-env-vars] } - } else { - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 - } - } - # Fallback TERM only if terminfo didn't set it - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - if (and (eq $E:TERM xterm-ghostty) (not (str:contains (str:join ' ' $env) 'TERM='))) { - set env = [$@env TERM=xterm-256color] - } - } + # Install terminfo on remote host if needed + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { + var ssh-config = "" + try { + set ssh-config = (external ssh -G $@args 2>/dev/null | slurp) + } catch { + set ssh-config = "" + } - # Execute - if (> (count $env) 0) { - e:env $@env e:ssh $@opts $@ctrl $@args - } else { - e:ssh $@opts $@ctrl $@args + var ssh-user = "" + var ssh-hostname = "" + + for line (str:split "\n" $ssh-config) { + var parts = (str:split " " $line) + if (and (> (count $parts) 1) (eq $parts[0] user)) { + set ssh-user = $parts[1] + } + if (and (> (count $parts) 1) (eq $parts[0] hostname)) { + set ssh-hostname = $parts[1] + } + } + + var ssh-target = $ssh-user"@"$ssh-hostname + + if (not-eq $ssh-hostname "") { + # Detect timeout command (BSD compatibility) + var ssh-timeout-cmd = "" + try { + external timeout --help >/dev/null 2>&1 + set ssh-timeout-cmd = timeout + } catch { + try { + external gtimeout --help >/dev/null 2>&1 + set ssh-timeout-cmd = gtimeout + } catch { + # no timeout command available + } + } + + # Check if terminfo is already cached + var ssh-cache-check-success = $false + try { + external ghostty --help >/dev/null 2>&1 + if (not-eq $ssh-timeout-cmd "") { + try { + external $ssh-timeout-cmd $GHOSTTY_SSH_CHECK_TIMEOUT"s" ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 + set ssh-cache-check-success = $true + } catch { + # cache check failed + } + } else { + try { + external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 + set ssh-cache-check-success = $true + } catch { + # cache check failed + } + } + } catch { + # ghostty not available + } + + if $ssh-cache-check-success { + set ssh-env = [$@ssh-env TERM=xterm-ghostty] + } else { + try { + external infocmp --help >/dev/null 2>&1 + + # Generate terminfo data (BSD base64 compatibility) + var ssh-terminfo = "" + try { + var base64-help = (external base64 --help 2>&1 | slurp) + if (str:contains $base64-help GNU) { + set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 -w0 2>/dev/null | slurp) + } else { + set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 2>/dev/null | external tr -d '\n' | slurp) + } + } catch { + set ssh-terminfo = "" + } + + if (not-eq $ssh-terminfo "") { + echo "Setting up Ghostty terminfo on remote host..." >&2 + var ssh-cpath-dir = "" + try { + set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) + } catch { + set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) + } + var ssh-cpath = $ssh-cpath-dir"/socket" + + var ssh-base64-decode-cmd = "" + try { + var base64-help = (external base64 --help 2>&1 | slurp) + if (str:contains $base64-help GNU) { + set ssh-base64-decode-cmd = "base64 -d" + } else { + set ssh-base64-decode-cmd = "base64 -D" + } + } catch { + set ssh-base64-decode-cmd = "base64 -d" + } + + var terminfo-install-success = $false + try { + echo $ssh-terminfo | external sh -c $ssh-base64-decode-cmd | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' >/dev/null 2>&1 + set terminfo-install-success = $true + } catch { + set terminfo-install-success = $false + } + + if $terminfo-install-success { + echo "Terminfo setup complete." >&2 + set ssh-env = [$@ssh-env TERM=xterm-ghostty] + set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] + + # Cache successful installation + if (and (not-eq $ssh-target "") (has-external ghostty)) { + if (not-eq $ssh-timeout-cmd "") { + external $ssh-timeout-cmd $GHOSTTY_SSH_CACHE_TIMEOUT"s" ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & + } else { + external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & + } + } + } else { + echo "Warning: Failed to install terminfo." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } else { + echo "Warning: Could not generate terminfo data." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } catch { + echo "Warning: ghostty command not available for cache management." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } + } else { + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } + } + + # Ensure TERM is set when using ssh-env feature + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + var ssh-term-set = $false + for ssh-v $ssh-env { + if (str:has-prefix $ssh-v TERM=) { + set ssh-term-set = $true + break + } + } + if (and (not $ssh-term-set) (eq $E:TERM xterm-ghostty)) { + set ssh-env = [$@ssh-env TERM=xterm-256color] + } + } + + var ssh-ret = 0 + try { + external ssh $@ssh-opts $@args + } catch e { + set ssh-ret = $e[reason][exit-status] + } + + # Restore original environment variables + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { + for ssh-v $ssh-exported-vars { + if (str:contains $ssh-v =) { + var ssh-var-parts = (str:split &max=2 = $ssh-v) + set-env $ssh-var-parts[0] $ssh-var-parts[1] + } else { + unset-env $ssh-v + } + } + } + + if (not-eq $ssh-ret 0) { + fail ssh-failed + } } - } } defer { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 4c780b5a7..7dc121919 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -86,88 +86,173 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # SSH Integration - if string match -qr 'ssh-(env|terminfo)' $GHOSTTY_SHELL_FEATURES + # SSH Integration for Fish Shell + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; or string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" + set -g GHOSTTY_SSH_CACHE_TIMEOUT (test -n "$GHOSTTY_SSH_CACHE_TIMEOUT"; and echo $GHOSTTY_SSH_CACHE_TIMEOUT; or echo 5) + set -g GHOSTTY_SSH_CHECK_TIMEOUT (test -n "$GHOSTTY_SSH_CHECK_TIMEOUT"; and echo $GHOSTTY_SSH_CHECK_TIMEOUT; or echo 3) - if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES - set -g _CACHE "$GHOSTTY_RESOURCES_DIR/shell-integration/shared/ghostty-ssh-cache" - end - - # SSH wrapper - function ssh - set -l env - set -l opts - set -l ctrl - - # Set up env vars first so terminfo installation inherits them - if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES - set -l vars \ - COLORTERM=truecolor \ - TERM_PROGRAM=ghostty + # SSH wrapper that preserves Ghostty features across remote connections + function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" + set -l ssh_env + set -l ssh_opts + # Configure environment variables for remote session + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + set -l ssh_env_vars \ + "COLORTERM=truecolor" \ + "TERM_PROGRAM=ghostty" + if test -n "$TERM_PROGRAM_VERSION" - set -a vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" + set -a ssh_env_vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" end - for v in $vars - set -l parts (string split = $v) - set -gx $parts[1] $parts[2] - set -a opts -o "SendEnv $parts[1]" -o "SetEnv $v" - end - end - - # Install terminfo if needed, reuse control connection for main session - if string match -q '*ssh-terminfo*' $GHOSTTY_SHELL_FEATURES - # Get target - set -l target (command ssh -G $argv 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') - - if test -n "$target" -a ("$_CACHE" chk "$target") - set -a env TERM=xterm-ghostty - else if command -v infocmp >/dev/null 2>&1 - set -l tinfo (infocmp -x xterm-ghostty 2>/dev/null) - set -l status_code $status - - if test $status_code -ne 0 - echo "Warning: xterm-ghostty terminfo not found locally." >&2 + # Store original values for restoration + set -l ssh_exported_vars + for ssh_v in $ssh_env_vars + set -l ssh_var_name (string split -m1 '=' -- $ssh_v)[1] + + if set -q $ssh_var_name + set -a ssh_exported_vars "$ssh_var_name="(eval echo \$$ssh_var_name) + else + set -a ssh_exported_vars $ssh_var_name end - if test -n "$tinfo" - echo "Setting up Ghostty terminfo on remote host..." >&2 - set -l cpath "/tmp/ghostty-ssh-$USER-"(random)"-"(date +%s) - set -l result (echo "$tinfo" | command ssh $opts -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s $argv ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') + # Export the variable + set -gx (string split -m1 '=' -- $ssh_v) - switch $result - case OK - echo "Terminfo setup complete." >&2 - test -n "$target" && "$_CACHE" add "$target" - set -a env TERM=xterm-ghostty - set -a ctrl -o "ControlPath=$cpath" - case '*' - echo "Warning: Failed to install terminfo." >&2 + # Use both SendEnv and SetEnv for maximum compatibility + set -a ssh_opts -o "SendEnv $ssh_var_name" + set -a ssh_opts -o "SetEnv $ssh_v" + end + + set -a ssh_env $ssh_env_vars + end + + # Install terminfo on remote host if needed + if string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" + set -l ssh_config (command ssh -G $argv 2>/dev/null) + set -l ssh_user (echo $ssh_config | while read -l ssh_key ssh_value + test "$ssh_key" = "user"; and echo $ssh_value; and break + end) + set -l ssh_hostname (echo $ssh_config | while read -l ssh_key ssh_value + test "$ssh_key" = "hostname"; and echo $ssh_value; and break + end) + set -l ssh_target "$ssh_user@$ssh_hostname" + + if test -n "$ssh_hostname" + # Detect timeout command (BSD compatibility) + set -l ssh_timeout_cmd + if command -v timeout >/dev/null 2>&1 + set ssh_timeout_cmd timeout + else if command -v gtimeout >/dev/null 2>&1 + set ssh_timeout_cmd gtimeout + end + + # Check if terminfo is already cached + set -l ssh_cache_check_success false + if command -v ghostty >/dev/null 2>&1 + if test -n "$ssh_timeout_cmd" + if $ssh_timeout_cmd "$GHOSTTY_SSH_CHECK_TIMEOUT"s ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + set ssh_cache_check_success true + end + else + if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + set ssh_cache_check_success true + end end end + + if test "$ssh_cache_check_success" = "true" + set -a ssh_env TERM=xterm-ghostty + else if command -v infocmp >/dev/null 2>&1 + # Generate terminfo data (BSD base64 compatibility) + set -l ssh_terminfo + if base64 --help 2>&1 | grep -q GNU + set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + else + set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') + end + + if test -n "$ssh_terminfo" + echo "Setting up Ghostty terminfo on remote host..." >&2 + set -l ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) + set -l ssh_cpath "$ssh_cpath_dir/socket" + + set -l ssh_base64_decode_cmd + if base64 --help 2>&1 | grep -q GNU + set ssh_base64_decode_cmd "base64 -d" + else + set ssh_base64_decode_cmd "base64 -D" + end + + if echo "$ssh_terminfo" | eval $ssh_base64_decode_cmd | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null + echo "Terminfo setup complete." >&2 + set -a ssh_env TERM=xterm-ghostty + set -a ssh_opts -o "ControlPath=$ssh_cpath" + + # Cache successful installation + if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 + fish -c " + if test -n '$ssh_timeout_cmd' + $ssh_timeout_cmd '$GHOSTTY_SSH_CACHE_TIMEOUT's ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true + else + ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true + end + " & + end + else + echo "Warning: Failed to install terminfo." >&2 + set -a ssh_env TERM=xterm-256color + end + else + echo "Warning: Could not generate terminfo data." >&2 + set -a ssh_env TERM=xterm-256color + end + else + echo "Warning: ghostty command not available for cache management." >&2 + set -a ssh_env TERM=xterm-256color + end else - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + set -a ssh_env TERM=xterm-256color + end end end - # Fallback TERM only if terminfo didn't set it - if string match -q '*ssh-env*' $GHOSTTY_SHELL_FEATURES - if test "$TERM" = xterm-ghostty -a ! (string join ' ' $env | string match -q '*TERM=*') - set -a env TERM=xterm-256color + # Ensure TERM is set when using ssh-env feature + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + set -l ssh_term_set false + for ssh_v in $ssh_env + if string match -q 'TERM=*' -- $ssh_v + set ssh_term_set true + break + end + end + if test "$ssh_term_set" = "false"; and test "$TERM" = "xterm-ghostty" + set -a ssh_env TERM=xterm-256color end end - # Execute - if test (count $env) -gt 0 - env $env command ssh $opts $ctrl $argv - else - command ssh $opts $ctrl $argv + command ssh $ssh_opts $argv + set -l ssh_ret $status + + # Restore original environment variables + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + for ssh_v in $ssh_exported_vars + if string match -q '*=*' -- $ssh_v + set -gx (string split -m1 '=' -- $ssh_v) + else + set -e $ssh_v + end + end end + + return $ssh_ret end end diff --git a/src/shell-integration/shared/ghostty-ssh-cache b/src/shell-integration/shared/ghostty-ssh-cache deleted file mode 100755 index e0a6d8452..000000000 --- a/src/shell-integration/shared/ghostty-ssh-cache +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash -# Minimal Ghostty SSH terminfo host cache - -readonly CACHE_FILE="${XDG_STATE_HOME:-$HOME/.local/state}/ghostty/terminfo_hosts" - -case "${1:-}" in - chk) [[ -f "$CACHE_FILE" ]] && grep -qFx "$2" "$CACHE_FILE" 2>/dev/null ;; - add) mkdir -p "${CACHE_FILE%/*}"; { [[ -f "$CACHE_FILE" ]] && cat "$CACHE_FILE"; echo "$2"; } | sort -u > "$CACHE_FILE.tmp" && mv "$CACHE_FILE.tmp" "$CACHE_FILE" && chmod 600 "$CACHE_FILE" ;; - list) [[ -s "$CACHE_FILE" ]] && echo "Hosts with Ghostty terminfo installed:" && cat "$CACHE_FILE" || echo "No cached hosts found." ;; - clear) rm -f "$CACHE_FILE" 2>/dev/null && echo "Ghostty SSH terminfo cache cleared." || echo "No Ghostty SSH terminfo cache found." ;; -esac diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 48f8cc934..9f78e9a89 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -245,78 +245,166 @@ _ghostty_deferred_init() { fi # SSH Integration - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" =~ (ssh-env|ssh-terminfo) ]]; then + : "${GHOSTTY_SSH_CACHE_TIMEOUT:=5}" + : "${GHOSTTY_SSH_CHECK_TIMEOUT:=3}" - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - readonly _CACHE="${GHOSTTY_RESOURCES_DIR}/shell-integration/shared/ghostty-ssh-cache" - fi - - # SSH wrapper + # SSH wrapper that preserves Ghostty features across remote connections ssh() { - local -a env opts ctrl - env=() - opts=() - ctrl=() + emulate -L zsh + setopt local_options no_glob_subst + + local -a ssh_env ssh_opts - # Set up env vars first so terminfo installation inherits them + # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local -a vars - vars=( - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ${TERM_PROGRAM_VERSION:+TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION} + local -a ssh_env_vars=( + "COLORTERM=truecolor" + "TERM_PROGRAM=ghostty" ) - for v in "${vars[@]}"; do - export "${v?}" - opts+=(-o "SendEnv ${v%=*}" -o "SetEnv $v") + [[ -n "$TERM_PROGRAM_VERSION" ]] && ssh_env_vars+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") + + # Temporarily export variables for SSH transmission + local -a ssh_exported_vars=() + local ssh_v ssh_var_name + for ssh_v in "${ssh_env_vars[@]}"; do + ssh_var_name="${ssh_v%%=*}" + + if [[ -n "${(P)ssh_var_name+x}" ]]; then + ssh_exported_vars+=("$ssh_var_name=${(P)ssh_var_name}") + else + ssh_exported_vars+=("$ssh_var_name") + fi + + export "${ssh_v}" + + # Use both SendEnv and SetEnv for maximum compatibility + ssh_opts+=(-o "SendEnv $ssh_var_name") + ssh_opts+=(-o "SetEnv $ssh_v") done + + ssh_env+=("${ssh_env_vars[@]}") fi - # Install terminfo if needed, reuse control connection for main session + # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - # Get target (only when needed for terminfo) - local target - target=$(command ssh -G "$@" 2>/dev/null | awk '/^(user|hostname) /{print $2}' | paste -sd'@') + local ssh_config ssh_user ssh_hostname ssh_target + ssh_config=$(command ssh -G "$@" 2>/dev/null) + ssh_user=$(printf '%s\n' "${(@f)ssh_config}" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "user" ]] && printf '%s\n' "$ssh_value" && break + done) + ssh_hostname=$(printf '%s\n' "${(@f)ssh_config}" | while IFS=' ' read -r ssh_key ssh_value; do + [[ "$ssh_key" == "hostname" ]] && printf '%s\n' "$ssh_value" && break + done) + ssh_target="${ssh_user}@${ssh_hostname}" - if [[ -n "$target" ]] && "$_CACHE" chk "$target"; then - env+=(TERM=xterm-ghostty) - elif command -v infocmp >/dev/null 2>&1; then - local tinfo - tinfo=$(infocmp -x xterm-ghostty 2>/dev/null) || echo "Warning: xterm-ghostty terminfo not found locally." >&2 - if [[ -n "$tinfo" ]]; then - echo "Setting up Ghostty terminfo on remote host..." >&2 - local cpath - cpath="/tmp/ghostty-ssh-$USER-$RANDOM-$(date +%s)" - case $(echo "$tinfo" | command ssh "${opts[@]}" -o ControlMaster=yes -o ControlPath="$cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && echo OK && exit - command -v tic >/dev/null 2>&1 || { echo NO_TIC; exit 1; } - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && echo OK || echo FAIL - ') in - OK) - echo "Terminfo setup complete." >&2 - [[ -n "$target" ]] && "$_CACHE" add "$target" - env+=(TERM=xterm-ghostty) - ctrl+=(-o "ControlPath=$cpath") - ;; - *) echo "Warning: Failed to install terminfo." >&2 ;; - esac + if [[ -n "$ssh_hostname" ]]; then + # Detect timeout command (BSD compatibility) + local ssh_timeout_cmd="" + if (( $+commands[timeout] )); then + ssh_timeout_cmd="timeout" + elif (( $+commands[gtimeout] )); then + ssh_timeout_cmd="gtimeout" + fi + + # Check if terminfo is already cached + local ssh_cache_check_success=false + if (( $+commands[ghostty] )); then + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CHECK_TIMEOUT}s" ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + else + ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true + fi + fi + + if [[ "$ssh_cache_check_success" == "true" ]]; then + ssh_env+=(TERM=xterm-ghostty) + elif (( $+commands[infocmp] )); then + local ssh_terminfo + + # Generate terminfo data (BSD base64 compatibility) + if base64 --help 2>&1 | grep -q GNU; then + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + else + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') + fi + + if [[ -n "$ssh_terminfo" ]]; then + print "Setting up Ghostty terminfo on remote host..." >&2 + local ssh_cpath_dir ssh_cpath + + ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" + ssh_cpath="$ssh_cpath_dir/socket" + + local ssh_base64_decode_cmd + if base64 --help 2>&1 | grep -q GNU; then + ssh_base64_decode_cmd="base64 -d" + else + ssh_base64_decode_cmd="base64 -D" + fi + + if print "$ssh_terminfo" | $ssh_base64_decode_cmd | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null; then + print "Terminfo setup complete." >&2 + ssh_env+=(TERM=xterm-ghostty) + ssh_opts+=(-o "ControlPath=$ssh_cpath") + + # Cache successful installation + if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then + { + if [[ -n "$ssh_timeout_cmd" ]]; then + $ssh_timeout_cmd "${GHOSTTY_SSH_CACHE_TIMEOUT}s" ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + else + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + fi + } &! + fi + else + print "Warning: Failed to install terminfo." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + print "Warning: Could not generate terminfo data." >&2 + ssh_env+=(TERM=xterm-256color) + fi + else + print "Warning: ghostty command not available for cache management." >&2 + ssh_env+=(TERM=xterm-256color) fi else - echo "Warning: infocmp not found locally. Terminfo installation unavailable." >&2 + [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]] && ssh_env+=(TERM=xterm-256color) fi fi - # Fallback TERM only if terminfo didn't set it + # Ensure TERM is set when using ssh-env feature if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - [[ $TERM == xterm-ghostty && ! " ${env[*]} " =~ " TERM=" ]] && env+=(TERM=xterm-256color) + local ssh_term_set=false ssh_v + for ssh_v in "${ssh_env[@]}"; do + [[ "$ssh_v" =~ ^TERM= ]] && ssh_term_set=true && break + done + [[ "$ssh_term_set" == "false" && "$TERM" == "xterm-ghostty" ]] && ssh_env+=(TERM=xterm-256color) fi - # Execute - if [[ ${#env[@]} -gt 0 ]]; then - env "${env[@]}" command ssh "${opts[@]}" "${ctrl[@]}" "$@" - else - command ssh "${opts[@]}" "${ctrl[@]}" "$@" + command ssh "${ssh_opts[@]}" "$@" + local ssh_ret=$? + + # Restore original environment variables + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + local ssh_v + for ssh_v in "${ssh_exported_vars[@]}"; do + if [[ "$ssh_v" == *=* ]]; then + export "${ssh_v}" + else + unset "${ssh_v}" + fi + done fi + + return $ssh_ret } fi From 5ec18f426c409273599d72a4b6f98055aff8e53b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Thu, 3 Jul 2025 23:17:46 -0700 Subject: [PATCH 43/86] tests: use ! operator instead of == false for consistency Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- src/cli/ssh_cache.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 71c47a7a7..e5acdfb7a 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -536,9 +536,9 @@ test "hostname validation - IPv6 addresses" { const testing = std.testing; try testing.expect(isValidHostname("[::1]")); try testing.expect(isValidHostname("[2001:db8::1]")); - try testing.expect(isValidHostname("[fe80::1%eth0]") == false); // Interface notation not supported - try testing.expect(isValidHostname("[]") == false); // Empty IPv6 - try testing.expect(isValidHostname("[invalid]") == false); // No colons + try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported + try testing.expect(!isValidHostname("[]")); // Empty IPv6 + try testing.expect(!isValidHostname("[invalid]")); // No colons } test "hostname validation - invalid cases" { From a22074a85ca21ccae413e161766d6bf7f063c596 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Sat, 5 Jul 2025 13:02:35 -0700 Subject: [PATCH 44/86] fix: optimize SSH integration and improve error handling - Replace dual-loop SSH config parsing with efficient single-pass case statement - Remove overly cautious timeout logic from cache checks for simplicity - Add base64 availability check with xterm-256color fallback when missing - Include hostname in terminfo setup messages for better UX - Maintain SendEnv/SetEnv dual approach for maximum OpenSSH compatibility (relying on SetEnv alone seems to drop some vars during my tests, despite them being explicitly included in AcceptEnv on the remote host) --- src/shell-integration/bash/ghostty.bash | 193 +++++++-------- .../elvish/lib/ghostty-integration.elv | 230 ++++++++---------- .../ghostty-shell-integration.fish | 190 +++++++-------- src/shell-integration/zsh/ghostty-integration | 190 +++++++-------- 4 files changed, 381 insertions(+), 422 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 8c4cd9e12..6016e9096 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -97,131 +97,116 @@ fi # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then - : "${GHOSTTY_SSH_CACHE_TIMEOUT:=5}" - : "${GHOSTTY_SSH_CHECK_TIMEOUT:=3}" - - # SSH wrapper that preserves Ghostty features across remote connections ssh() { - local ssh_env=() ssh_opts=() + builtin local ssh_env ssh_opts ssh_exported_vars + ssh_env=() + ssh_opts=() + ssh_exported_vars=() # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local -a ssh_env_vars=( + ssh_opts+=(-o "SetEnv COLORTERM=truecolor") + + if [[ -n "${TERM_PROGRAM+x}" ]]; then + ssh_exported_vars+=("TERM_PROGRAM=${TERM_PROGRAM}") + else + ssh_exported_vars+=("TERM_PROGRAM") + fi + builtin export "TERM_PROGRAM=ghostty" + ssh_opts+=(-o "SendEnv TERM_PROGRAM") + + if [[ -n "$TERM_PROGRAM_VERSION" ]]; then + if [[ -n "${TERM_PROGRAM_VERSION+x}" ]]; then + ssh_exported_vars+=("TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}") + else + ssh_exported_vars+=("TERM_PROGRAM_VERSION") + fi + ssh_opts+=(-o "SendEnv TERM_PROGRAM_VERSION") + fi + + ssh_env+=( "COLORTERM=truecolor" "TERM_PROGRAM=ghostty" ) if [[ -n "$TERM_PROGRAM_VERSION" ]]; then - ssh_env_vars+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") + ssh_env+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") fi - - # Temporarily export variables for SSH transmission - local -a ssh_exported_vars=() - for ssh_v in "${ssh_env_vars[@]}"; do - local ssh_var_name="${ssh_v%%=*}" - - if [[ -n "${!ssh_var_name+x}" ]]; then - ssh_exported_vars+=("$ssh_var_name=${!ssh_var_name}") - else - ssh_exported_vars+=("$ssh_var_name") - fi - - builtin export "${ssh_v?}" - - # Use both SendEnv and SetEnv for maximum compatibility - ssh_opts+=(-o "SendEnv $ssh_var_name") - ssh_opts+=(-o "SetEnv $ssh_v") - done - - ssh_env+=("${ssh_env_vars[@]}") fi # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then builtin local ssh_config ssh_user ssh_hostname ssh_config=$(builtin command ssh -G "$@" 2>/dev/null) - ssh_user=$(echo "$ssh_config" | while IFS=' ' read -r ssh_key ssh_value; do - [[ "$ssh_key" == "ssh_user" ]] && echo "$ssh_value" && break - done) - ssh_hostname=$(echo "$ssh_config" | while IFS=' ' read -r ssh_key ssh_value; do - [[ "$ssh_key" == "hostname" ]] && echo "$ssh_value" && break - done) + + while IFS=' ' read -r ssh_key ssh_value; do + case "$ssh_key" in + user) ssh_user="$ssh_value" ;; + hostname) ssh_hostname="$ssh_value" ;; + esac + [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break + done <<< "$ssh_config" + ssh_target="${ssh_user}@${ssh_hostname}" if [[ -n "$ssh_hostname" ]]; then - # Detect timeout command (BSD compatibility) - local ssh_timeout_cmd="" - if command -v timeout >/dev/null 2>&1; then - ssh_timeout_cmd="timeout" - elif command -v gtimeout >/dev/null 2>&1; then - ssh_timeout_cmd="gtimeout" - fi - # Check if terminfo is already cached - local ssh_cache_check_success=false - if command -v ghostty >/dev/null 2>&1; then - if [[ -n "$ssh_timeout_cmd" ]]; then - $ssh_timeout_cmd "${GHOSTTY_SSH_CHECK_TIMEOUT}s" ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true - else - ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true - fi + builtin local ssh_cache_check_success=false + if builtin command -v ghostty >/dev/null 2>&1; then + ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true fi if [[ "$ssh_cache_check_success" == "true" ]]; then ssh_env+=(TERM=xterm-ghostty) elif builtin command -v infocmp >/dev/null 2>&1; then - builtin local ssh_terminfo - - # Generate terminfo data (BSD base64 compatibility) - if base64 --help 2>&1 | grep -q GNU; then - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + if ! builtin command -v base64 >/dev/null 2>&1; then + builtin echo "Warning: base64 command not available for terminfo installation." >&2 + ssh_env+=(TERM=xterm-256color) else - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') - fi + builtin local ssh_terminfo ssh_base64_decode_cmd - if [[ -n "$ssh_terminfo" ]]; then - builtin echo "Setting up Ghostty terminfo on remote host..." >&2 - builtin local ssh_cpath_dir ssh_cpath - - ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" - ssh_cpath="$ssh_cpath_dir/socket" - - local ssh_base64_decode_cmd + # BSD vs GNU base64 compatibility if base64 --help 2>&1 | grep -q GNU; then ssh_base64_decode_cmd="base64 -d" + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) else ssh_base64_decode_cmd="base64 -D" + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') fi - if builtin echo "$ssh_terminfo" | $ssh_base64_decode_cmd | builtin command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null; then - builtin echo "Terminfo setup complete." >&2 - ssh_env+=(TERM=xterm-ghostty) - ssh_opts+=(-o "ControlPath=$ssh_cpath") + if [[ -n "$ssh_terminfo" ]]; then + builtin echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 + builtin local ssh_cpath_dir ssh_cpath - # Cache successful installation - if [[ -n "$ssh_target" ]] && command -v ghostty >/dev/null 2>&1; then - ( - set +m - { - if [[ -n "$ssh_timeout_cmd" ]]; then - $ssh_timeout_cmd "${GHOSTTY_SSH_CACHE_TIMEOUT}s" ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - else + ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" + ssh_cpath="$ssh_cpath_dir/socket" + + if builtin echo "$ssh_terminfo" | $ssh_base64_decode_cmd | builtin command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null; then + builtin echo "Terminfo setup complete on $ssh_hostname." >&2 + ssh_env+=(TERM=xterm-ghostty) + ssh_opts+=(-o "ControlPath=$ssh_cpath") + + # Cache successful installation + if [[ -n "$ssh_target" ]] && builtin command -v ghostty >/dev/null 2>&1; then + ( + set +m + { ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - fi - } & - ) + } & + ) + fi + else + builtin echo "Warning: Failed to install terminfo." >&2 + ssh_env+=(TERM=xterm-256color) fi else - builtin echo "Warning: Failed to install terminfo." >&2 + builtin echo "Warning: Could not generate terminfo data." >&2 ssh_env+=(TERM=xterm-256color) fi - else - builtin echo "Warning: Could not generate terminfo data." >&2 - ssh_env+=(TERM=xterm-256color) fi else builtin echo "Warning: ghostty command not available for cache management." >&2 @@ -234,22 +219,30 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then fi fi - # Ensure TERM is set when using ssh-env feature - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local ssh_term_set=false - for ssh_v in "${ssh_env[@]}"; do - if [[ "$ssh_v" =~ ^TERM= ]]; then - ssh_term_set=true - break - fi - done - if [[ "$ssh_term_set" == "false" && "$TERM" == "xterm-ghostty" ]]; then - ssh_env+=(TERM=xterm-256color) + # Execute SSH with environment handling + builtin local ssh_term_override="" + for ssh_v in "${ssh_env[@]}"; do + if [[ "$ssh_v" =~ ^TERM=(.*)$ ]]; then + ssh_term_override="${BASH_REMATCH[1]}" + break fi + done + + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env && -z "$ssh_term_override" ]]; then + ssh_env+=(TERM=xterm-256color) + ssh_term_override="xterm-256color" fi - builtin command ssh "${ssh_opts[@]}" "$@" - local ssh_ret=$? + if [[ -n "$ssh_term_override" ]]; then + builtin local ssh_original_term="$TERM" + builtin export TERM="$ssh_term_override" + builtin command ssh "${ssh_opts[@]}" "$@" + local ssh_ret=$? + builtin export TERM="$ssh_original_term" + else + builtin command ssh "${ssh_opts[@]}" "$@" + local ssh_ret=$? + fi # Restore original environment variables if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 76fa7bafa..52d01e4fb 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -100,50 +100,41 @@ # SSH Integration use str - use re if (or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo)) { - var GHOSTTY_SSH_CACHE_TIMEOUT = (if (has-env GHOSTTY_SSH_CACHE_TIMEOUT) { echo $E:GHOSTTY_SSH_CACHE_TIMEOUT } else { echo 5 }) - var GHOSTTY_SSH_CHECK_TIMEOUT = (if (has-env GHOSTTY_SSH_CHECK_TIMEOUT) { echo $E:GHOSTTY_SSH_CHECK_TIMEOUT } else { echo 3 }) - - # SSH wrapper that preserves Ghostty features across remote connections fn ssh {|@args| var ssh-env = [] var ssh-opts = [] + var ssh-exported-vars = [] # Configure environment variables for remote session if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - var ssh-env-vars = [ - COLORTERM=truecolor - TERM_PROGRAM=ghostty - ] + set ssh-opts = [$@ssh-opts -o "SetEnv COLORTERM=truecolor"] + + if (has-env TERM_PROGRAM) { + set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM="$E:TERM_PROGRAM] + } else { + set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM"] + } + set-env TERM_PROGRAM ghostty + set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM"] if (has-env TERM_PROGRAM_VERSION) { - set ssh-env-vars = [$@ssh-env-vars TERM_PROGRAM_VERSION=$E:TERM_PROGRAM_VERSION] - } - - # Store original values for restoration - var ssh-exported-vars = [] - for ssh-v $ssh-env-vars { - var ssh-var-name = (str:split &max=2 = $ssh-v)[0] - - if (has-env $ssh-var-name) { - var original-value = (get-env $ssh-var-name) - set ssh-exported-vars = [$@ssh-exported-vars $ssh-var-name=$original-value] + if (has-env TERM_PROGRAM_VERSION) { + set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM_VERSION="$E:TERM_PROGRAM_VERSION] } else { - set ssh-exported-vars = [$@ssh-exported-vars $ssh-var-name] + set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM_VERSION"] } - - # Export the variable - var ssh-var-parts = (str:split &max=2 = $ssh-v) - set-env $ssh-var-parts[0] $ssh-var-parts[1] - - # Use both SendEnv and SetEnv for maximum compatibility - set ssh-opts = [$@ssh-opts -o "SendEnv "$ssh-var-name] - set ssh-opts = [$@ssh-opts -o "SetEnv "$ssh-v] + set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM_VERSION"] } - set ssh-env = [$@ssh-env $@ssh-env-vars] + set ssh-env = [ + "COLORTERM=truecolor" + "TERM_PROGRAM=ghostty" + ] + if (has-env TERM_PROGRAM_VERSION) { + set ssh-env = [$@ssh-env "TERM_PROGRAM_VERSION="$E:TERM_PROGRAM_VERSION] + } } # Install terminfo on remote host if needed @@ -160,52 +151,28 @@ for line (str:split "\n" $ssh-config) { var parts = (str:split " " $line) - if (and (> (count $parts) 1) (eq $parts[0] user)) { - set ssh-user = $parts[1] - } - if (and (> (count $parts) 1) (eq $parts[0] hostname)) { - set ssh-hostname = $parts[1] + if (> (count $parts) 1) { + if (eq $parts[0] user) { + set ssh-user = $parts[1] + } elif (eq $parts[0] hostname) { + set ssh-hostname = $parts[1] + } + if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { + break + } } } - + var ssh-target = $ssh-user"@"$ssh-hostname if (not-eq $ssh-hostname "") { - # Detect timeout command (BSD compatibility) - var ssh-timeout-cmd = "" - try { - external timeout --help >/dev/null 2>&1 - set ssh-timeout-cmd = timeout - } catch { - try { - external gtimeout --help >/dev/null 2>&1 - set ssh-timeout-cmd = gtimeout - } catch { - # no timeout command available - } - } - # Check if terminfo is already cached var ssh-cache-check-success = $false try { - external ghostty --help >/dev/null 2>&1 - if (not-eq $ssh-timeout-cmd "") { - try { - external $ssh-timeout-cmd $GHOSTTY_SSH_CHECK_TIMEOUT"s" ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 - set ssh-cache-check-success = $true - } catch { - # cache check failed - } - } else { - try { - external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 - set ssh-cache-check-success = $true - } catch { - # cache check failed - } - } + external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 + set ssh-cache-check-success = $true } catch { - # ghostty not available + # cache check failed } if $ssh-cache-check-success { @@ -213,74 +180,68 @@ } else { try { external infocmp --help >/dev/null 2>&1 - - # Generate terminfo data (BSD base64 compatibility) - var ssh-terminfo = "" + try { - var base64-help = (external base64 --help 2>&1 | slurp) - if (str:contains $base64-help GNU) { - set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 -w0 2>/dev/null | slurp) - } else { - set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 2>/dev/null | external tr -d '\n' | slurp) - } - } catch { - set ssh-terminfo = "" - } - - if (not-eq $ssh-terminfo "") { - echo "Setting up Ghostty terminfo on remote host..." >&2 - var ssh-cpath-dir = "" - try { - set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) - } catch { - set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) - } - var ssh-cpath = $ssh-cpath-dir"/socket" + external base64 --help >/dev/null 2>&1 + # Generate terminfo data (BSD base64 compatibility) + var ssh-terminfo = "" var ssh-base64-decode-cmd = "" try { var base64-help = (external base64 --help 2>&1 | slurp) if (str:contains $base64-help GNU) { set ssh-base64-decode-cmd = "base64 -d" + set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 -w0 2>/dev/null | slurp) } else { set ssh-base64-decode-cmd = "base64 -D" + set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 2>/dev/null | external tr -d '\n' | slurp) } } catch { - set ssh-base64-decode-cmd = "base64 -d" + set ssh-terminfo = "" } - var terminfo-install-success = $false - try { - echo $ssh-terminfo | external sh -c $ssh-base64-decode-cmd | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' >/dev/null 2>&1 - set terminfo-install-success = $true - } catch { - set terminfo-install-success = $false - } + if (not-eq $ssh-terminfo "") { + echo "Setting up Ghostty terminfo on "$ssh-hostname"..." >&2 + var ssh-cpath-dir = "" + try { + set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) + } catch { + set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) + } + var ssh-cpath = $ssh-cpath-dir"/socket" - if $terminfo-install-success { - echo "Terminfo setup complete." >&2 - set ssh-env = [$@ssh-env TERM=xterm-ghostty] - set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] + var terminfo-install-success = $false + try { + echo $ssh-terminfo | external sh -c $ssh-base64-decode-cmd | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' >/dev/null 2>&1 + set terminfo-install-success = $true + } catch { + set terminfo-install-success = $false + } - # Cache successful installation - if (and (not-eq $ssh-target "") (has-external ghostty)) { - if (not-eq $ssh-timeout-cmd "") { - external $ssh-timeout-cmd $GHOSTTY_SSH_CACHE_TIMEOUT"s" ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & - } else { + if $terminfo-install-success { + echo "Terminfo setup complete on "$ssh-hostname"." >&2 + set ssh-env = [$@ssh-env TERM=xterm-ghostty] + set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] + + # Cache successful installation + if (and (not-eq $ssh-target "") (has-external ghostty)) { external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & } + } else { + echo "Warning: Failed to install terminfo." >&2 + set ssh-env = [$@ssh-env TERM=xterm-256color] } } else { - echo "Warning: Failed to install terminfo." >&2 + echo "Warning: Could not generate terminfo data." >&2 set ssh-env = [$@ssh-env TERM=xterm-256color] } - } else { - echo "Warning: Could not generate terminfo data." >&2 + } catch { + echo "Warning: base64 command not available for terminfo installation." >&2 set ssh-env = [$@ssh-env TERM=xterm-256color] } } catch { @@ -295,25 +256,36 @@ } } - # Ensure TERM is set when using ssh-env feature - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - var ssh-term-set = $false - for ssh-v $ssh-env { - if (str:has-prefix $ssh-v TERM=) { - set ssh-term-set = $true - break - } - } - if (and (not $ssh-term-set) (eq $E:TERM xterm-ghostty)) { - set ssh-env = [$@ssh-env TERM=xterm-256color] + # Execute SSH with environment handling + var ssh-term-override = "" + for ssh-v $ssh-env { + if (str:has-prefix $ssh-v TERM=) { + set ssh-term-override = (str:trim-prefix $ssh-v TERM=) + break } } + if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (eq $ssh-term-override "")) { + set ssh-env = [$@ssh-env TERM=xterm-256color] + set ssh-term-override = xterm-256color + } + var ssh-ret = 0 - try { - external ssh $@ssh-opts $@args - } catch e { - set ssh-ret = $e[reason][exit-status] + if (not-eq $ssh-term-override "") { + var ssh-original-term = $E:TERM + set-env TERM $ssh-term-override + try { + external ssh $@ssh-opts $@args + } catch e { + set ssh-ret = $e[reason][exit-status] + } + set-env TERM $ssh-original-term + } else { + try { + external ssh $@ssh-opts $@args + } catch e { + set ssh-ret = $e[reason][exit-status] + } } # Restore original environment variables diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 7dc121919..332675264 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -86,132 +86,119 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # SSH Integration for Fish Shell + # SSH Integration if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; or string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" - set -g GHOSTTY_SSH_CACHE_TIMEOUT (test -n "$GHOSTTY_SSH_CACHE_TIMEOUT"; and echo $GHOSTTY_SSH_CACHE_TIMEOUT; or echo 5) - set -g GHOSTTY_SSH_CHECK_TIMEOUT (test -n "$GHOSTTY_SSH_CHECK_TIMEOUT"; and echo $GHOSTTY_SSH_CHECK_TIMEOUT; or echo 3) - - # SSH wrapper that preserves Ghostty features across remote connections function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" set -l ssh_env set -l ssh_opts + set -l ssh_exported_vars # Configure environment variables for remote session if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" - set -l ssh_env_vars \ - "COLORTERM=truecolor" \ - "TERM_PROGRAM=ghostty" - + set -a ssh_opts -o "SetEnv COLORTERM=truecolor" + + if set -q TERM_PROGRAM + set -a ssh_exported_vars "TERM_PROGRAM=$TERM_PROGRAM" + else + set -a ssh_exported_vars "TERM_PROGRAM" + end + set -gx TERM_PROGRAM ghostty + set -a ssh_opts -o "SendEnv TERM_PROGRAM" + if test -n "$TERM_PROGRAM_VERSION" - set -a ssh_env_vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" - end - - # Store original values for restoration - set -l ssh_exported_vars - for ssh_v in $ssh_env_vars - set -l ssh_var_name (string split -m1 '=' -- $ssh_v)[1] - - if set -q $ssh_var_name - set -a ssh_exported_vars "$ssh_var_name="(eval echo \$$ssh_var_name) + if set -q TERM_PROGRAM_VERSION + set -a ssh_exported_vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" else - set -a ssh_exported_vars $ssh_var_name + set -a ssh_exported_vars "TERM_PROGRAM_VERSION" end - - # Export the variable - set -gx (string split -m1 '=' -- $ssh_v) - - # Use both SendEnv and SetEnv for maximum compatibility - set -a ssh_opts -o "SendEnv $ssh_var_name" - set -a ssh_opts -o "SetEnv $ssh_v" + set -a ssh_opts -o "SendEnv TERM_PROGRAM_VERSION" end - set -a ssh_env $ssh_env_vars + set -a ssh_env "COLORTERM=truecolor" + set -a ssh_env "TERM_PROGRAM=ghostty" + if test -n "$TERM_PROGRAM_VERSION" + set -a ssh_env "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" + end end # Install terminfo on remote host if needed if string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" set -l ssh_config (command ssh -G $argv 2>/dev/null) - set -l ssh_user (echo $ssh_config | while read -l ssh_key ssh_value - test "$ssh_key" = "user"; and echo $ssh_value; and break - end) - set -l ssh_hostname (echo $ssh_config | while read -l ssh_key ssh_value - test "$ssh_key" = "hostname"; and echo $ssh_value; and break - end) + set -l ssh_user + set -l ssh_hostname + + for line in $ssh_config + set -l parts (string split ' ' -- $line) + if test (count $parts) -ge 2 + switch $parts[1] + case user + set ssh_user $parts[2] + case hostname + set ssh_hostname $parts[2] + end + if test -n "$ssh_user"; and test -n "$ssh_hostname" + break + end + end + end + set -l ssh_target "$ssh_user@$ssh_hostname" if test -n "$ssh_hostname" - # Detect timeout command (BSD compatibility) - set -l ssh_timeout_cmd - if command -v timeout >/dev/null 2>&1 - set ssh_timeout_cmd timeout - else if command -v gtimeout >/dev/null 2>&1 - set ssh_timeout_cmd gtimeout - end - # Check if terminfo is already cached set -l ssh_cache_check_success false if command -v ghostty >/dev/null 2>&1 - if test -n "$ssh_timeout_cmd" - if $ssh_timeout_cmd "$GHOSTTY_SSH_CHECK_TIMEOUT"s ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 - set ssh_cache_check_success true - end - else - if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 - set ssh_cache_check_success true - end + if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + set ssh_cache_check_success true end end if test "$ssh_cache_check_success" = "true" set -a ssh_env TERM=xterm-ghostty else if command -v infocmp >/dev/null 2>&1 - # Generate terminfo data (BSD base64 compatibility) - set -l ssh_terminfo - if base64 --help 2>&1 | grep -q GNU - set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + if not command -v base64 >/dev/null 2>&1 + echo "Warning: base64 command not available for terminfo installation." >&2 + set -a ssh_env TERM=xterm-256color else - set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') - end - - if test -n "$ssh_terminfo" - echo "Setting up Ghostty terminfo on remote host..." >&2 - set -l ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) - set -l ssh_cpath "$ssh_cpath_dir/socket" - + set -l ssh_terminfo set -l ssh_base64_decode_cmd + + # BSD vs GNU base64 compatibility if base64 --help 2>&1 | grep -q GNU set ssh_base64_decode_cmd "base64 -d" + set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) else set ssh_base64_decode_cmd "base64 -D" + set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') end - if echo "$ssh_terminfo" | eval $ssh_base64_decode_cmd | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null - echo "Terminfo setup complete." >&2 - set -a ssh_env TERM=xterm-ghostty - set -a ssh_opts -o "ControlPath=$ssh_cpath" + if test -n "$ssh_terminfo" + echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 + set -l ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) + set -l ssh_cpath "$ssh_cpath_dir/socket" - # Cache successful installation - if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 - fish -c " - if test -n '$ssh_timeout_cmd' - $ssh_timeout_cmd '$GHOSTTY_SSH_CACHE_TIMEOUT's ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true - else - ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true - end - " & + if echo "$ssh_terminfo" | eval $ssh_base64_decode_cmd | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null + echo "Terminfo setup complete on $ssh_hostname." >&2 + set -a ssh_env TERM=xterm-ghostty + set -a ssh_opts -o "ControlPath=$ssh_cpath" + + # Cache successful installation + if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 + fish -c "ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true" & + end + else + echo "Warning: Failed to install terminfo." >&2 + set -a ssh_env TERM=xterm-256color end else - echo "Warning: Failed to install terminfo." >&2 + echo "Warning: Could not generate terminfo data." >&2 set -a ssh_env TERM=xterm-256color end - else - echo "Warning: Could not generate terminfo data." >&2 - set -a ssh_env TERM=xterm-256color end else echo "Warning: ghostty command not available for cache management." >&2 @@ -224,22 +211,31 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # Ensure TERM is set when using ssh-env feature - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" - set -l ssh_term_set false - for ssh_v in $ssh_env - if string match -q 'TERM=*' -- $ssh_v - set ssh_term_set true - break - end - end - if test "$ssh_term_set" = "false"; and test "$TERM" = "xterm-ghostty" - set -a ssh_env TERM=xterm-256color + # Execute SSH with environment handling + set -l ssh_term_override + for ssh_v in $ssh_env + if string match -q 'TERM=*' -- $ssh_v + set ssh_term_override (string replace 'TERM=' '' -- $ssh_v) + break end end - command ssh $ssh_opts $argv - set -l ssh_ret $status + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; and test -z "$ssh_term_override" + set -a ssh_env TERM=xterm-256color + set ssh_term_override xterm-256color + end + + set -l ssh_ret + if test -n "$ssh_term_override" + set -l ssh_original_term "$TERM" + set -gx TERM "$ssh_term_override" + command ssh $ssh_opts $argv + set ssh_ret $status + set -gx TERM "$ssh_original_term" + else + command ssh $ssh_opts $argv + set ssh_ret $status + end # Restore original environment variables if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 9f78e9a89..40ee58b49 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -244,132 +244,116 @@ _ghostty_deferred_init() { } fi - # SSH Integration +# SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" =~ (ssh-env|ssh-terminfo) ]]; then - : "${GHOSTTY_SSH_CACHE_TIMEOUT:=5}" - : "${GHOSTTY_SSH_CHECK_TIMEOUT:=3}" - - # SSH wrapper that preserves Ghostty features across remote connections ssh() { emulate -L zsh setopt local_options no_glob_subst - - local -a ssh_env ssh_opts + + local ssh_env ssh_opts ssh_exported_vars + ssh_env=() + ssh_opts=() + ssh_exported_vars=() # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local -a ssh_env_vars=( + ssh_opts+=(-o "SetEnv COLORTERM=truecolor") + + if [[ -n "${TERM_PROGRAM+x}" ]]; then + ssh_exported_vars+=("TERM_PROGRAM=${TERM_PROGRAM}") + else + ssh_exported_vars+=("TERM_PROGRAM") + fi + export "TERM_PROGRAM=ghostty" + ssh_opts+=(-o "SendEnv TERM_PROGRAM") + + if [[ -n "$TERM_PROGRAM_VERSION" ]]; then + if [[ -n "${TERM_PROGRAM_VERSION+x}" ]]; then + ssh_exported_vars+=("TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}") + else + ssh_exported_vars+=("TERM_PROGRAM_VERSION") + fi + ssh_opts+=(-o "SendEnv TERM_PROGRAM_VERSION") + fi + + ssh_env+=( "COLORTERM=truecolor" "TERM_PROGRAM=ghostty" ) - [[ -n "$TERM_PROGRAM_VERSION" ]] && ssh_env_vars+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") - - # Temporarily export variables for SSH transmission - local -a ssh_exported_vars=() - local ssh_v ssh_var_name - for ssh_v in "${ssh_env_vars[@]}"; do - ssh_var_name="${ssh_v%%=*}" - - if [[ -n "${(P)ssh_var_name+x}" ]]; then - ssh_exported_vars+=("$ssh_var_name=${(P)ssh_var_name}") - else - ssh_exported_vars+=("$ssh_var_name") - fi - - export "${ssh_v}" - - # Use both SendEnv and SetEnv for maximum compatibility - ssh_opts+=(-o "SendEnv $ssh_var_name") - ssh_opts+=(-o "SetEnv $ssh_v") - done - - ssh_env+=("${ssh_env_vars[@]}") + [[ -n "$TERM_PROGRAM_VERSION" ]] && ssh_env+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") fi # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then - local ssh_config ssh_user ssh_hostname ssh_target + local ssh_config ssh_user ssh_hostname ssh_config=$(command ssh -G "$@" 2>/dev/null) - ssh_user=$(printf '%s\n' "${(@f)ssh_config}" | while IFS=' ' read -r ssh_key ssh_value; do - [[ "$ssh_key" == "user" ]] && printf '%s\n' "$ssh_value" && break - done) - ssh_hostname=$(printf '%s\n' "${(@f)ssh_config}" | while IFS=' ' read -r ssh_key ssh_value; do - [[ "$ssh_key" == "hostname" ]] && printf '%s\n' "$ssh_value" && break - done) + + while IFS=' ' read -r ssh_key ssh_value; do + case "$ssh_key" in + user) ssh_user="$ssh_value" ;; + hostname) ssh_hostname="$ssh_value" ;; + esac + [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break + done <<< "$ssh_config" + ssh_target="${ssh_user}@${ssh_hostname}" if [[ -n "$ssh_hostname" ]]; then - # Detect timeout command (BSD compatibility) - local ssh_timeout_cmd="" - if (( $+commands[timeout] )); then - ssh_timeout_cmd="timeout" - elif (( $+commands[gtimeout] )); then - ssh_timeout_cmd="gtimeout" - fi - # Check if terminfo is already cached local ssh_cache_check_success=false if (( $+commands[ghostty] )); then - if [[ -n "$ssh_timeout_cmd" ]]; then - $ssh_timeout_cmd "${GHOSTTY_SSH_CHECK_TIMEOUT}s" ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true - else - ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true - fi + ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true fi if [[ "$ssh_cache_check_success" == "true" ]]; then ssh_env+=(TERM=xterm-ghostty) elif (( $+commands[infocmp] )); then - local ssh_terminfo - - # Generate terminfo data (BSD base64 compatibility) - if base64 --help 2>&1 | grep -q GNU; then - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) + if ! (( $+commands[base64] )); then + print "Warning: base64 command not available for terminfo installation." >&2 + ssh_env+=(TERM=xterm-256color) else - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') - fi + local ssh_terminfo ssh_base64_decode_cmd - if [[ -n "$ssh_terminfo" ]]; then - print "Setting up Ghostty terminfo on remote host..." >&2 - local ssh_cpath_dir ssh_cpath - - ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" - ssh_cpath="$ssh_cpath_dir/socket" - - local ssh_base64_decode_cmd + # BSD vs GNU base64 compatibility if base64 --help 2>&1 | grep -q GNU; then ssh_base64_decode_cmd="base64 -d" + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) else ssh_base64_decode_cmd="base64 -D" + ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') fi - if print "$ssh_terminfo" | $ssh_base64_decode_cmd | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null; then - print "Terminfo setup complete." >&2 - ssh_env+=(TERM=xterm-ghostty) - ssh_opts+=(-o "ControlPath=$ssh_cpath") + if [[ -n "$ssh_terminfo" ]]; then + print "Setting up Ghostty terminfo on $ssh_hostname..." >&2 + local ssh_cpath_dir ssh_cpath - # Cache successful installation - if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then - { - if [[ -n "$ssh_timeout_cmd" ]]; then - $ssh_timeout_cmd "${GHOSTTY_SSH_CACHE_TIMEOUT}s" ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - else + ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" + ssh_cpath="$ssh_cpath_dir/socket" + + if print "$ssh_terminfo" | $ssh_base64_decode_cmd | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null; then + print "Terminfo setup complete on $ssh_hostname." >&2 + ssh_env+=(TERM=xterm-ghostty) + ssh_opts+=(-o "ControlPath=$ssh_cpath") + + # Cache successful installation + if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then + { ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - fi - } &! + } &! + fi + else + print "Warning: Failed to install terminfo." >&2 + ssh_env+=(TERM=xterm-256color) fi else - print "Warning: Failed to install terminfo." >&2 + print "Warning: Could not generate terminfo data." >&2 ssh_env+=(TERM=xterm-256color) fi - else - print "Warning: Could not generate terminfo data." >&2 - ssh_env+=(TERM=xterm-256color) fi else print "Warning: ghostty command not available for cache management." >&2 @@ -380,21 +364,35 @@ _ghostty_deferred_init() { fi fi - # Ensure TERM is set when using ssh-env feature - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local ssh_term_set=false ssh_v - for ssh_v in "${ssh_env[@]}"; do - [[ "$ssh_v" =~ ^TERM= ]] && ssh_term_set=true && break - done - [[ "$ssh_term_set" == "false" && "$TERM" == "xterm-ghostty" ]] && ssh_env+=(TERM=xterm-256color) + # Execute SSH with environment handling + local ssh_term_override="" + local ssh_v + for ssh_v in "${ssh_env[@]}"; do + if [[ "$ssh_v" =~ ^TERM=(.*)$ ]]; then + ssh_term_override="${match[1]}" + break + fi + done + + if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env && -z "$ssh_term_override" ]]; then + ssh_env+=(TERM=xterm-256color) + ssh_term_override="xterm-256color" fi - command ssh "${ssh_opts[@]}" "$@" - local ssh_ret=$? + local ssh_ret + if [[ -n "$ssh_term_override" ]]; then + local ssh_original_term="$TERM" + export TERM="$ssh_term_override" + command ssh "${ssh_opts[@]}" "$@" + ssh_ret=$? + export TERM="$ssh_original_term" + else + command ssh "${ssh_opts[@]}" "$@" + ssh_ret=$? + fi # Restore original environment variables if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - local ssh_v for ssh_v in "${ssh_exported_vars[@]}"; do if [[ "$ssh_v" == *=* ]]; then export "${ssh_v}" From a727b59b2beda24ef7d79ce282a87ac5bcf84013 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Sat, 5 Jul 2025 13:15:59 -0700 Subject: [PATCH 45/86] fix: replace custom const with std lib, remove dead-weight test - Replaced custom const `SECONDS_PER_DAY` with `std.time.s_per_day` - Removed concurrent access test - would need real file ops to be meaningful --- src/cli/ssh_cache.zig | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index e5acdfb7a..112f3a5c5 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -12,7 +12,6 @@ pub const CacheError = error{ const MAX_CACHE_SIZE = 512 * 1024; // 512KB - sufficient for approximately 10k entries const NEVER_EXPIRE = 0; -const SECONDS_PER_DAY = 86400; pub const Options = struct { clear: bool = false, @@ -65,7 +64,7 @@ const CacheEntry = struct { fn isExpired(self: CacheEntry, expire_days: u32) bool { if (expire_days == NEVER_EXPIRE) return false; const now = std.time.timestamp(); - const age_days = @divTrunc(now - self.timestamp, SECONDS_PER_DAY); + const age_days = @divTrunc(now - self.timestamp, std.time.s_per_day); return age_days > expire_days; } }; @@ -377,7 +376,7 @@ fn listHosts(alloc: Allocator, writer: anytype) !void { const now = std.time.timestamp(); for (items.items) |entry| { - const age_days = @divTrunc(now - entry.timestamp, SECONDS_PER_DAY); + const age_days = @divTrunc(now - entry.timestamp, std.time.s_per_day); if (age_days == 0) { try writer.print(" {s} (today)\n", .{entry.hostname}); } else if (age_days == 1) { @@ -623,14 +622,14 @@ test "cache entry expiration" { const fresh_entry = CacheEntry{ .hostname = "test.com", - .timestamp = now - SECONDS_PER_DAY, // 1 day old + .timestamp = now - std.time.s_per_day, // 1 day old .terminfo_version = "xterm-ghostty", }; try testing.expect(!fresh_entry.isExpired(90)); const old_entry = CacheEntry{ .hostname = "old.com", - .timestamp = now - (SECONDS_PER_DAY * 100), // 100 days old + .timestamp = now - (std.time.s_per_day * 100), // 100 days old .terminfo_version = "xterm-ghostty", }; try testing.expect(old_entry.isExpired(90)); @@ -646,7 +645,7 @@ test "cache entry expiration - boundary cases" { // Exactly at expiration boundary const boundary_entry = CacheEntry{ .hostname = "boundary.com", - .timestamp = now - (SECONDS_PER_DAY * 30), // Exactly 30 days old + .timestamp = now - (std.time.s_per_day * 30), // Exactly 30 days old .terminfo_version = "xterm-ghostty", }; try testing.expect(!boundary_entry.isExpired(30)); // Should not be expired @@ -738,17 +737,3 @@ test "duplicate cache entries - memory management" { try testing.expectEqual(@as(i64, 1640995300), entry.timestamp); try testing.expectEqualStrings("xterm-ghostty-v2", entry.terminfo_version); } - -test "concurrent access simulation - file locking" { - const testing = std.testing; - - // This test simulates the file locking mechanism - // In practice, this would require actual file operations - // but we can test the error handling logic - - const TestError = error{CacheLocked}; - - const result = TestError.CacheLocked; - try testing.expectError(TestError.CacheLocked, result); -} - From 3ff11cdd86cd8a5a0d26631a8935edf5f1dfeede Mon Sep 17 00:00:00 2001 From: "Sl (Shahaf Levi)" Date: Sun, 6 Jul 2025 23:38:22 +0300 Subject: [PATCH 46/86] Add Hebrew Translations --- CODEOWNERS | 1 + po/he_IL.UTF-8.po | 298 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 3 files changed, 300 insertions(+) create mode 100644 po/he_IL.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 7995650b7..3bb6a4123 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -181,6 +181,7 @@ /po/zh_CN.UTF-8.po @ghostty-org/zh_CN /po/ga_IE.UTF-8.po @ghostty-org/ga_IE /po/ko_KR.UTF-8.po @ghostty-org/ko_KR +/po/he_IL.UTF-8.po @ghostty-org/he_IL # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po new file mode 100644 index 000000000..636bf46e3 --- /dev/null +++ b/po/he_IL.UTF-8.po @@ -0,0 +1,298 @@ +# Hebrew translations for com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Sl (Shahaf Levi), Sl's Repository Ltd , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-06-28 17:01+0200\n" +"PO-Revision-Date: 2025-03-13 00:00+0000\n" +"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" +"Language-Team: Hebrew \n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "שינוי כותרת המסוף" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "השאר/י ריק כדי לשחזר את כותרת ברירת המחדל." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "ביטול" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "אישור" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "שגיאות בהגדרות" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "נמצאו אחת או יותר שגיאות בהגדרות. אנא בדוק/י את השגיאות המופיעות מטה ולאחר מכן טען/י את ההגדרות מחדש או התעלם/י מהשגיאות." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "התעלמות" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "טעינה מחדש של ההגדרות" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "פיצול למעלה" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "פיצול למטה" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "פיצול שמאלה" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "פיצול ימינה" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "הרץ/י פקודה…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 +msgid "Copy" +msgstr "העתקה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "הדבקה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:18 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:73 +msgid "Clear" +msgstr "ניקוי" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:23 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:78 +msgid "Reset" +msgstr "איפוס" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42 +msgid "Split" +msgstr "פיצול" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 +msgid "Change Title…" +msgstr "שינוי כותרת…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "כרטיסייה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 +#: src/apprt/gtk/Window.zig:263 +msgid "New Tab" +msgstr "כרטיסייה חדשה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:67 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:35 +msgid "Close Tab" +msgstr "סגור/י כרטיסייה" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "חלון" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:76 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:18 +msgid "New Window" +msgstr "חלון חדש" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:81 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:23 +msgid "Close Window" +msgstr "סגור/י חלון" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "הגדרות" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "פתיחת ההגדרות" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "לוח פקודות" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "בודק המסוף" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1036 +msgid "About Ghostty" +msgstr "אודות Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "יציאה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "אשר/י גישה ללוח ההעתקה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "יש אפליקציה שמנסה לקרוא מלוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "דחייה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "אישור" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "זכור/י את הבחירה עבור פיצול זה" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "טען/י את ההגדרות מחדש כדי להציג את הבקשה הזו שוב" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "יש אפליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "אזהרה: ההדבקה עלולה להיות מסוכנת" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "הדבקת טקסט זה במסוף עלולה להיות מסוכנת, מכיוון שככל הנראה היא תוביל להרצה של פקודות מסוימות." + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "תפריט ראשי" + +#: src/apprt/gtk/Window.zig:238 +msgid "View Open Tabs" +msgstr "הצג/י כרטיסיות פתוחות" + +#: src/apprt/gtk/Window.zig:264 +msgid "New Split" +msgstr "פיצול חדש" + +#: src/apprt/gtk/Window.zig:327 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ את/ה מריץ/ה גרסת ניפוי שגיאות של Ghostty! הביצועים יהיו ירודים." + +#: src/apprt/gtk/Window.zig:773 +msgid "Reloaded the configuration" +msgstr "ההגדרות הוטענו מחדש" + +#: src/apprt/gtk/Window.zig:1017 +msgid "Ghostty Developers" +msgstr "המפתחים של Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: בודק המסוף" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "סגירה" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "לצאת מGhostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "לסגור את החלון?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "לסגור את הכרטיסייה?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "לסגור את הפיצול?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "כל הפעלות המסוף יסתיימו." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "כל הפעלות המסוף בחלון זה יסתיימו." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "כל הפעלות המסוף בכרטיסייה זו יסתיימו." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "התהליך שרץ כרגע בפיצול זה יסתיים." + +#: src/apprt/gtk/Surface.zig:1257 +msgid "Copied to clipboard" +msgstr "הועתק ללוח ההעתקה" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index a4d6c1577..2ecae27ac 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{ "ca_ES.UTF-8", "bg_BG.UTF-8", "ga_IE.UTF-8", + "he_IL.UTF-8", }; /// Set for faster membership lookup of locales. From b10b0f06c329b865b72ee40c3122f009c457f2d4 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 15:53:59 -0600 Subject: [PATCH 47/86] font: remove unused fields from Glyph We can reintroduce `advance` if we ever want to do proportional string drawing, but we don't use it anywhere right now. And we also don't need `sprite` anymore since that was just there to disable constraints for sprites back when we did them on the GPU. --- src/font/Glyph.zig | 6 ------ src/font/face/coretext.zig | 6 ------ src/font/face/freetype.zig | 2 -- src/font/face/web_canvas.zig | 1 - src/font/sprite/Face.zig | 3 --- 5 files changed, 18 deletions(-) diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig index fa29e44fa..f99370271 100644 --- a/src/font/Glyph.zig +++ b/src/font/Glyph.zig @@ -17,9 +17,3 @@ offset_y: i32, /// be normalized to be between 0 and 1 prior to use in shaders. atlas_x: u32, atlas_y: u32, - -/// horizontal position to increase drawing position for strings -advance_x: f32, - -/// Whether we drew this glyph ourselves with the sprite font. -sprite: bool = false, diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 5c9c259d2..7d750b0d6 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -333,7 +333,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; const metrics = opts.grid_metrics; @@ -498,10 +497,6 @@ pub const Face = struct { break :offset_x result; }; - // Get our advance - var advances: [glyphs.len]macos.graphics.Size = undefined; - _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); - return .{ .width = px_width, .height = px_height, @@ -509,7 +504,6 @@ pub const Face = struct { .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = @floatCast(advances[0].width), }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index b27b28ab8..079cf5b2d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -373,7 +373,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; // For synthetic bold, we embolden the glyph. @@ -662,7 +661,6 @@ pub const Face = struct { .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = f26dot6ToFloat(glyph.*.advance.x), }; } diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig index 30540191d..7ea2f0426 100644 --- a/src/font/face/web_canvas.zig +++ b/src/font/face/web_canvas.zig @@ -235,7 +235,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = 0, }; } diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 1463fb38b..dfff8fa75 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -195,7 +195,6 @@ pub fn renderGlyph( .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; const metrics = self.metrics; @@ -227,8 +226,6 @@ pub fn renderGlyph( .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)), .atlas_x = region.x, .atlas_y = region.y, - .advance_x = @floatFromInt(width), - .sprite = true, }; } From d3aece21d8eb5985681f0d57ed7f280c9a7065b5 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 16:32:22 -0600 Subject: [PATCH 48/86] font: more generic bearing adjustments This generally adjusts the bearings of any glyph whose original advance was narrower than the cell, which helps a lot with proportional fallback glyphs so they aren't just left-aligned. This only applies to situations where the glyph was originally narrower than the cell, so that we don't mess up ligatures, and this centers the old advance width in the new one rather than adjusting proportionally, because otherwise we can mess up glyphs that are meant to align with others when placed vertically. --- src/font/face/coretext.zig | 48 +++++++++++++++++++++++++++----------- src/font/face/freetype.zig | 46 +++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7d750b0d6..6aedd7696 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -481,20 +481,42 @@ pub const Face = struct { // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = @intFromFloat(@round(x)); - - // If our cell was resized then we adjust our glyph's - // position relative to the new center. This keeps glyphs - // centered in the cell whether it was made wider or narrower. - if (metrics.original_cell_width) |original_width| { - const before: i32 = @intCast(original_width); - const after: i32 = @intCast(metrics.cell_width); - // Increase the offset by half of the difference - // between the widths to keep things centered. - result += @divTrunc(after - before, 2); + // If the glyph's advance is narrower than the cell width then we + // center the advance of the glyph within the cell width. At first + // I implemented this to proportionally scale the center position + // of the glyph but that messes up glyphs that are meant to align + // vertically with others, so this is a compromise. + // + // This makes it so that when the `adjust-cell-width` config is + // used, or when a fallback font with a different advance width + // is used, we don't get weirdly aligned glyphs. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if (opts.constraint.align_horizontal == .none) { + var advances: [glyphs.len]macos.graphics.Size = undefined; + _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); + const advance = advances[0].width; + const new_advance = + cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); + // If the original advance is greater than the cell width then + // it's possible that this is a ligature or other glyph that is + // intended to overflow the cell to one side or the other, and + // adjusting the bearings could mess that up, so we just leave + // it alone if that's the case. + // + // We also don't want to do anything if the advance is zero or + // less, since this is used for stuff like combining characters. + if (advance > new_advance or advance <= 0.0) { + break :offset_x @intFromFloat(@round(x)); + } + break :offset_x @intFromFloat( + @round(x + (new_advance - advance) / 2), + ); + } else { + break :offset_x @intFromFloat(@round(x)); } - - break :offset_x result; }; return .{ diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 079cf5b2d..6aeb951af 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -638,20 +638,40 @@ pub const Face = struct { // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = @intFromFloat(@floor(x)); - - // If our cell was resized then we adjust our glyph's - // position relative to the new center. This keeps glyphs - // centered in the cell whether it was made wider or narrower. - if (metrics.original_cell_width) |original_width| { - const before: i32 = @intCast(original_width); - const after: i32 = @intCast(metrics.cell_width); - // Increase the offset by half of the difference - // between the widths to keep things centered. - result += @divTrunc(after - before, 2); + // If the glyph's advance is narrower than the cell width then we + // center the advance of the glyph within the cell width. At first + // I implemented this to proportionally scale the center position + // of the glyph but that messes up glyphs that are meant to align + // vertically with others, so this is a compromise. + // + // This makes it so that when the `adjust-cell-width` config is + // used, or when a fallback font with a different advance width + // is used, we don't get weirdly aligned glyphs. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if (opts.constraint.align_horizontal == .none) { + const advance = f26dot6ToFloat(glyph.*.advance.x); + const new_advance = + cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); + // If the original advance is greater than the cell width then + // it's possible that this is a ligature or other glyph that is + // intended to overflow the cell to one side or the other, and + // adjusting the bearings could mess that up, so we just leave + // it alone if that's the case. + // + // We also don't want to do anything if the advance is zero or + // less, since this is used for stuff like combining characters. + if (advance > new_advance or advance <= 0.0) { + break :offset_x @intFromFloat(@floor(x)); + } + break :offset_x @intFromFloat( + @floor(x + (new_advance - advance) / 2), + ); + } else { + break :offset_x @intFromFloat(@floor(x)); } - - break :offset_x result; }; return Glyph{ From db08bf1655d10f5c459f41f2c01963bede98db55 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 16:37:15 -0600 Subject: [PATCH 49/86] font: adjust fallback font sizes to match primary metrics This better harmonizes fallback fonts with the primary font by matching the heights of lowercase letters. This should be a big improvement for users who use mixed scripts and so rely heavily on fallback fonts. --- src/font/Collection.zig | 169 ++++++++++++++++++++++++++++++++----- src/font/Metrics.zig | 12 +++ src/font/face/coretext.zig | 15 ++++ src/font/face/freetype.zig | 22 ++++- 4 files changed, 196 insertions(+), 22 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 8533331bc..cdbd3d84f 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -69,10 +69,14 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { if (self.load_options) |*v| v.deinit(alloc); } -pub const AddError = Allocator.Error || error{ - CollectionFull, - DeferredLoadingUnavailable, -}; +pub const AddError = + Allocator.Error || + AdjustSizeError || + error{ + CollectionFull, + DeferredLoadingUnavailable, + SetSizeFailed, + }; /// Add a face to the collection for the given style. This face will be added /// next in priority if others exist already, i.e. it'll be the _last_ to be @@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{ /// If no error is encountered then the collection takes ownership of the face, /// in which case face will be deallocated when the collection is deallocated. /// -/// If a loaded face is added to the collection, it should be the same -/// size as all the other faces in the collection. This function will not -/// verify or modify the size until the size of the entire collection is -/// changed. +/// If a loaded face is added to the collection, its size will be changed to +/// match the size specified in load_options, adjusted for harmonization with +/// the primary face. pub fn add( self: *Collection, alloc: Allocator, @@ -103,9 +106,106 @@ pub fn add( return error.DeferredLoadingUnavailable; try list.append(alloc, face); + + var owned: *Entry = list.at(idx); + + // If the face is already loaded, apply font size adjustment + // now, otherwise we'll apply it whenever we do load it. + if (owned.getLoaded()) |loaded| { + if (try self.adjustedSize(loaded)) |opts| { + loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed; + } + } + return .{ .style = style, .idx = @intCast(idx) }; } +pub const AdjustSizeError = font.Face.GetMetricsError; + +// Calculate a size for the provided face that will match it with the primary +// font, metrically, to improve consistency with fallback fonts. Right now we +// match the font based on the ex height, or the ideograph width if the font +// has ideographs in it. +// +// This returns null if load options is null or if self.load_options is null. +// +// +// This is very much like the `font-size-adjust` CSS property in how it works. +// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust +// +// TODO: In the future, provide config options that allow the user to select +// which metric should be matched for fallback fonts, instead of hard +// coding it as ex height. +pub fn adjustedSize( + self: *Collection, + face: *Face, +) AdjustSizeError!?LoadOptions { + const load_options = self.load_options orelse return null; + + // We silently do nothing if we can't get the primary + // face, because this might be the primary face itself. + const primary_face = self.getFace(.{ .idx = 0 }) catch return null; + + // We do nothing if the primary face and this face are the same. + if (@intFromPtr(primary_face) == @intFromPtr(face)) return null; + + const primary_metrics = try primary_face.getMetrics(); + const face_metrics = try face.getMetrics(); + + // We use the ex height to match our font sizes, so that the height of + // lower-case letters matches between all fonts in the fallback chain. + // + // We estimate ex height as 0.75 * cap height if it's not specifically + // provided, and we estimate cap height as 0.75 * ascent in the same case. + // + // If the fallback font has an ic_width we prefer that, for normalization + // of CJK font sizes when mixed with latin fonts. + // + // We estimate the ic_width as twice the cell width if it isn't provided. + var primary_cap = primary_metrics.cap_height orelse 0.0; + if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75; + + var primary_ex = primary_metrics.ex_height orelse 0.0; + if (primary_ex <= 0) primary_ex = primary_cap * 0.75; + + var primary_ic = primary_metrics.ic_width orelse 0.0; + if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2; + + var face_cap = face_metrics.cap_height orelse 0.0; + if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75; + + var face_ex = face_metrics.ex_height orelse 0.0; + if (face_ex <= 0) face_ex = face_cap * 0.75; + + var face_ic = face_metrics.ic_width orelse 0.0; + if (face_ic <= 0) face_ic = face_metrics.cell_width * 2; + + // If the line height of the scaled font would be larger than + // the line height of the primary font, we don't want that, so + // we take the minimum between matching the ic/ex and the line + // height. + // + // NOTE: We actually allow the line height to be up to 1.2 + // times the primary line height because empirically + // this is usually fine and is better for CJK. + // + // TODO: We should probably provide a config option that lets + // the user pick what metric to use for size adjustment. + const scale = @min( + 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(), + if (face_metrics.ic_width != null) + primary_ic / face_ic + else + primary_ex / face_ex, + ); + + // Make a copy of our load options and multiply the size by our scale. + var opts = load_options; + opts.size.points *= @as(f32, @floatCast(scale)); + + return opts; +} + /// Return the Face represented by a given Index. The returned pointer /// is only valid as long as this collection is not modified. /// @@ -129,21 +229,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face { break :item item; }; - return try self.getFaceFromEntry(item); + const face = try self.getFaceFromEntry( + item, + // We only want to adjust the size if this isn't the primary face. + index.style != .regular or index.idx > 0, + ); + + return face; } /// Get the face from an entry. /// /// This entry must not be an alias. -fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face { +fn getFaceFromEntry( + self: *Collection, + entry: *Entry, + /// Whether to adjust the font size to match the primary face after loading. + adjust: bool, +) !*Face { assert(entry.* != .alias); return switch (entry.*) { inline .deferred, .fallback_deferred => |*d, tag| deferred: { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; - const face = try d.load(opts.library, opts.faceOptions()); + var face = try d.load(opts.library, opts.faceOptions()); d.deinit(); + + // If we need to adjust the size, do so. + if (adjust) if (try self.adjustedSize(&face)) |new_opts| { + try face.setSize(new_opts.faceOptions()); + }; + entry.* = switch (tag) { .deferred => .{ .loaded = face }, .fallback_deferred => .{ .fallback_loaded = face }, @@ -247,7 +364,7 @@ pub fn completeStyles( while (it.next()) |entry| { // Load our face. If we fail to load it, we just skip it and // continue on to try the next one. - const face = self.getFaceFromEntry(entry) catch |err| { + const face = self.getFaceFromEntry(entry, false) catch |err| { log.warn("error loading regular entry={d} err={}", .{ it.index - 1, err, @@ -371,7 +488,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to bold it. - const regular = try self.getFaceFromEntry(entry); + const regular = try self.getFaceFromEntry(entry, false); const face = try regular.syntheticBold(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -391,7 +508,7 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to italicize it. - const regular = try self.getFaceFromEntry(entry); + const regular = try self.getFaceFromEntry(entry, false); const face = try regular.syntheticItalic(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -420,9 +537,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { while (it.next()) |array| { var entry_it = array.value.iterator(0); while (entry_it.next()) |entry| switch (entry.*) { - .loaded, .fallback_loaded => |*f| try f.setSize( - opts.faceOptions(), - ), + .loaded, + .fallback_loaded, + => |*f| { + const new_opts = try self.adjustedSize(f) orelse opts.*; + try f.setSize(new_opts.faceOptions()); + }, // Deferred aren't loaded so we don't need to set their size. // The size for when they're loaded is set since `opts` changed. @@ -549,6 +669,16 @@ pub const Entry = union(enum) { } } + /// If this face is loaded, or is an alias to a loaded face, + /// then this returns the `Face`, otherwise returns null. + pub fn getLoaded(self: *Entry) ?*Face { + return switch (self.*) { + .deferred, .fallback_deferred => null, + .loaded, .fallback_loaded => |*face| face, + .alias => |v| v.getLoaded(), + }; + } + /// True if the entry is deferred. fn isDeferred(self: Entry) bool { return switch (self) { @@ -906,12 +1036,13 @@ test "metrics" { var c = init(); defer c.deinit(alloc); - c.load_options = .{ .library = lib }; + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + .{ .size = size }, ) }); try c.updateMetrics(); diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index bf527a021..069606c06 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -107,6 +107,18 @@ pub const FaceMetrics = struct { /// a provided ex height metric or measured from the height of the /// lowercase x glyph. ex_height: ?f64 = null, + + /// The width of the character "水" (CJK water ideograph, U+6C34), + /// if present. This is used for font size adjustment, to normalize + /// the width of CJK fonts mixed with latin fonts. + /// + /// NOTE: IC = Ideograph Character + ic_width: ?f64 = null, + + /// Convenience function for getting the line height (ascent - descent). + pub inline fn lineHeight(self: FaceMetrics) f64 { + return self.ascent - self.descent; + } }; /// Calculate our metrics based on values extracted from a font. diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 6aedd7696..c1f16e025 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -757,6 +757,20 @@ pub const Face = struct { break :cell_width max; }; + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + const glyph = self.glyphIndex('水') orelse break :ic_width null; + + var advances: [1]macos.graphics.Size = undefined; + _ = ct_font.getAdvancesForGlyphs( + .horizontal, + &.{@intCast(glyph)}, + &advances, + ); + + break :ic_width advances[0].width; + }; + return .{ .cell_width = cell_width, .ascent = ascent, @@ -768,6 +782,7 @@ pub const Face = struct { .strikethrough_thickness = strikethrough_thickness, .cap_height = cap_height, .ex_height = ex_height, + .ic_width = ic_width, }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 6aeb951af..db5a3622e 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -851,7 +851,7 @@ pub const Face = struct { while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { max = @max( @@ -889,7 +889,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); @@ -902,7 +902,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); @@ -913,6 +913,21 @@ pub const Face = struct { }; }; + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + const glyph = face.getCharIndex('水') orelse break :ic_width null; + + face.loadGlyph(glyph, .{ + .render = false, + .no_svg = true, + }) catch break :ic_width null; + + break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x); + }; + return .{ .cell_width = cell_width, @@ -928,6 +943,7 @@ pub const Face = struct { .cap_height = cap_height, .ex_height = ex_height, + .ic_width = ic_width, }; } From b7ffbf933f1b2274a14f6471eb932fee0987096a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 6 Jul 2025 20:23:20 -0700 Subject: [PATCH 50/86] macos: open URLs with NSWorkspace APIs instead of `open` Fixes #5256 This updates the macOS apprt to implement the `OPEN_URL` apprt action to use the NSWorkspace APIs instead of the `open` command line utility. As part of this, we removed the `ghostty_config_open` libghostty API and instead introduced a new `ghostty_config_open_path` API that returns the path to open, and then we use the `NSWorkspace` APIs to open it (same function as the `OPEN_URL` action). --- include/ghostty.h | 8 ++- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ macos/Sources/App/macOS/AppDelegate.swift | 2 +- macos/Sources/Ghostty/Ghostty.Action.swift | 30 +++++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 51 +++++++++++++++++-- macos/Sources/Ghostty/Package.swift | 20 ++++++++ .../Extensions/NSWorkspace+Extension.swift | 29 +++++++++++ src/apprt/gtk/App.zig | 19 ++++++- src/config/CAPI.zig | 31 ++++++----- src/config/edit.zig | 13 ++--- src/main_c.zig | 34 ++++++++++++- 11 files changed, 212 insertions(+), 29 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift diff --git a/include/ghostty.h b/include/ghostty.h index 2a4a7fb6e..312e6595a 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -350,6 +350,11 @@ typedef struct { const char* message; } ghostty_diagnostic_s; +typedef struct { + const char* ptr; + uintptr_t len; +} ghostty_string_s; + typedef struct { double tl_px_x; double tl_px_y; @@ -797,6 +802,7 @@ int ghostty_init(uintptr_t, char**); void ghostty_cli_try_action(void); ghostty_info_s ghostty_info(void); const char* ghostty_translate(const char*); +void ghostty_string_free(ghostty_string_s); ghostty_config_t ghostty_config_new(); void ghostty_config_free(ghostty_config_t); @@ -811,7 +817,7 @@ ghostty_input_trigger_s ghostty_config_trigger(ghostty_config_t, uintptr_t); uint32_t ghostty_config_diagnostics_count(ghostty_config_t); ghostty_diagnostic_s ghostty_config_get_diagnostic(ghostty_config_t, uint32_t); -void ghostty_config_open(); +ghostty_string_s ghostty_config_open_path(void); ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*, ghostty_config_t); diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 08c3ef3b3..f6eedd864 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; A505D21D2E1A2FA20018808F /* FileHandle+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */; }; + A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */; }; A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; }; A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; }; A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; }; @@ -160,6 +161,7 @@ 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = ""; }; A505D21C2E1A2F9E0018808F /* FileHandle+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileHandle+Extension.swift"; sourceTree = ""; }; + A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Extension.swift"; sourceTree = ""; }; A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; }; A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; }; A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = ""; }; @@ -531,6 +533,7 @@ AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, + A505D21E2E1B6DDC0018808F /* NSWorkspace+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, A58636722DF4813000E04A10 /* UndoManager+Extension.swift */, A5CC36142C9CDA03004D6760 /* View+Extension.swift */, @@ -819,6 +822,7 @@ A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, + A505D21F2E1B6DE00018808F /* NSWorkspace+Extension.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 53b6dce88..38500b7d3 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -932,7 +932,7 @@ class AppDelegate: NSObject, //MARK: - IB Actions @IBAction func openConfig(_ sender: Any?) { - ghostty.openConfig() + Ghostty.App.openConfig() } @IBAction func reloadConfig(_ sender: Any?) { diff --git a/macos/Sources/Ghostty/Ghostty.Action.swift b/macos/Sources/Ghostty/Ghostty.Action.swift index dfdb0bff5..a6559600d 100644 --- a/macos/Sources/Ghostty/Ghostty.Action.swift +++ b/macos/Sources/Ghostty/Ghostty.Action.swift @@ -40,4 +40,34 @@ extension Ghostty.Action { self.amount = c.amount } } + + struct OpenURL { + enum Kind { + case unknown + case text + + init(_ c: ghostty_action_open_url_kind_e) { + switch c { + case GHOSTTY_ACTION_OPEN_URL_KIND_TEXT: + self = .text + default: + self = .unknown + } + } + } + + let kind: Kind + let url: String + + init(c: ghostty_action_open_url_s) { + self.kind = Kind(c.kind) + + if let urlCString = c.url { + let data = Data(bytes: urlCString, count: Int(c.len)) + self.url = String(data: data, encoding: .utf8) ?? "" + } else { + self.url = "" + } + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 17abe2b0e..0fdea1760 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -114,9 +114,21 @@ extension Ghostty { ghostty_app_tick(app) } - func openConfig() { - guard let app = self.app else { return } - ghostty_app_open_config(app) + static func openConfig() { + let str = Ghostty.AllocatedString(ghostty_config_open_path()).string + guard !str.isEmpty else { return } + #if os(macOS) + let fileURL = URL(fileURLWithPath: str).absoluteString + var action = ghostty_action_open_url_s() + action.kind = GHOSTTY_ACTION_OPEN_URL_KIND_TEXT + fileURL.withCString { cStr in + action.url = cStr + action.len = UInt(fileURL.count) + _ = openURL(action) + } + #else + fatalError("Unsupported platform for opening config file") + #endif } /// Reload the configuration. @@ -488,7 +500,7 @@ extension Ghostty { pwdChanged(app, target: target, v: action.action.pwd) case GHOSTTY_ACTION_OPEN_CONFIG: - ghostty_config_open() + openConfig() case GHOSTTY_ACTION_FLOAT_WINDOW: toggleFloatWindow(app, target: target, mode: action.action.float_window) @@ -546,6 +558,9 @@ extension Ghostty { case GHOSTTY_ACTION_CHECK_FOR_UPDATES: checkForUpdates(app) + + case GHOSTTY_ACTION_OPEN_URL: + return openURL(action.action.open_url) case GHOSTTY_ACTION_UNDO: return undo(app, target: target) @@ -598,6 +613,34 @@ extension Ghostty { appDelegate.checkForUpdates(nil) } } + + private static func openURL( + _ v: ghostty_action_open_url_s + ) -> Bool { + let action = Ghostty.Action.OpenURL(c: v) + + // Convert the URL string to a URL object + guard let url = URL(string: action.url) else { + Ghostty.logger.warning("invalid URL for open URL action: \(action.url)") + return false + } + + switch action.kind { + case .text: + // Open with the default text editor + if let textEditor = NSWorkspace.shared.defaultTextEditor { + NSWorkspace.shared.open([url], withApplicationAt: textEditor, configuration: NSWorkspace.OpenConfiguration()) + return true + } + + case .unknown: + break + } + + // Open with the default application for the URL + NSWorkspace.shared.open(url) + return true + } private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { let undoManager: UndoManager? diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index f30f2f6f9..9b05934df 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -73,6 +73,26 @@ extension Ghostty { // MARK: Swift Types for C Types +extension Ghostty { + class AllocatedString { + private let cString: ghostty_string_s + + init(_ c: ghostty_string_s) { + self.cString = c + } + + var string: String { + guard let ptr = cString.ptr else { return "" } + let data = Data(bytes: ptr, count: Int(cString.len)) + return String(data: data, encoding: .utf8) ?? "" + } + + deinit { + ghostty_string_free(cString) + } + } +} + extension Ghostty { enum SetFloatWIndow { case on diff --git a/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift new file mode 100644 index 000000000..bc2d028b5 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/NSWorkspace+Extension.swift @@ -0,0 +1,29 @@ +import AppKit +import UniformTypeIdentifiers + +extension NSWorkspace { + /// Returns the URL of the default text editor application. + /// - Returns: The URL of the default text editor, or nil if no default text editor is found. + var defaultTextEditor: URL? { + defaultApplicationURL(forContentType: UTType.plainText.identifier) + } + + /// Returns the URL of the default application for opening files with the specified content type. + /// - Parameter contentType: The content type identifier (UTI) to find the default application for. + /// - Returns: The URL of the default application, or nil if no default application is found. + func defaultApplicationURL(forContentType contentType: String) -> URL? { + return LSCopyDefaultApplicationURLForContentType( + contentType as CFString, + .all, + nil + )?.takeRetainedValue() as? URL + } + + /// Returns the URL of the default application for opening files with the specified file extension. + /// - Parameter ext: The file extension to find the default application for. + /// - Returns: The URL of the default application, or nil if no default application is found. + func defaultApplicationURL(forExtension ext: String) -> URL? { + guard let uti = UTType(filenameExtension: ext) else { return nil} + return defaultApplicationURL(forContentType: uti.identifier) + } +} diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 369090ee2..907f3a36d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -496,7 +496,7 @@ pub fn performAction( .resize_split => self.resizeSplit(target, value), .equalize_splits => self.equalizeSplits(target), .goto_split => return self.gotoSplit(target, value), - .open_config => try configpkg.edit.open(self.core_app.alloc), + .open_config => return self.openConfig(), .config_change => self.configChange(target, value.config), .reload_config => try self.reloadConfig(target, value), .inspector => self.controlInspector(target, value), @@ -1759,7 +1759,22 @@ fn initActions(self: *App) void { } } -pub fn openUrl( +fn openConfig(self: *App) !bool { + // Get the config file path + const alloc = self.core_app.alloc; + const path = configpkg.edit.openPath(alloc) catch |err| { + log.warn("error getting config file path: {}", .{err}); + return false; + }; + defer alloc.free(path); + + // Open it using openURL. "path" isn't actually a URL but + // at the time of writing that works just fine for GTK. + self.openUrl(.{ .kind = .text, .url = path }); + return true; +} + +fn openUrl( app: *App, value: apprt.action.OpenUrl, ) void { diff --git a/src/config/CAPI.zig b/src/config/CAPI.zig index 0b7108a59..bdc59797a 100644 --- a/src/config/CAPI.zig +++ b/src/config/CAPI.zig @@ -1,7 +1,9 @@ const std = @import("std"); +const assert = std.debug.assert; const cli = @import("../cli.zig"); const inputpkg = @import("../input.zig"); -const global = &@import("../global.zig").state; +const state = &@import("../global.zig").state; +const c = @import("../main_c.zig"); const Config = @import("Config.zig"); const c_get = @import("c_get.zig"); @@ -12,14 +14,14 @@ const log = std.log.scoped(.config); /// Create a new configuration filled with the initial default values. export fn ghostty_config_new() ?*Config { - const result = global.alloc.create(Config) catch |err| { + const result = state.alloc.create(Config) catch |err| { log.err("error allocating config err={}", .{err}); return null; }; - result.* = Config.default(global.alloc) catch |err| { + result.* = Config.default(state.alloc) catch |err| { log.err("error creating config err={}", .{err}); - global.alloc.destroy(result); + state.alloc.destroy(result); return null; }; @@ -29,20 +31,20 @@ export fn ghostty_config_new() ?*Config { export fn ghostty_config_free(ptr: ?*Config) void { if (ptr) |v| { v.deinit(); - global.alloc.destroy(v); + state.alloc.destroy(v); } } /// Deep clone the configuration. export fn ghostty_config_clone(self: *Config) ?*Config { - const result = global.alloc.create(Config) catch |err| { + const result = state.alloc.create(Config) catch |err| { log.err("error allocating config err={}", .{err}); return null; }; - result.* = self.clone(global.alloc) catch |err| { + result.* = self.clone(state.alloc) catch |err| { log.err("error cloning config err={}", .{err}); - global.alloc.destroy(result); + state.alloc.destroy(result); return null; }; @@ -51,7 +53,7 @@ export fn ghostty_config_clone(self: *Config) ?*Config { /// Load the configuration from the CLI args. export fn ghostty_config_load_cli_args(self: *Config) void { - self.loadCliArgs(global.alloc) catch |err| { + self.loadCliArgs(state.alloc) catch |err| { log.err("error loading config err={}", .{err}); }; } @@ -60,7 +62,7 @@ export fn ghostty_config_load_cli_args(self: *Config) void { /// is usually done first. The default file locations are locations /// such as the home directory. export fn ghostty_config_load_default_files(self: *Config) void { - self.loadDefaultFiles(global.alloc) catch |err| { + self.loadDefaultFiles(state.alloc) catch |err| { log.err("error loading config err={}", .{err}); }; } @@ -69,7 +71,7 @@ export fn ghostty_config_load_default_files(self: *Config) void { /// file locations in the previously loaded configuration. This will /// recursively continue to load up to a built-in limit. export fn ghostty_config_load_recursive_files(self: *Config) void { - self.loadRecursiveFiles(global.alloc) catch |err| { + self.loadRecursiveFiles(state.alloc) catch |err| { log.err("error loading config err={}", .{err}); }; } @@ -122,10 +124,13 @@ export fn ghostty_config_get_diagnostic(self: *Config, idx: u32) Diagnostic { return .{ .message = message.ptr }; } -export fn ghostty_config_open() void { - edit.open(global.alloc) catch |err| { +export fn ghostty_config_open_path() c.String { + const path = edit.openPath(state.alloc) catch |err| { log.err("error opening config in editor err={}", .{err}); + return .empty; }; + + return .fromSlice(path); } /// Sync with ghostty_diagnostic_s diff --git a/src/config/edit.zig b/src/config/edit.zig index ae4394942..38dc98169 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -5,18 +5,19 @@ const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const internal_os = @import("../os/main.zig"); -/// Open the configuration in the OS default editor according to the default -/// paths the main config file could be in. +/// The path to the configuration that should be opened for editing. /// -/// On Linux, this will open the file at the XDG config path. This is the +/// On Linux, this will use the file at the XDG config path. This is the /// only valid path for Linux so we don't need to check for other paths. /// /// On macOS, both XDG and AppSupport paths are valid. Because Ghostty -/// prioritizes AppSupport over XDG, we will open AppSupport if it exists, +/// prioritizes AppSupport over XDG, we will use AppSupport if it exists, /// followed by XDG if it exists, and finally AppSupport if neither exist. /// For the existence check, we also prefer non-empty files over empty /// files. -pub fn open(alloc_gpa: Allocator) !void { +/// +/// The returned value is allocated using the provided allocator. +pub fn openPath(alloc_gpa: Allocator) ![:0]const u8 { // Use an arena to make memory management easier in here. var arena = ArenaAllocator.init(alloc_gpa); defer arena.deinit(); @@ -41,7 +42,7 @@ pub fn open(alloc_gpa: Allocator) !void { } }; - try internal_os.open(alloc_gpa, .text, config_path); + return try alloc_gpa.dupeZ(u8, config_path); } /// Returns the config path to use for open for the current OS. diff --git a/src/main_c.zig b/src/main_c.zig index 0722900e7..2c266cfb5 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -19,7 +19,12 @@ const internal_os = @import("os/main.zig"); // Some comptime assertions that our C API depends on. comptime { - assert(apprt.runtime == apprt.embedded); + // We allow tests to reference this file because we unit test + // some of the C API. At runtime though we should never get these + // functions unless we are building libghostty. + if (!builtin.is_test) { + assert(apprt.runtime == apprt.embedded); + } } /// Global options so we can log. This is identical to main. @@ -29,7 +34,9 @@ comptime { // These structs need to be referenced so the `export` functions // are truly exported by the C API lib. _ = @import("config.zig").CAPI; - _ = apprt.runtime.CAPI; + if (@hasDecl(apprt.runtime, "CAPI")) { + _ = apprt.runtime.CAPI; + } } /// ghostty_info_s @@ -46,6 +53,24 @@ const Info = extern struct { }; }; +/// ghostty_string_s +pub const String = extern struct { + ptr: ?[*]const u8, + len: usize, + + pub const empty: String = .{ + .ptr = null, + .len = 0, + }; + + pub fn fromSlice(slice: []const u8) String { + return .{ + .ptr = slice.ptr, + .len = slice.len, + }; + } +}; + /// Initialize ghostty global state. export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { assert(builtin.link_libc); @@ -95,3 +120,8 @@ export fn ghostty_info() Info { export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { return internal_os.i18n._(msgid); } + +/// Free a string allocated by Ghostty. +export fn ghostty_string_free(str: String) void { + state.alloc.free(str.ptr.?[0..str.len]); +} From d33161ad66d46e5ca4d7f41a11abd85b9937a406 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 22:40:43 -0600 Subject: [PATCH 51/86] fix(font): include line gap in `lineHeight` helper --- src/font/Metrics.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 069606c06..f96d753b3 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -115,9 +115,10 @@ pub const FaceMetrics = struct { /// NOTE: IC = Ideograph Character ic_width: ?f64 = null, - /// Convenience function for getting the line height (ascent - descent). + /// Convenience function for getting the line height + /// (ascent - descent + line_gap). pub inline fn lineHeight(self: FaceMetrics) f64 { - return self.ascent - self.descent; + return self.ascent - self.descent + self.line_gap; } }; From 08fd1688ff451c4cf2d1cc3e4864f05e71cefbdc Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 22:45:13 -0600 Subject: [PATCH 52/86] font: add test for size adjustment, fix small bug in resize Previously produced very wrong values when calling Collection.setSize, since it was assuming that the provided face had the same point size as the primary face, which isn't true during resize-- so instead we just have faces keep track of their set size, this is generally useful. --- src/font/Collection.zig | 64 ++++++++++++++++++++++++++++++++++++-- src/font/face/coretext.zig | 4 +++ src/font/face/freetype.zig | 5 +++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index cdbd3d84f..1d85d8a28 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -129,7 +129,6 @@ pub const AdjustSizeError = font.Face.GetMetricsError; // // This returns null if load options is null or if self.load_options is null. // -// // This is very much like the `font-size-adjust` CSS property in how it works. // ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust // @@ -199,8 +198,10 @@ pub fn adjustedSize( primary_ex / face_ex, ); - // Make a copy of our load options and multiply the size by our scale. + // Make a copy of our load options, set the size to the size of + // the provided face, and then multiply that by our scaling factor. var opts = load_options; + opts.size = face.size; opts.size.points *= @as(f32, @floatCast(scale)); return opts; @@ -1089,3 +1090,62 @@ test "metrics" { .cursor_height = 34, }, c.metrics); } + +// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font. +test "adjusted sizes" { + const testing = std.testing; + const alloc = testing.allocator; + const testFont = font.embedded.inconsolata; + const fallback = font.embedded.monaspace_neon; + + var lib = try Library.init(alloc); + defer lib.deinit(); + + var c = init(); + defer c.deinit(alloc); + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; + + // Add our primary face. + _ = try c.add(alloc, .regular, .{ .loaded = try .init( + lib, + testFont, + .{ .size = size }, + ) }); + + try c.updateMetrics(); + + // Add the fallback face. + const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init( + lib, + fallback, + .{ .size = size }, + ) }); + + // The ex heights should match. + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqAbs( + primary_metrics.ex_height.?, + fallback_metrics.ex_height.?, + // We accept anything within half a pixel. + 0.5, + ); + } + + // Resize should keep that relationship. + try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqAbs( + primary_metrics.ex_height.?, + fallback_metrics.ex_height.?, + // We accept anything within half a pixel. + 0.5, + ); + } +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index c1f16e025..00cc31b26 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -31,6 +31,9 @@ pub const Face = struct { /// tables). color: ?ColorState = null, + /// The current size this font is set to. + size: font.face.DesiredSize, + /// True if our build is using Harfbuzz. If we're not, we can avoid /// some Harfbuzz-specific code paths. const harfbuzz_shaper = font.options.backend.hasHarfbuzz(); @@ -106,6 +109,7 @@ pub const Face = struct { .font = ct_font, .hb_font = hb_font, .color = color, + .size = opts.size, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index db5a3622e..ae3bd0968 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -59,6 +59,9 @@ pub const Face = struct { bold: bool = false, } = .{}, + /// The current size this font is set to. + size: font.face.DesiredSize, + /// Initialize a new font face with the given source in-memory. pub fn initFile( lib: Library, @@ -107,6 +110,7 @@ pub const Face = struct { .hb_font = hb_font, .ft_mutex = ft_mutex, .load_flags = opts.freetype_load_flags, + .size = opts.size, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -203,6 +207,7 @@ pub const Face = struct { /// for clearing any glyph caches, font atlas data, etc. pub fn setSize(self: *Face, opts: font.face.Options) !void { try setSize_(self.face, opts.size); + self.size = opts.size; } fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void { From e1e2f823ba020c9b60b76aaabac2bee097f5566d Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 08:57:20 -0600 Subject: [PATCH 53/86] font/coretext: fix horizontal bearing calculation This was subtly wrong in a way that was most obvious when text switched from regular to bold, where it would seem to wiggle since the bearings of each letter would shift by a pixel in either direction. This affected applications like fzf which uses bold to dynamically highlight the line you have selected. --- src/font/face/coretext.zig | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 00cc31b26..83f993715 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -366,8 +366,10 @@ pub const Face = struct { // of extra width to the area that's drawn in beyond just the width of // the glyph itself, so we include that extra fraction of a pixel when // calculating the width and height here. - const px_width: u32 = @intFromFloat(@ceil(width + rect.origin.x - @floor(rect.origin.x))); - const px_height: u32 = @intFromFloat(@ceil(height + rect.origin.y - @floor(rect.origin.y))); + const frac_x = rect.origin.x - @floor(rect.origin.x); + const frac_y = rect.origin.y - @floor(rect.origin.y); + const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); + const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -513,13 +515,13 @@ pub const Face = struct { // We also don't want to do anything if the advance is zero or // less, since this is used for stuff like combining characters. if (advance > new_advance or advance <= 0.0) { - break :offset_x @intFromFloat(@round(x)); + break :offset_x @intFromFloat(@ceil(x - frac_x)); } break :offset_x @intFromFloat( - @round(x + (new_advance - advance) / 2), + @ceil(x - frac_x + (new_advance - advance) / 2), ); } else { - break :offset_x @intFromFloat(@round(x)); + break :offset_x @intFromFloat(@ceil(x - frac_x)); } }; From 23cc50b12c1b670ff3f96c63437f61742d3b4d3c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 09:09:37 -0600 Subject: [PATCH 54/86] font/Metrics: remove original_cell_width, no longer needed --- src/font/Metrics.zig | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index f96d753b3..097f600cb 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -35,10 +35,6 @@ cursor_thickness: u32 = 1, /// The height in pixels of the cursor sprite. cursor_height: u32, -/// Original cell width in pixels. This is used to keep -/// glyphs centered if the cell width is adjusted wider. -original_cell_width: ?u32 = null, - /// Minimum acceptable values for some fields to prevent modifiers /// from being able to, for example, cause 0-thickness underlines. const Minimums = struct { @@ -214,11 +210,6 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void { const new = @max(entry.value_ptr.apply(original), 1); if (new == original) continue; - // Preserve the original cell width if not set. - if (self.original_cell_width == null) { - self.original_cell_width = self.cell_width; - } - // Set the new value @field(self, @tagName(tag)) = new; From 5b1d3903796683112bbf4255a0825b839f4525e8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 7 Jul 2025 10:29:54 -0500 Subject: [PATCH 55/86] gtk: rebuild gresources.c/h if CSS or icons change --- src/build/SharedDeps.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0aab5ecf8..b6e9900e2 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -760,6 +760,9 @@ pub fn gtkDistResources( }); const resources_c = generate_c.addOutputFileArg("ghostty_resources.c"); generate_c.addFileArg(gresource_xml); + for (gresource.dependencies) |file| { + generate_c.addFileInput(b.path(file)); + } const generate_h = b.addSystemCommand(&.{ "glib-compile-resources", @@ -770,6 +773,9 @@ pub fn gtkDistResources( }); const resources_h = generate_h.addOutputFileArg("ghostty_resources.h"); generate_h.addFileArg(gresource_xml); + for (gresource.dependencies) |file| { + generate_h.addFileInput(b.path(file)); + } return .{ .resources_c = .{ From c47459b4a2887a8301927a92368f01da738743e3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 09:34:56 -0600 Subject: [PATCH 56/86] font: add icon height to nerd font constraints Icons were often WAY too big before because they were filling the whole cell in height, which isn't great lol. This commit adds an `icon_height` metric which is used to constrain glyphs that shouldn't be the size of the entire cell. --- src/font/Collection.zig | 2 ++ src/font/Metrics.zig | 19 +++++++++++- src/font/face.zig | 51 +++++++++++++++++++++---------- src/font/face/coretext.zig | 5 ++- src/font/face/freetype.zig | 6 ++-- src/font/nerd_font_attributes.zig | 6 ++-- src/font/nerd_font_codegen.py | 11 ++++--- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 1d85d8a28..eb4349fb0 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -1072,6 +1072,7 @@ test "metrics" { .overline_thickness = 1, .box_thickness = 1, .cursor_height = 17, + .icon_height = 11, }, c.metrics); // Resize should change metrics @@ -1088,6 +1089,7 @@ test "metrics" { .overline_thickness = 2, .box_thickness = 2, .cursor_height = 34, + .icon_height = 23, }, c.metrics); } diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 097f600cb..89f6a507f 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -35,6 +35,9 @@ cursor_thickness: u32 = 1, /// The height in pixels of the cursor sprite. cursor_height: u32, +/// The constraint height for nerd fonts icons. +icon_height: u32, + /// Minimum acceptable values for some fields to prevent modifiers /// from being able to, for example, cause 0-thickness underlines. const Minimums = struct { @@ -46,6 +49,7 @@ const Minimums = struct { const box_thickness = 1; const cursor_thickness = 1; const cursor_height = 1; + const icon_height = 1; }; /// Metrics extracted from a font face, based on @@ -129,7 +133,7 @@ pub fn calc(face: FaceMetrics) Metrics { // that the cell is large enough for the provided size, since we cast // it to an integer later. const cell_width = @ceil(face.cell_width); - const cell_height = @ceil(face.ascent - face.descent + face.line_gap); + const cell_height = @ceil(face.lineHeight()); // We split our line gap in two parts, and put half of it on the top // of the cell and the other half on the bottom, so that our text never @@ -173,6 +177,17 @@ pub fn calc(face: FaceMetrics) Metrics { (face.strikethrough_position orelse ex_height * 0.5 + strikethrough_thickness * 0.5)); + // The calculation for icon height in the nerd fonts patcher + // is two thirds cap height to one third line height, but we + // use an opinionated default of 1.2 * cap height instead. + // + // Doing this prevents fonts with very large line heights + // from having excessively oversized icons, and allows fonts + // with very small line heights to still have roomy icons. + // + // We do cap it at `cell_height` though for obvious reasons. + const icon_height = @min(cell_height, cap_height * 1.2); + var result: Metrics = .{ .cell_width = @intFromFloat(cell_width), .cell_height = @intFromFloat(cell_height), @@ -185,6 +200,7 @@ pub fn calc(face: FaceMetrics) Metrics { .overline_thickness = @intFromFloat(underline_thickness), .box_thickness = @intFromFloat(underline_thickness), .cursor_height = @intFromFloat(cell_height), + .icon_height = @intFromFloat(icon_height), }; // Ensure all metrics are within their allowable range. @@ -423,6 +439,7 @@ fn init() Metrics { .overline_thickness = 0, .box_thickness = 0, .cursor_height = 0, + .icon_height = 0, }; } diff --git a/src/font/face.zig b/src/font/face.zig index 245edcf4b..8c1171fb4 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -150,6 +150,9 @@ pub const RenderOptions = struct { /// Maximum number of cells horizontally to use. max_constraint_width: u2 = 2, + /// What to use as the height metric when constraining the glyph. + height: Height = .cell, + pub const Size = enum { /// Don't change the size of this glyph. none, @@ -176,6 +179,13 @@ pub const RenderOptions = struct { center, }; + pub const Height = enum { + /// Use the full height of the cell for constraining this glyph. + cell, + /// Use the "icon height" from the grid metrics as the height. + icon, + }; + /// The size and position of a glyph. pub const GlyphSize = struct { width: f64, @@ -189,35 +199,35 @@ pub const RenderOptions = struct { pub fn constrain( self: Constraint, glyph: GlyphSize, - /// Width of one cell. - cell_width: f64, - /// Height of one cell. - cell_height: f64, + metrics: Metrics, /// Number of cells horizontally available for this glyph. constraint_width: u2, ) GlyphSize { var g = glyph; - const available_width = - cell_width * @as(f64, @floatFromInt( - @min( - self.max_constraint_width, - constraint_width, - ), - )); + const available_width: f64 = @floatFromInt( + metrics.cell_width * @min( + self.max_constraint_width, + constraint_width, + ), + ); + const available_height: f64 = @floatFromInt(switch (self.height) { + .cell => metrics.cell_height, + .icon => metrics.icon_height, + }); const w = available_width - self.pad_left * available_width - self.pad_right * available_width; - const h = cell_height - - self.pad_top * cell_height - - self.pad_bottom * cell_height; + const h = available_height - + self.pad_top * available_height - + self.pad_bottom * available_height; // Subtract padding from the bearings so that our // alignment and sizing code works correctly. We // re-add before returning. g.x -= self.pad_left * available_width; - g.y -= self.pad_bottom * cell_height; + g.y -= self.pad_bottom * available_height; switch (self.size_horizontal) { .none => {}, @@ -319,7 +329,16 @@ pub const RenderOptions = struct { // Re-add our padding before returning. g.x += self.pad_left * available_width; - g.y += self.pad_bottom * cell_height; + g.y += self.pad_bottom * available_height; + + // If the available height is less than the cell height, we + // add half of the difference to center it in the full height. + // + // If necessary, in the future, we can adjust this to account + // for alignment, but that isn't necessary with any of the nf + // icons afaict. + const cell_height: f64 = @floatFromInt(metrics.cell_height); + g.y += (cell_height - available_height) / 2; return g; } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 00cc31b26..009b4b2b3 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -341,7 +341,7 @@ pub const Face = struct { const metrics = opts.grid_metrics; const cell_width: f64 = @floatFromInt(metrics.cell_width); - const cell_height: f64 = @floatFromInt(metrics.cell_height); + // const cell_height: f64 = @floatFromInt(metrics.cell_height); const glyph_size = opts.constraint.constrain( .{ @@ -350,8 +350,7 @@ pub const Face = struct { .x = rect.origin.x, .y = rect.origin.y + @as(f64, @floatFromInt(metrics.cell_baseline)), }, - cell_width, - cell_height, + metrics, opts.constraint_width, ); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index ae3bd0968..f42868e5c 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -395,7 +395,7 @@ pub const Face = struct { const metrics = opts.grid_metrics; const cell_width: f64 = @floatFromInt(metrics.cell_width); - const cell_height: f64 = @floatFromInt(metrics.cell_height); + // const cell_height: f64 = @floatFromInt(metrics.cell_height); const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX); const glyph_y: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingY) - glyph_height; @@ -407,8 +407,7 @@ pub const Face = struct { .x = glyph_x, .y = glyph_y + @as(f64, @floatFromInt(metrics.cell_baseline)), }, - cell_width, - cell_height, + metrics, opts.constraint_width, ); @@ -1058,6 +1057,7 @@ test "color emoji" { .overline_thickness = 0, .box_thickness = 0, .cursor_height = 0, + .icon_height = 0, }, .constraint_width = 2, .constraint = .{ .size_horizontal = .cover, .size_vertical = .cover, diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 70920bb0a..e72c7a00e 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -25,6 +25,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .cover, .size_vertical = .fit, + .height = .icon, .max_constraint_width = 1, .align_horizontal = .center, .align_vertical = .center, @@ -285,7 +286,7 @@ pub fn getConstraint(cp: u21) Constraint { 0xe0d0...0xe0d1, => .{ .size_horizontal = .cover, - .size_vertical = .cover, + .size_vertical = .fit, .align_horizontal = .start, .align_vertical = .center, }, @@ -294,7 +295,7 @@ pub fn getConstraint(cp: u21) Constraint { 0xe0d5, => .{ .size_horizontal = .cover, - .size_vertical = .cover, + .size_vertical = .fit, .align_horizontal = .center, .align_vertical = .center, }, @@ -362,6 +363,7 @@ pub fn getConstraint(cp: u21) Constraint { => .{ .size_horizontal = .fit, .size_vertical = .fit, + .height = .icon, .align_horizontal = .center, .align_vertical = .center, }, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index e74b2ead1..4087b9ac6 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -180,16 +180,19 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if "xy" in stretch: s += " .size_horizontal = .stretch,\n" s += " .size_vertical = .stretch,\n" - elif "!" in stretch: + elif "!" in stretch or "^" in stretch: s += " .size_horizontal = .cover,\n" s += " .size_vertical = .fit,\n" - elif "^" in stretch: - s += " .size_horizontal = .cover,\n" - s += " .size_vertical = .cover,\n" else: s += " .size_horizontal = .fit,\n" s += " .size_vertical = .fit,\n" + # `^` indicates that scaling should fill + # the whole cell, not just the icon height. + if "^" not in stretch: + s += " .height = .icon,\n" + + # There are two cases where we want to limit the constraint width to 1: # - If there's a `1` in the stretch mode string. # - If the stretch mode is `xy` and there's not an explicit `2`. From bcb6ee6db676c6f6c05fbb3ab43aa66c6b7b1433 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 09:43:33 -0600 Subject: [PATCH 57/86] config: add adjust-icon-height option Metric modifier for the `icon_height` metric. --- src/config/Config.zig | 12 ++++++++++++ src/font/SharedGridSet.zig | 3 +++ 2 files changed, 15 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index a53986bc9..8ca8d3154 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -396,6 +396,18 @@ pub const compatibility = std.StaticStringMap( /// Thickness in pixels or percentage adjustment of box drawing characters. /// See the notes about adjustments in `adjust-cell-width`. @"adjust-box-thickness": ?MetricModifier = null, +/// Height in pixels or percentage adjustment of maximum height for nerd font icons. +/// +/// Increasing this value will allow nerd font icons to be larger, but won't +/// necessarily force them to be. Decreasing this value will make nerd font +/// icons smaller. +/// +/// The default value for the icon height is 1.2 times the height of capital +/// letters in your primary font, so something like -16.6% would make icons +/// roughly the same height as capital letters. +/// +/// See the notes about adjustments in `adjust-cell-width`. +@"adjust-icon-height": ?MetricModifier = null, /// The method to use for calculating the cell width of a grapheme cluster. /// The default value is `unicode` which uses the Unicode standard to determine diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index b77b44f23..14a8babad 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -449,6 +449,7 @@ pub const DerivedConfig = struct { @"adjust-cursor-thickness": ?Metrics.Modifier, @"adjust-cursor-height": ?Metrics.Modifier, @"adjust-box-thickness": ?Metrics.Modifier, + @"adjust-icon-height": ?Metrics.Modifier, @"freetype-load-flags": font.face.FreetypeLoadFlags, /// Initialize a DerivedConfig. The config should be either a @@ -488,6 +489,7 @@ pub const DerivedConfig = struct { .@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness", .@"adjust-cursor-height" = config.@"adjust-cursor-height", .@"adjust-box-thickness" = config.@"adjust-box-thickness", + .@"adjust-icon-height" = config.@"adjust-icon-height", .@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {}, // This must be last so the arena contains all our allocations @@ -634,6 +636,7 @@ pub const Key = struct { if (config.@"adjust-cursor-thickness") |m| try set.put(alloc, .cursor_thickness, m); if (config.@"adjust-cursor-height") |m| try set.put(alloc, .cursor_height, m); if (config.@"adjust-box-thickness") |m| try set.put(alloc, .box_thickness, m); + if (config.@"adjust-icon-height") |m| try set.put(alloc, .icon_height, m); break :set set; }; From 08db61e27e875b1a211867523d72ca414224656b Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 7 Jul 2025 08:58:44 -0700 Subject: [PATCH 58/86] refactor: simplify SSH environment variable handling - Remove complex ssh_exported_vars tracking and local environment modification in favor of trusting Ghostty's local environment - Replace regex patterns with glob-based feature detection for better performance - Fix local variable declaration consistency throughout - Streamline logic while maintaining all functionality --- src/shell-integration/bash/ghostty.bash | 48 ++++--------------- .../elvish/lib/ghostty-integration.elv | 31 +----------- .../ghostty-shell-integration.fish | 30 +----------- src/shell-integration/zsh/ghostty-integration | 46 ++++-------------- 4 files changed, 21 insertions(+), 134 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index f13a7d378..205494c94 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -96,33 +96,16 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then fi # SSH Integration -if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then +if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] || [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then ssh() { - builtin local ssh_env ssh_opts ssh_exported_vars + builtin local ssh_env ssh_opts ssh_env=() ssh_opts=() - ssh_exported_vars=() # Configure environment variables for remote session - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then ssh_opts+=(-o "SetEnv COLORTERM=truecolor") - - if [[ -n "${TERM_PROGRAM+x}" ]]; then - ssh_exported_vars+=("TERM_PROGRAM=${TERM_PROGRAM}") - else - ssh_exported_vars+=("TERM_PROGRAM") - fi - builtin export "TERM_PROGRAM=ghostty" - ssh_opts+=(-o "SendEnv TERM_PROGRAM") - - if [[ -n "$TERM_PROGRAM_VERSION" ]]; then - if [[ -n "${TERM_PROGRAM_VERSION+x}" ]]; then - ssh_exported_vars+=("TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}") - else - ssh_exported_vars+=("TERM_PROGRAM_VERSION") - fi - ssh_opts+=(-o "SendEnv TERM_PROGRAM_VERSION") - fi + ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION") ssh_env+=( "COLORTERM=truecolor" @@ -134,7 +117,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then fi # Install terminfo on remote host if needed - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then builtin local ssh_config ssh_user ssh_hostname ssh_config=$(builtin command ssh -G "$@" 2>/dev/null) @@ -146,7 +129,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break done <<< "$ssh_config" - ssh_target="${ssh_user}@${ssh_hostname}" + builtin local ssh_target="${ssh_user}@${ssh_hostname}" if [[ -n "$ssh_hostname" ]]; then # Check if terminfo is already cached @@ -213,7 +196,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then ssh_env+=(TERM=xterm-256color) fi else - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then ssh_env+=(TERM=xterm-256color) fi fi @@ -228,7 +211,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then fi done - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env && -z "$ssh_term_override" ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term_override" ]]; then ssh_env+=(TERM=xterm-256color) ssh_term_override="xterm-256color" fi @@ -244,17 +227,6 @@ if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-(env|terminfo) ]]; then local ssh_ret=$? fi - # Restore original environment variables - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - for ssh_v in "${ssh_exported_vars[@]}"; do - if [[ "$ssh_v" == *=* ]]; then - builtin export "${ssh_v?}" - else - builtin unset "${ssh_v}" - fi - done - fi - return $ssh_ret } fi @@ -286,8 +258,8 @@ function __ghostty_precmd() { # Cursor if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then - PS1=$PS1'\[\e[5 q\]' # blinking bar for input - builtin printf "\e[0 q" # reset to default cursor + [[ "$PS1" != *'\[\e[5 q\]'* ]] && PS1=$PS1'\[\e[5 q\]' # input + [[ "$PS0" != *'\[\e[0 q\]'* ]] && PS0=$PS0'\[\e[0 q\]' # reset fi # Title (working directory) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 52d01e4fb..c2d187961 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -105,28 +105,11 @@ fn ssh {|@args| var ssh-env = [] var ssh-opts = [] - var ssh-exported-vars = [] # Configure environment variables for remote session if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { set ssh-opts = [$@ssh-opts -o "SetEnv COLORTERM=truecolor"] - - if (has-env TERM_PROGRAM) { - set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM="$E:TERM_PROGRAM] - } else { - set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM"] - } - set-env TERM_PROGRAM ghostty - set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM"] - - if (has-env TERM_PROGRAM_VERSION) { - if (has-env TERM_PROGRAM_VERSION) { - set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM_VERSION="$E:TERM_PROGRAM_VERSION] - } else { - set ssh-exported-vars = [$@ssh-exported-vars "TERM_PROGRAM_VERSION"] - } - set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM_VERSION"] - } + set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"] set ssh-env = [ "COLORTERM=truecolor" @@ -288,18 +271,6 @@ } } - # Restore original environment variables - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - for ssh-v $ssh-exported-vars { - if (str:contains $ssh-v =) { - var ssh-var-parts = (str:split &max=2 = $ssh-v) - set-env $ssh-var-parts[0] $ssh-var-parts[1] - } else { - unset-env $ssh-v - } - } - } - if (not-eq $ssh-ret 0) { fail ssh-failed } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 332675264..c45c20f92 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -91,28 +91,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" set -l ssh_env set -l ssh_opts - set -l ssh_exported_vars # Configure environment variables for remote session if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" set -a ssh_opts -o "SetEnv COLORTERM=truecolor" - - if set -q TERM_PROGRAM - set -a ssh_exported_vars "TERM_PROGRAM=$TERM_PROGRAM" - else - set -a ssh_exported_vars "TERM_PROGRAM" - end - set -gx TERM_PROGRAM ghostty - set -a ssh_opts -o "SendEnv TERM_PROGRAM" - - if test -n "$TERM_PROGRAM_VERSION" - if set -q TERM_PROGRAM_VERSION - set -a ssh_exported_vars "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" - else - set -a ssh_exported_vars "TERM_PROGRAM_VERSION" - end - set -a ssh_opts -o "SendEnv TERM_PROGRAM_VERSION" - end + set -a ssh_opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION" set -a ssh_env "COLORTERM=truecolor" set -a ssh_env "TERM_PROGRAM=ghostty" @@ -237,17 +220,6 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set ssh_ret $status end - # Restore original environment variables - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" - for ssh_v in $ssh_exported_vars - if string match -q '*=*' -- $ssh_v - set -gx (string split -m1 '=' -- $ssh_v) - else - set -e $ssh_v - end - end - end - return $ssh_ret end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 40ee58b49..37880e5f8 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -244,37 +244,20 @@ _ghostty_deferred_init() { } fi -# SSH Integration - if [[ "$GHOSTTY_SHELL_FEATURES" =~ (ssh-env|ssh-terminfo) ]]; then + # SSH Integration + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] || [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then ssh() { emulate -L zsh setopt local_options no_glob_subst - local ssh_env ssh_opts ssh_exported_vars + local ssh_env ssh_opts ssh_env=() ssh_opts=() - ssh_exported_vars=() # Configure environment variables for remote session - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then ssh_opts+=(-o "SetEnv COLORTERM=truecolor") - - if [[ -n "${TERM_PROGRAM+x}" ]]; then - ssh_exported_vars+=("TERM_PROGRAM=${TERM_PROGRAM}") - else - ssh_exported_vars+=("TERM_PROGRAM") - fi - export "TERM_PROGRAM=ghostty" - ssh_opts+=(-o "SendEnv TERM_PROGRAM") - - if [[ -n "$TERM_PROGRAM_VERSION" ]]; then - if [[ -n "${TERM_PROGRAM_VERSION+x}" ]]; then - ssh_exported_vars+=("TERM_PROGRAM_VERSION=${TERM_PROGRAM_VERSION}") - else - ssh_exported_vars+=("TERM_PROGRAM_VERSION") - fi - ssh_opts+=(-o "SendEnv TERM_PROGRAM_VERSION") - fi + ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION") ssh_env+=( "COLORTERM=truecolor" @@ -284,7 +267,7 @@ _ghostty_deferred_init() { fi # Install terminfo on remote host if needed - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-terminfo ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then local ssh_config ssh_user ssh_hostname ssh_config=$(command ssh -G "$@" 2>/dev/null) @@ -296,7 +279,7 @@ _ghostty_deferred_init() { [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break done <<< "$ssh_config" - ssh_target="${ssh_user}@${ssh_hostname}" + local ssh_target="${ssh_user}@${ssh_hostname}" if [[ -n "$ssh_hostname" ]]; then # Check if terminfo is already cached @@ -360,7 +343,7 @@ _ghostty_deferred_init() { ssh_env+=(TERM=xterm-256color) fi else - [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]] && ssh_env+=(TERM=xterm-256color) + [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] && ssh_env+=(TERM=xterm-256color) fi fi @@ -374,7 +357,7 @@ _ghostty_deferred_init() { fi done - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env && -z "$ssh_term_override" ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term_override" ]]; then ssh_env+=(TERM=xterm-256color) ssh_term_override="xterm-256color" fi @@ -391,17 +374,6 @@ _ghostty_deferred_init() { ssh_ret=$? fi - # Restore original environment variables - if [[ "$GHOSTTY_SHELL_FEATURES" =~ ssh-env ]]; then - for ssh_v in "${ssh_exported_vars[@]}"; do - if [[ "$ssh_v" == *=* ]]; then - export "${ssh_v}" - else - unset "${ssh_v}" - fi - done - fi - return $ssh_ret } fi From c3b14dff71073989e330218d98aeabed292ecd59 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 7 Jul 2025 10:00:56 -0700 Subject: [PATCH 59/86] refactor: simplify SSH terminfo and environment handling - Simplify feature detection to use single wildcard check - Replace ssh_env array with simple ssh_term string variable - Use TERM environment prefix instead of save/restore pattern - Remove unnecessary backgrounded subshell for cache operations --- src/shell-integration/bash/ghostty.bash | 59 ++++------------ .../elvish/lib/ghostty-integration.elv | 68 +++++++------------ .../ghostty-shell-integration.fish | 51 ++++---------- src/shell-integration/zsh/ghostty-integration | 56 ++++----------- 4 files changed, 68 insertions(+), 166 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 205494c94..287fbb584 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -96,24 +96,16 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then fi # SSH Integration -if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] || [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then +if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh() { - builtin local ssh_env ssh_opts - ssh_env=() + builtin local ssh_term ssh_opts + ssh_term="" ssh_opts=() # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then ssh_opts+=(-o "SetEnv COLORTERM=truecolor") ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION") - - ssh_env+=( - "COLORTERM=truecolor" - "TERM_PROGRAM=ghostty" - ) - if [[ -n "$TERM_PROGRAM_VERSION" ]]; then - ssh_env+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") - fi fi # Install terminfo on remote host if needed @@ -139,11 +131,11 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] || [[ "$GHOSTTY_SHELL_FEATURES" fi if [[ "$ssh_cache_check_success" == "true" ]]; then - ssh_env+=(TERM=xterm-ghostty) + ssh_term="xterm-ghostty" elif builtin command -v infocmp >/dev/null 2>&1; then if ! builtin command -v base64 >/dev/null 2>&1; then builtin echo "Warning: base64 command not available for terminfo installation." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" else builtin local ssh_terminfo ssh_base64_decode_cmd @@ -170,64 +162,43 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] || [[ "$GHOSTTY_SHELL_FEATURES" exit 1 ' 2>/dev/null; then builtin echo "Terminfo setup complete on $ssh_hostname." >&2 - ssh_env+=(TERM=xterm-ghostty) + ssh_term="xterm-ghostty" ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation if [[ -n "$ssh_target" ]] && builtin command -v ghostty >/dev/null 2>&1; then - ( - set +m - { - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - } & - ) + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true fi else builtin echo "Warning: Failed to install terminfo." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi else builtin echo "Warning: Could not generate terminfo data." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi fi else builtin echo "Warning: ghostty command not available for cache management." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi else if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi fi fi # Execute SSH with environment handling - builtin local ssh_term_override="" - for ssh_v in "${ssh_env[@]}"; do - if [[ "$ssh_v" =~ ^TERM=(.*)$ ]]; then - ssh_term_override="${BASH_REMATCH[1]}" - break - fi - done - - if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term_override" ]]; then - ssh_env+=(TERM=xterm-256color) - ssh_term_override="xterm-256color" + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term" ]]; then + ssh_term="xterm-256color" fi - if [[ -n "$ssh_term_override" ]]; then - builtin local ssh_original_term="$TERM" - builtin export TERM="$ssh_term_override" - builtin command ssh "${ssh_opts[@]}" "$@" - local ssh_ret=$? - builtin export TERM="$ssh_original_term" + if [[ -n "$ssh_term" ]]; then + TERM="$ssh_term" builtin command ssh "${ssh_opts[@]}" "$@" else builtin command ssh "${ssh_opts[@]}" "$@" - local ssh_ret=$? fi - - return $ssh_ret } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index c2d187961..d348c7381 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -101,23 +101,16 @@ # SSH Integration use str - if (or (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo)) { + if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) { + # SSH wrapper that preserves Ghostty features across remote connections fn ssh {|@args| - var ssh-env = [] + var ssh-term = "" var ssh-opts = [] # Configure environment variables for remote session if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { set ssh-opts = [$@ssh-opts -o "SetEnv COLORTERM=truecolor"] set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"] - - set ssh-env = [ - "COLORTERM=truecolor" - "TERM_PROGRAM=ghostty" - ] - if (has-env TERM_PROGRAM_VERSION) { - set ssh-env = [$@ssh-env "TERM_PROGRAM_VERSION="$E:TERM_PROGRAM_VERSION] - } } # Install terminfo on remote host if needed @@ -159,7 +152,7 @@ } if $ssh-cache-check-success { - set ssh-env = [$@ssh-env TERM=xterm-ghostty] + set ssh-term = "xterm-ghostty" } else { try { external infocmp --help >/dev/null 2>&1 @@ -208,71 +201,58 @@ if $terminfo-install-success { echo "Terminfo setup complete on "$ssh-hostname"." >&2 - set ssh-env = [$@ssh-env TERM=xterm-ghostty] + set ssh-term = "xterm-ghostty" set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] # Cache successful installation if (and (not-eq $ssh-target "") (has-external ghostty)) { - external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 & + try { + external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 + } catch { + # cache add failed + } } } else { echo "Warning: Failed to install terminfo." >&2 - set ssh-env = [$@ssh-env TERM=xterm-256color] + set ssh-term = "xterm-256color" } } else { echo "Warning: Could not generate terminfo data." >&2 - set ssh-env = [$@ssh-env TERM=xterm-256color] + set ssh-term = "xterm-256color" } } catch { echo "Warning: base64 command not available for terminfo installation." >&2 - set ssh-env = [$@ssh-env TERM=xterm-256color] + set ssh-term = "xterm-256color" } } catch { echo "Warning: ghostty command not available for cache management." >&2 - set ssh-env = [$@ssh-env TERM=xterm-256color] + set ssh-term = "xterm-256color" } } } else { if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - set ssh-env = [$@ssh-env TERM=xterm-256color] + set ssh-term = "xterm-256color" } } } # Execute SSH with environment handling - var ssh-term-override = "" - for ssh-v $ssh-env { - if (str:has-prefix $ssh-v TERM=) { - set ssh-term-override = (str:trim-prefix $ssh-v TERM=) - break - } + if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (eq $ssh-term "")) { + set ssh-term = "xterm-256color" } - if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (eq $ssh-term-override "")) { - set ssh-env = [$@ssh-env TERM=xterm-256color] - set ssh-term-override = xterm-256color - } - - var ssh-ret = 0 - if (not-eq $ssh-term-override "") { - var ssh-original-term = $E:TERM - set-env TERM $ssh-term-override + if (not-eq $ssh-term "") { + var old-term = $E:TERM + set-env TERM $ssh-term try { external ssh $@ssh-opts $@args } catch e { - set ssh-ret = $e[reason][exit-status] + set-env TERM $old-term + fail $e } - set-env TERM $ssh-original-term + set-env TERM $old-term } else { - try { - external ssh $@ssh-opts $@args - } catch e { - set ssh-ret = $e[reason][exit-status] - } - } - - if (not-eq $ssh-ret 0) { - fail ssh-failed + external ssh $@ssh-opts $@args } } } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index c45c20f92..ba2fb48b8 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -87,21 +87,15 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # SSH Integration - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; or string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" + if string match -q '*ssh-*' -- "$GHOSTTY_SHELL_FEATURES" function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" - set -l ssh_env + set -l ssh_term "" set -l ssh_opts # Configure environment variables for remote session if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" set -a ssh_opts -o "SetEnv COLORTERM=truecolor" set -a ssh_opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION" - - set -a ssh_env "COLORTERM=truecolor" - set -a ssh_env "TERM_PROGRAM=ghostty" - if test -n "$TERM_PROGRAM_VERSION" - set -a ssh_env "TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION" - end end # Install terminfo on remote host if needed @@ -137,11 +131,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end if test "$ssh_cache_check_success" = "true" - set -a ssh_env TERM=xterm-ghostty + set ssh_term "xterm-ghostty" else if command -v infocmp >/dev/null 2>&1 if not command -v base64 >/dev/null 2>&1 echo "Warning: base64 command not available for terminfo installation." >&2 - set -a ssh_env TERM=xterm-256color + set ssh_term "xterm-256color" else set -l ssh_terminfo set -l ssh_base64_decode_cmd @@ -167,60 +161,43 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" exit 1 ' 2>/dev/null echo "Terminfo setup complete on $ssh_hostname." >&2 - set -a ssh_env TERM=xterm-ghostty + set ssh_term "xterm-ghostty" set -a ssh_opts -o "ControlPath=$ssh_cpath" # Cache successful installation if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 - fish -c "ghostty +ssh-cache --add='$ssh_target' >/dev/null 2>&1; or true" & + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true end else echo "Warning: Failed to install terminfo." >&2 - set -a ssh_env TERM=xterm-256color + set ssh_term "xterm-256color" end else echo "Warning: Could not generate terminfo data." >&2 - set -a ssh_env TERM=xterm-256color + set ssh_term "xterm-256color" end end else echo "Warning: ghostty command not available for cache management." >&2 - set -a ssh_env TERM=xterm-256color + set ssh_term "xterm-256color" end else if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" - set -a ssh_env TERM=xterm-256color + set ssh_term "xterm-256color" end end end # Execute SSH with environment handling - set -l ssh_term_override - for ssh_v in $ssh_env - if string match -q 'TERM=*' -- $ssh_v - set ssh_term_override (string replace 'TERM=' '' -- $ssh_v) - break - end + if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; and test -z "$ssh_term" + set ssh_term "xterm-256color" end - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; and test -z "$ssh_term_override" - set -a ssh_env TERM=xterm-256color - set ssh_term_override xterm-256color - end - - set -l ssh_ret - if test -n "$ssh_term_override" - set -l ssh_original_term "$TERM" - set -gx TERM "$ssh_term_override" - command ssh $ssh_opts $argv - set ssh_ret $status - set -gx TERM "$ssh_original_term" + if test -n "$ssh_term" + env TERM="$ssh_term" command ssh $ssh_opts $argv else command ssh $ssh_opts $argv - set ssh_ret $status end - - return $ssh_ret end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 37880e5f8..df62cdb6c 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -245,25 +245,19 @@ _ghostty_deferred_init() { fi # SSH Integration - if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] || [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh() { emulate -L zsh setopt local_options no_glob_subst - local ssh_env ssh_opts - ssh_env=() + local ssh_term ssh_opts + ssh_term="" ssh_opts=() # Configure environment variables for remote session if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then ssh_opts+=(-o "SetEnv COLORTERM=truecolor") ssh_opts+=(-o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION") - - ssh_env+=( - "COLORTERM=truecolor" - "TERM_PROGRAM=ghostty" - ) - [[ -n "$TERM_PROGRAM_VERSION" ]] && ssh_env+=("TERM_PROGRAM_VERSION=$TERM_PROGRAM_VERSION") fi # Install terminfo on remote host if needed @@ -289,11 +283,11 @@ _ghostty_deferred_init() { fi if [[ "$ssh_cache_check_success" == "true" ]]; then - ssh_env+=(TERM=xterm-ghostty) + ssh_term="xterm-ghostty" elif (( $+commands[infocmp] )); then if ! (( $+commands[base64] )); then print "Warning: base64 command not available for terminfo installation." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" else local ssh_terminfo ssh_base64_decode_cmd @@ -320,61 +314,41 @@ _ghostty_deferred_init() { exit 1 ' 2>/dev/null; then print "Terminfo setup complete on $ssh_hostname." >&2 - ssh_env+=(TERM=xterm-ghostty) + ssh_term="xterm-ghostty" ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then - { - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - } &! + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true fi else print "Warning: Failed to install terminfo." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi else print "Warning: Could not generate terminfo data." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi fi else print "Warning: ghostty command not available for cache management." >&2 - ssh_env+=(TERM=xterm-256color) + ssh_term="xterm-256color" fi else - [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] && ssh_env+=(TERM=xterm-256color) + [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] && ssh_term="xterm-256color" fi fi # Execute SSH with environment handling - local ssh_term_override="" - local ssh_v - for ssh_v in "${ssh_env[@]}"; do - if [[ "$ssh_v" =~ ^TERM=(.*)$ ]]; then - ssh_term_override="${match[1]}" - break - fi - done - - if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term_override" ]]; then - ssh_env+=(TERM=xterm-256color) - ssh_term_override="xterm-256color" + if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term" ]]; then + ssh_term="xterm-256color" fi - local ssh_ret - if [[ -n "$ssh_term_override" ]]; then - local ssh_original_term="$TERM" - export TERM="$ssh_term_override" - command ssh "${ssh_opts[@]}" "$@" - ssh_ret=$? - export TERM="$ssh_original_term" + if [[ -n "$ssh_term" ]]; then + TERM="$ssh_term" command ssh "${ssh_opts[@]}" "$@" else command ssh "${ssh_opts[@]}" "$@" - ssh_ret=$? fi - - return $ssh_ret } fi From 9d81a5f5ec430d3595db591fec80aa2fbfafdd45 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 7 Jul 2025 11:08:20 -0700 Subject: [PATCH 60/86] ci: switch Apple notarization to an new account The Ghostty Apple ID has been frozen. I'm working on figuring out how to get it back. In the meantime, this switches the notarization to my personal Apple ID. I originally created the dedicated Apple ID to limit access since we were using app passwords. But I've since discovered that we can create API tokens that have limited access, so I don't think this is a problem anymore. --- .github/workflows/release-pr.yml | 20 ++++++++++++-------- .github/workflows/release-tag.yml | 10 ++++++---- .github/workflows/release-tip.yml | 30 ++++++++++++++++++------------ 3 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index cf96ffb21..e260996bb 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -147,14 +147,16 @@ jobs: - name: "Notarize app bundle" env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 + xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" + rm notarization_key.p8 # We can't notarize an app bundle directly, but we need to compress it as an archive. # Therefore, we create a zip file containing our app bundle, so that we can send it to the @@ -299,14 +301,16 @@ jobs: - name: "Notarize app bundle" env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 + xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" + rm notarization_key.p8 # We can't notarize an app bundle directly, but we need to compress it as an archive. # Therefore, we create a zip file containing our app bundle, so that we can send it to the diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 98ecf2fa3..4cc364127 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -229,14 +229,16 @@ jobs: - name: "Notarize DMG" env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 + xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" + rm notarization_key.p8 # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index b0916e657..b7c4949a5 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -267,14 +267,16 @@ jobs: - name: "Notarize DMG" env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 + xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" + rm notarization_key.p8 # Here we send the notarization request to the Apple's Notarization service, waiting for the result. # This typically takes a few seconds inside a CI environment, but it might take more depending on the App @@ -471,14 +473,16 @@ jobs: - name: "Notarize app bundle" env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 + xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" + rm notarization_key.p8 # We can't notarize an app bundle directly, but we need to compress it as an archive. # Therefore, we create a zip file containing our app bundle, so that we can send it to the @@ -646,14 +650,16 @@ jobs: - name: "Notarize app bundle" env: - PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }} - PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }} - PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }} + APPLE_NOTARIZATION_ISSUER: ${{ secrets.APPLE_NOTARIZATION_ISSUER }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} run: | # Store the notarization credentials so that we can prevent a UI password dialog # from blocking the CI echo "Create keychain profile" - xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD" + echo "$APPLE_NOTARIZATION_KEY" > notarization_key.p8 + xcrun notarytool store-credentials "notarytool-profile" --key notarization_key.p8 --key-id "$APPLE_NOTARIZATION_KEY_ID" --issuer "$APPLE_NOTARIZATION_ISSUER" + rm notarization_key.p8 # We can't notarize an app bundle directly, but we need to compress it as an archive. # Therefore, we create a zip file containing our app bundle, so that we can send it to the From f27993737794ea60855bb03c6ae07647be56c230 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Mon, 7 Jul 2025 11:33:26 -0700 Subject: [PATCH 61/86] refactor: simplify terminfo handling and remove base64 dependency - Default ssh_term to xterm-256color to eliminate fallback assignments - Remove base64 and replace infocmp -Q2 with standard -0 -x options for compatibility - Use process substitution instead of intermediate ssh_config variable - Always set TERM explicitly since ssh_term is always defined --- src/shell-integration/bash/ghostty.bash | 81 +++------ .../elvish/lib/ghostty-integration.elv | 161 +++++++----------- .../ghostty-shell-integration.fish | 82 +++------ src/shell-integration/zsh/ghostty-integration | 79 +++------ 4 files changed, 145 insertions(+), 258 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 287fbb584..5b6bb249d 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -99,7 +99,7 @@ fi if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh() { builtin local ssh_term ssh_opts - ssh_term="" + ssh_term="xterm-256color" ssh_opts=() # Configure environment variables for remote session @@ -110,8 +110,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then - builtin local ssh_config ssh_user ssh_hostname - ssh_config=$(builtin command ssh -G "$@" 2>/dev/null) + builtin local ssh_user ssh_hostname while IFS=' ' read -r ssh_key ssh_value; do case "$ssh_key" in @@ -119,7 +118,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then hostname) ssh_hostname="$ssh_value" ;; esac [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break - done <<< "$ssh_config" + done < <(builtin command ssh -G "$@" 2>/dev/null) builtin local ssh_target="${ssh_user}@${ssh_hostname}" @@ -133,72 +132,44 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then if [[ "$ssh_cache_check_success" == "true" ]]; then ssh_term="xterm-ghostty" elif builtin command -v infocmp >/dev/null 2>&1; then - if ! builtin command -v base64 >/dev/null 2>&1; then - builtin echo "Warning: base64 command not available for terminfo installation." >&2 - ssh_term="xterm-256color" - else - builtin local ssh_terminfo ssh_base64_decode_cmd + builtin local ssh_terminfo ssh_cpath_dir ssh_cpath - # BSD vs GNU base64 compatibility - if base64 --help 2>&1 | grep -q GNU; then - ssh_base64_decode_cmd="base64 -d" - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) - else - ssh_base64_decode_cmd="base64 -D" - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') - fi + ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null) - if [[ -n "$ssh_terminfo" ]]; then - builtin echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 - builtin local ssh_cpath_dir ssh_cpath + if [[ -n "$ssh_terminfo" ]]; then + builtin echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 - ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" - ssh_cpath="$ssh_cpath_dir/socket" + ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" + ssh_cpath="$ssh_cpath_dir/socket" - if builtin echo "$ssh_terminfo" | $ssh_base64_decode_cmd | builtin command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null; then - builtin echo "Terminfo setup complete on $ssh_hostname." >&2 - ssh_term="xterm-ghostty" - ssh_opts+=(-o "ControlPath=$ssh_cpath") + if builtin echo "$ssh_terminfo" | builtin command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null; then + builtin echo "Terminfo setup complete on $ssh_hostname." >&2 + ssh_term="xterm-ghostty" + ssh_opts+=(-o "ControlPath=$ssh_cpath") - # Cache successful installation - if [[ -n "$ssh_target" ]] && builtin command -v ghostty >/dev/null 2>&1; then - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - fi - else - builtin echo "Warning: Failed to install terminfo." >&2 - ssh_term="xterm-256color" + # Cache successful installation + if [[ -n "$ssh_target" ]] && builtin command -v ghostty >/dev/null 2>&1; then + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true fi else - builtin echo "Warning: Could not generate terminfo data." >&2 - ssh_term="xterm-256color" + builtin echo "Warning: Failed to install terminfo." >&2 fi + else + builtin echo "Warning: Could not generate terminfo data." >&2 fi else builtin echo "Warning: ghostty command not available for cache management." >&2 - ssh_term="xterm-256color" - fi - else - if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]]; then - ssh_term="xterm-256color" fi fi fi - # Execute SSH with environment handling - if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term" ]]; then - ssh_term="xterm-256color" - fi - - if [[ -n "$ssh_term" ]]; then - TERM="$ssh_term" builtin command ssh "${ssh_opts[@]}" "$@" - else - builtin command ssh "${ssh_opts[@]}" "$@" - fi + # Execute SSH with TERM environment variable + TERM="$ssh_term" builtin command ssh "${ssh_opts[@]}" "$@" } fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index d348c7381..44cf135dc 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -102,9 +102,8 @@ use str if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-) { - # SSH wrapper that preserves Ghostty features across remote connections fn ssh {|@args| - var ssh-term = "" + var ssh-term = "xterm-256color" var ssh-opts = [] # Configure environment variables for remote session @@ -115,28 +114,26 @@ # Install terminfo on remote host if needed if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-terminfo) { - var ssh-config = "" - try { - set ssh-config = (external ssh -G $@args 2>/dev/null | slurp) - } catch { - set ssh-config = "" - } - var ssh-user = "" var ssh-hostname = "" - for line (str:split "\n" $ssh-config) { - var parts = (str:split " " $line) - if (> (count $parts) 1) { - if (eq $parts[0] user) { - set ssh-user = $parts[1] - } elif (eq $parts[0] hostname) { - set ssh-hostname = $parts[1] - } - if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { - break + try { + var ssh-config = (external ssh -G $@args 2>/dev/null | slurp) + for line (str:split "\n" $ssh-config) { + var parts = (str:split " " $line) + if (> (count $parts) 1) { + if (eq $parts[0] user) { + set ssh-user = $parts[1] + } elif (eq $parts[0] hostname) { + set ssh-hostname = $parts[1] + } + if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { + break + } } } + } catch { + # ssh config failed } var ssh-target = $ssh-user"@"$ssh-hostname @@ -157,103 +154,75 @@ try { external infocmp --help >/dev/null 2>&1 - try { - external base64 --help >/dev/null 2>&1 + var ssh-terminfo = "" + var ssh-cpath-dir = "" + var ssh-cpath = "" + + try { + set ssh-terminfo = (external infocmp -0 -x xterm-ghostty 2>/dev/null | slurp) + } catch { + set ssh-terminfo = "" + } + + if (not-eq $ssh-terminfo "") { + echo "Setting up Ghostty terminfo on "$ssh-hostname"..." >&2 - # Generate terminfo data (BSD base64 compatibility) - var ssh-terminfo = "" - var ssh-base64-decode-cmd = "" try { - var base64-help = (external base64 --help 2>&1 | slurp) - if (str:contains $base64-help GNU) { - set ssh-base64-decode-cmd = "base64 -d" - set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 -w0 2>/dev/null | slurp) - } else { - set ssh-base64-decode-cmd = "base64 -D" - set ssh-terminfo = (external infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | external base64 2>/dev/null | external tr -d '\n' | slurp) - } + set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) } catch { - set ssh-terminfo = "" + set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) + } + set ssh-cpath = $ssh-cpath-dir"/socket" + + var terminfo-install-success = $false + try { + echo $ssh-terminfo | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' >/dev/null 2>&1 + set terminfo-install-success = $true + } catch { + set terminfo-install-success = $false } - if (not-eq $ssh-terminfo "") { - echo "Setting up Ghostty terminfo on "$ssh-hostname"..." >&2 - var ssh-cpath-dir = "" - try { - set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) - } catch { - set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) - } - var ssh-cpath = $ssh-cpath-dir"/socket" + if $terminfo-install-success { + echo "Terminfo setup complete on "$ssh-hostname"." >&2 + set ssh-term = "xterm-ghostty" + set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] - var terminfo-install-success = $false - try { - echo $ssh-terminfo | external sh -c $ssh-base64-decode-cmd | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' >/dev/null 2>&1 - set terminfo-install-success = $true - } catch { - set terminfo-install-success = $false - } - - if $terminfo-install-success { - echo "Terminfo setup complete on "$ssh-hostname"." >&2 - set ssh-term = "xterm-ghostty" - set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] - - # Cache successful installation - if (and (not-eq $ssh-target "") (has-external ghostty)) { - try { - external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 - } catch { - # cache add failed - } + # Cache successful installation + if (and (not-eq $ssh-target "") (has-external ghostty)) { + try { + external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 + } catch { + # cache add failed } - } else { - echo "Warning: Failed to install terminfo." >&2 - set ssh-term = "xterm-256color" } } else { - echo "Warning: Could not generate terminfo data." >&2 - set ssh-term = "xterm-256color" + echo "Warning: Failed to install terminfo." >&2 } - } catch { - echo "Warning: base64 command not available for terminfo installation." >&2 - set ssh-term = "xterm-256color" + } else { + echo "Warning: Could not generate terminfo data." >&2 } } catch { echo "Warning: ghostty command not available for cache management." >&2 - set ssh-term = "xterm-256color" } } - } else { - if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - set ssh-term = "xterm-256color" - } } } - # Execute SSH with environment handling - if (and (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) (eq $ssh-term "")) { - set ssh-term = "xterm-256color" - } - - if (not-eq $ssh-term "") { - var old-term = $E:TERM - set-env TERM $ssh-term - try { - external ssh $@ssh-opts $@args - } catch e { - set-env TERM $old-term - fail $e - } - set-env TERM $old-term - } else { + # Execute SSH with TERM environment variable + var old-term = $E:TERM + set-env TERM $ssh-term + try { external ssh $@ssh-opts $@args + } catch e { + set-env TERM $old-term + fail $e } + set-env TERM $old-term } } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index ba2fb48b8..ab0f23086 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -89,7 +89,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # SSH Integration if string match -q '*ssh-*' -- "$GHOSTTY_SHELL_FEATURES" function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" - set -l ssh_term "" + set -l ssh_term "xterm-256color" set -l ssh_opts # Configure environment variables for remote session @@ -100,11 +100,10 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" # Install terminfo on remote host if needed if string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" - set -l ssh_config (command ssh -G $argv 2>/dev/null) set -l ssh_user set -l ssh_hostname - for line in $ssh_config + for line in (command ssh -G $argv 2>/dev/null) set -l parts (string split ' ' -- $line) if test (count $parts) -ge 2 switch $parts[1] @@ -133,71 +132,46 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if test "$ssh_cache_check_success" = "true" set ssh_term "xterm-ghostty" else if command -v infocmp >/dev/null 2>&1 - if not command -v base64 >/dev/null 2>&1 - echo "Warning: base64 command not available for terminfo installation." >&2 - set ssh_term "xterm-256color" - else - set -l ssh_terminfo - set -l ssh_base64_decode_cmd + set -l ssh_terminfo + set -l ssh_cpath_dir + set -l ssh_cpath - # BSD vs GNU base64 compatibility - if base64 --help 2>&1 | grep -q GNU - set ssh_base64_decode_cmd "base64 -d" - set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) - else - set ssh_base64_decode_cmd "base64 -D" - set ssh_terminfo (infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') - end + set ssh_terminfo (infocmp -0 -x xterm-ghostty 2>/dev/null) - if test -n "$ssh_terminfo" - echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 - set -l ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) - set -l ssh_cpath "$ssh_cpath_dir/socket" + if test -n "$ssh_terminfo" + echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 - if echo "$ssh_terminfo" | eval $ssh_base64_decode_cmd | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null - echo "Terminfo setup complete on $ssh_hostname." >&2 - set ssh_term "xterm-ghostty" - set -a ssh_opts -o "ControlPath=$ssh_cpath" + set ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) + set ssh_cpath "$ssh_cpath_dir/socket" - # Cache successful installation - if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true - end - else - echo "Warning: Failed to install terminfo." >&2 - set ssh_term "xterm-256color" + if echo "$ssh_terminfo" | command ssh $ssh_opts -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s $argv ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null + echo "Terminfo setup complete on $ssh_hostname." >&2 + set ssh_term "xterm-ghostty" + set -a ssh_opts -o "ControlPath=$ssh_cpath" + + # Cache successful installation + if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true end else - echo "Warning: Could not generate terminfo data." >&2 - set ssh_term "xterm-256color" + echo "Warning: Failed to install terminfo." >&2 end + else + echo "Warning: Could not generate terminfo data." >&2 end else echo "Warning: ghostty command not available for cache management." >&2 - set ssh_term "xterm-256color" - end - else - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" - set ssh_term "xterm-256color" end end end - # Execute SSH with environment handling - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES"; and test -z "$ssh_term" - set ssh_term "xterm-256color" - end - - if test -n "$ssh_term" - env TERM="$ssh_term" command ssh $ssh_opts $argv - else - command ssh $ssh_opts $argv - end + # Execute SSH with TERM environment variable + env TERM="$ssh_term" command ssh $ssh_opts $argv end end diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index df62cdb6c..7c7ab7972 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -251,7 +251,7 @@ _ghostty_deferred_init() { setopt local_options no_glob_subst local ssh_term ssh_opts - ssh_term="" + ssh_term="xterm-256color" ssh_opts=() # Configure environment variables for remote session @@ -262,8 +262,7 @@ _ghostty_deferred_init() { # Install terminfo on remote host if needed if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-terminfo* ]]; then - local ssh_config ssh_user ssh_hostname - ssh_config=$(command ssh -G "$@" 2>/dev/null) + local ssh_user ssh_hostname while IFS=' ' read -r ssh_key ssh_value; do case "$ssh_key" in @@ -271,7 +270,7 @@ _ghostty_deferred_init() { hostname) ssh_hostname="$ssh_value" ;; esac [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break - done <<< "$ssh_config" + done < <(command ssh -G "$@" 2>/dev/null) local ssh_target="${ssh_user}@${ssh_hostname}" @@ -285,70 +284,44 @@ _ghostty_deferred_init() { if [[ "$ssh_cache_check_success" == "true" ]]; then ssh_term="xterm-ghostty" elif (( $+commands[infocmp] )); then - if ! (( $+commands[base64] )); then - print "Warning: base64 command not available for terminfo installation." >&2 - ssh_term="xterm-256color" - else - local ssh_terminfo ssh_base64_decode_cmd + local ssh_terminfo ssh_cpath_dir ssh_cpath - # BSD vs GNU base64 compatibility - if base64 --help 2>&1 | grep -q GNU; then - ssh_base64_decode_cmd="base64 -d" - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 -w0 2>/dev/null) - else - ssh_base64_decode_cmd="base64 -D" - ssh_terminfo=$(infocmp -0 -Q2 -q xterm-ghostty 2>/dev/null | base64 2>/dev/null | tr -d '\n') - fi + ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null) - if [[ -n "$ssh_terminfo" ]]; then - print "Setting up Ghostty terminfo on $ssh_hostname..." >&2 - local ssh_cpath_dir ssh_cpath + if [[ -n "$ssh_terminfo" ]]; then + print "Setting up Ghostty terminfo on $ssh_hostname..." >&2 - ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" - ssh_cpath="$ssh_cpath_dir/socket" + ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" + ssh_cpath="$ssh_cpath_dir/socket" - if print "$ssh_terminfo" | $ssh_base64_decode_cmd | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' 2>/dev/null; then - print "Terminfo setup complete on $ssh_hostname." >&2 - ssh_term="xterm-ghostty" - ssh_opts+=(-o "ControlPath=$ssh_cpath") + if print "$ssh_terminfo" | command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null; then + print "Terminfo setup complete on $ssh_hostname." >&2 + ssh_term="xterm-ghostty" + ssh_opts+=(-o "ControlPath=$ssh_cpath") - # Cache successful installation - if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - fi - else - print "Warning: Failed to install terminfo." >&2 - ssh_term="xterm-256color" + # Cache successful installation + if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true fi else - print "Warning: Could not generate terminfo data." >&2 - ssh_term="xterm-256color" + print "Warning: Failed to install terminfo." >&2 fi + else + print "Warning: Could not generate terminfo data." >&2 fi else print "Warning: ghostty command not available for cache management." >&2 - ssh_term="xterm-256color" fi - else - [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* ]] && ssh_term="xterm-256color" fi fi - # Execute SSH with environment handling - if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-env* && -z "$ssh_term" ]]; then - ssh_term="xterm-256color" - fi - - if [[ -n "$ssh_term" ]]; then - TERM="$ssh_term" command ssh "${ssh_opts[@]}" "$@" - else - command ssh "${ssh_opts[@]}" "$@" - fi + # Execute SSH with TERM environment variable + TERM="$ssh_term" command ssh "${ssh_opts[@]}" "$@" } fi From a67b8b35f6a882e334f3ee300518348c8b0cd438 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 17:55:29 -0600 Subject: [PATCH 62/86] font/coretext: disable Apple's quantization, do it ourselves Using the "subpixel quantization" option for rendering our glyph was creating bad edge cases where we'd lose the bottom or left row / column of pixels in a glyph sometimes. I investigated the exact effect of this option and it seems like beyond quantizing the position and scale it's also doing some rudimentary auto-hinting. That said, the auto-hinting doesn't do that much for us, and the fact that it horizontally snaps coordinates to thirds of a pixel instead of whole pixels makes things worse in terms of legibility at small pixel sizes, so ultimately it's better with our own handling anyway. I extensively compared the result of Apple's option with our own manual quantization here and I'm pretty sure this will always match the whole pixel sizes, and where it differs (other than things like crossbars) it seems to make glyphs generally more legible not less. --- src/font/face/coretext.zig | 88 +++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index c98f511b6..e2d60905d 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -354,21 +354,48 @@ pub const Face = struct { opts.constraint_width, ); - const width = glyph_size.width; - const height = glyph_size.height; - const x = glyph_size.x; - const y = glyph_size.y; + // We manually quantize the position and size of the glyph to whole + // pixel boundaries. Since macOS doesn't do font hinting this helps + // a lot for legibility at small sizes on low dpi displays. + // + // Well, okay, so, it seems like macOS does have a rudimentary auto- + // hinter of sorts, except they call it "subpixel quantization"[^1]. + // + // Why not just use that? Because it's unpredictable and would force + // us to have an extra pixel of padding in the atlas for most glyphs + // that don't need it, since it's hard to know whether a given glyph + // will have its bottom or left edge snapped out an extra pixel. + // + // Also, this empirically just looks a whole lot better than theirs. + // Admittedly this is a very specific use case, we're rendering for + // a monospace grid and don't really have to worry about sub-pixel + // positioning; I'm sure Apple's technique is better for cases with + // proportional text. + // + // An effort was made to more or less match Apple's quantization in + // terms of resulting whole-pixel glyph sizes. Oddly it looks like + // Apple is still horizontally quantizing to thirds of a pixel, as + // if they're doing subpixel rendering for a horizontally striped + // LCD, even though they haven't done subpixel rendering for years. + // We don't match them on that, it tends to just make it blurrier. + // + // [^1]: Well I'm 80% sure it's hinting since it seems to account for + // features inside of the glyph like crossbars, not just the bounding + // box like we do. The documentation is... sparse. Ref: + // https://developer.apple.com/documentation/coregraphics/cgcontext/setshouldsubpixelquantizefonts(_:)?language=objc + // + // TODO: Maybe gate this so it only applies at small font sizes, + // or else offer a user config option that can disable it. + const x = @round(glyph_size.x); + const y = @round(glyph_size.y); + // We subtract a third here so that we behave (somewhat) like the weird + // one third pixel quantization that Apple does. This is basically just + // a fudge factor though. + const width = @max(1.0, @ceil(glyph_size.width + glyph_size.x - x - 1.0 / 3.0)); + const height = @max(1.0, @ceil(glyph_size.height + glyph_size.y - y)); - // We have to include the fractional pixels that we won't be offsetting - // in our width and height calculations, that is, we offset by the floor - // of the bearings when we render the glyph, meaning there's still a bit - // of extra width to the area that's drawn in beyond just the width of - // the glyph itself, so we include that extra fraction of a pixel when - // calculating the width and height here. - const frac_x = rect.origin.x - @floor(rect.origin.x); - const frac_y = rect.origin.y - @floor(rect.origin.y); - const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); - const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); + const px_width: u32 = @intFromFloat(@ceil(width)); + const px_height: u32 = @intFromFloat(@ceil(height)); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -433,12 +460,23 @@ pub const Face = struct { }, }); + // "Font smoothing" is what we call "thickening", it's an attempt + // to compensate for optical thinning of fonts, but at this point + // it's just something that makes the text look closer to system + // applications if users want that. context.setAllowsFontSmoothing(ctx, true); - context.setShouldSmoothFonts(ctx, opts.thicken); // The amadeus "enthicken" - context.setAllowsFontSubpixelQuantization(ctx, true); - context.setShouldSubpixelQuantizeFonts(ctx, true); + context.setShouldSmoothFonts(ctx, opts.thicken); + + // Subpixel positioning allows glyphs to be placed at non-integer + // coordinates. We need this for our alignment. context.setAllowsFontSubpixelPositioning(ctx, true); context.setShouldSubpixelPositionFonts(ctx, true); + + // See comments about quantization earlier in the function. + context.setAllowsFontSubpixelQuantization(ctx, false); + context.setShouldSubpixelQuantizeFonts(ctx, false); + + // Anti-aliasing is self explanatory. context.setAllowsAntialiasing(ctx, true); context.setShouldAntialias(ctx, true); @@ -459,6 +497,8 @@ pub const Face = struct { context.setLineWidth(ctx, line_width); } + // Scale the drawing context so that when we draw + // our glyph it's stretched to the constrained size. context.scaleCTM( ctx, width / rect.size.width, @@ -469,8 +509,8 @@ pub const Face = struct { // are offset by bearings, so we have to undo those bearings in order // to get them to 0,0. self.font.drawGlyphs(&glyphs, &.{.{ - .x = -@floor(rect.origin.x), - .y = -@floor(rect.origin.y), + .x = -rect.origin.x, + .y = -rect.origin.y, }}, ctx); // Write our rasterized glyph to the atlas. @@ -479,9 +519,7 @@ pub const Face = struct { // This should be the distance from the bottom of // the cell to the top of the glyph's bounding box. - const offset_y: i32 = - @as(i32, @intFromFloat(@floor(y))) + - @as(i32, @intCast(px_height)); + const offset_y: i32 = @as(i32, @intFromFloat(@ceil(y + height))); // This should be the distance from the left of // the cell to the left of the glyph's bounding box. @@ -514,13 +552,13 @@ pub const Face = struct { // We also don't want to do anything if the advance is zero or // less, since this is used for stuff like combining characters. if (advance > new_advance or advance <= 0.0) { - break :offset_x @intFromFloat(@ceil(x - frac_x)); + break :offset_x @intFromFloat(@ceil(x)); } break :offset_x @intFromFloat( - @ceil(x - frac_x + (new_advance - advance) / 2), + @round(x + (new_advance - advance) / 2), ); } else { - break :offset_x @intFromFloat(@ceil(x - frac_x)); + break :offset_x @intFromFloat(@ceil(x)); } }; From f95476b1815353eb3b20668f32f767cff2bef358 Mon Sep 17 00:00:00 2001 From: Jason Rayne Date: Tue, 8 Jul 2025 10:45:42 -0700 Subject: [PATCH 63/86] refactor: apply maintainer feedback to SSH integration scripts across all shells - Update Bash script (baseline): Simplify cache checking logic, clarify "xterm-ghostty terminfo" message, remove unnecessary ssh_opts from terminfo installation, remove extra success message - Align ZSH/Fish/Elvish with updated Bash: Remove extra success messages, adopt simplified cache checking, standardize setup messages - Apply Elvish improvements: Remove unnecessary try/catch blocks, use idiomatic error handling patterns - Apply Fish improvements: Replace string pattern matching with efficient `contains` checks on split features list --- src/shell-integration/bash/ghostty.bash | 12 +- .../elvish/lib/ghostty-integration.elv | 127 ++++++------------ .../ghostty-shell-integration.fish | 20 +-- src/shell-integration/zsh/ghostty-integration | 10 +- 4 files changed, 56 insertions(+), 113 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 5b6bb249d..63255bbc3 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -124,12 +124,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then if [[ -n "$ssh_hostname" ]]; then # Check if terminfo is already cached - builtin local ssh_cache_check_success=false - if builtin command -v ghostty >/dev/null 2>&1; then - ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true - fi - - if [[ "$ssh_cache_check_success" == "true" ]]; then + if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif builtin command -v infocmp >/dev/null 2>&1; then builtin local ssh_terminfo ssh_cpath_dir ssh_cpath @@ -137,18 +132,17 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null) if [[ -n "$ssh_terminfo" ]]; then - builtin echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 + builtin echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2 ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" ssh_cpath="$ssh_cpath_dir/socket" - if builtin echo "$ssh_terminfo" | builtin command ssh "${ssh_opts[@]}" -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' + if builtin echo "$ssh_terminfo" | builtin command ssh -o ControlMaster=yes -o ControlPath="$ssh_cpath" -o ControlPersist=60s "$@" ' infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 command -v tic >/dev/null 2>&1 || exit 1 mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 exit 1 ' 2>/dev/null; then - builtin echo "Terminfo setup complete on $ssh_hostname." >&2 ssh_term="xterm-ghostty" ssh_opts+=(-o "ControlPath=$ssh_cpath") diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 44cf135dc..2eadbfd06 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -108,8 +108,10 @@ # Configure environment variables for remote session if (str:contains $E:GHOSTTY_SHELL_FEATURES ssh-env) { - set ssh-opts = [$@ssh-opts -o "SetEnv COLORTERM=truecolor"] - set ssh-opts = [$@ssh-opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION"] + set ssh-opts = (conj $ssh-opts + -o "SetEnv COLORTERM=truecolor" + -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION" + ) } # Install terminfo on remote host if needed @@ -117,112 +119,71 @@ var ssh-user = "" var ssh-hostname = "" - try { - var ssh-config = (external ssh -G $@args 2>/dev/null | slurp) - for line (str:split "\n" $ssh-config) { - var parts = (str:split " " $line) - if (> (count $parts) 1) { - if (eq $parts[0] user) { - set ssh-user = $parts[1] - } elif (eq $parts[0] hostname) { - set ssh-hostname = $parts[1] - } - if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { - break - } + # Parse ssh config + var ssh-config = (external ssh -G $@args 2>/dev/null | slurp) + for line (str:split "\n" $ssh-config) { + var parts = (str:split " " $line) + if (> (count $parts) 1) { + var ssh-key = $parts[0] + var ssh-value = $parts[1] + if (eq $ssh-key user) { + set ssh-user = $ssh-value + } elif (eq $ssh-key hostname) { + set ssh-hostname = $ssh-value + } + if (and (not-eq $ssh-user "") (not-eq $ssh-hostname "")) { + break } } - } catch { - # ssh config failed } var ssh-target = $ssh-user"@"$ssh-hostname if (not-eq $ssh-hostname "") { # Check if terminfo is already cached - var ssh-cache-check-success = $false - try { - external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1 - set ssh-cache-check-success = $true - } catch { - # cache check failed - } - - if $ssh-cache-check-success { + if (and (has-external ghostty) (bool ?(external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1))) { set ssh-term = "xterm-ghostty" - } else { - try { - external infocmp --help >/dev/null 2>&1 + } elif (has-external infocmp) { + var ssh-terminfo = (external infocmp -0 -x xterm-ghostty 2>/dev/null | slurp) + + if (not-eq $ssh-terminfo "") { + echo "Setting up xterm-ghostty terminfo on "$ssh-hostname"..." >&2 - var ssh-terminfo = "" var ssh-cpath-dir = "" - var ssh-cpath = "" - try { - set ssh-terminfo = (external infocmp -0 -x xterm-ghostty 2>/dev/null | slurp) + set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) } catch { - set ssh-terminfo = "" + set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) } + var ssh-cpath = $ssh-cpath-dir"/socket" - if (not-eq $ssh-terminfo "") { - echo "Setting up Ghostty terminfo on "$ssh-hostname"..." >&2 + if (bool ?(echo $ssh-terminfo | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' + infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 + command -v tic >/dev/null 2>&1 || exit 1 + mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 + exit 1 + ' 2>/dev/null)) { + set ssh-term = "xterm-ghostty" + set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath) - try { - set ssh-cpath-dir = (external mktemp -d "/tmp/ghostty-ssh-"$ssh-user".XXXXXX" 2>/dev/null | slurp) - } catch { - set ssh-cpath-dir = "/tmp/ghostty-ssh-"$ssh-user"."(randint 10000 99999) - } - set ssh-cpath = $ssh-cpath-dir"/socket" - - var terminfo-install-success = $false - try { - echo $ssh-terminfo | external ssh $@ssh-opts -o ControlMaster=yes -o ControlPath=$ssh-cpath -o ControlPersist=60s $@args ' - infocmp xterm-ghostty >/dev/null 2>&1 && exit 0 - command -v tic >/dev/null 2>&1 || exit 1 - mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 - exit 1 - ' >/dev/null 2>&1 - set terminfo-install-success = $true - } catch { - set terminfo-install-success = $false - } - - if $terminfo-install-success { - echo "Terminfo setup complete on "$ssh-hostname"." >&2 - set ssh-term = "xterm-ghostty" - set ssh-opts = [$@ssh-opts -o ControlPath=$ssh-cpath] - - # Cache successful installation - if (and (not-eq $ssh-target "") (has-external ghostty)) { - try { - external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 - } catch { - # cache add failed - } - } - } else { - echo "Warning: Failed to install terminfo." >&2 + # Cache successful installation + if (and (not-eq $ssh-target "") (has-external ghostty)) { + external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 } } else { - echo "Warning: Could not generate terminfo data." >&2 + echo "Warning: Failed to install terminfo." >&2 } - } catch { - echo "Warning: ghostty command not available for cache management." >&2 + } else { + echo "Warning: Could not generate terminfo data." >&2 } + } else { + echo "Warning: ghostty command not available for cache management." >&2 } } } # Execute SSH with TERM environment variable - var old-term = $E:TERM - set-env TERM $ssh-term - try { - external ssh $@ssh-opts $@args - } catch e { - set-env TERM $old-term - fail $e - } - set-env TERM $old-term + external E:TERM=$ssh-term ssh $@ssh-opts $@args } } diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index ab0f23086..0bba43b31 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -87,19 +87,21 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end # SSH Integration - if string match -q '*ssh-*' -- "$GHOSTTY_SHELL_FEATURES" + set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES") + if contains ssh-env $features; or contains ssh-terminfo $features function ssh --wraps=ssh --description "SSH wrapper with Ghostty integration" + set -l features (string split ',' -- "$GHOSTTY_SHELL_FEATURES") set -l ssh_term "xterm-256color" set -l ssh_opts # Configure environment variables for remote session - if string match -q '*ssh-env*' -- "$GHOSTTY_SHELL_FEATURES" + if contains ssh-env $features set -a ssh_opts -o "SetEnv COLORTERM=truecolor" set -a ssh_opts -o "SendEnv TERM_PROGRAM TERM_PROGRAM_VERSION" end # Install terminfo on remote host if needed - if string match -q '*ssh-terminfo*' -- "$GHOSTTY_SHELL_FEATURES" + if contains ssh-terminfo $features set -l ssh_user set -l ssh_hostname @@ -122,14 +124,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if test -n "$ssh_hostname" # Check if terminfo is already cached - set -l ssh_cache_check_success false - if command -v ghostty >/dev/null 2>&1 - if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 - set ssh_cache_check_success true - end - end - - if test "$ssh_cache_check_success" = "true" + if command -v ghostty >/dev/null 2>&1; and ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 set ssh_term "xterm-ghostty" else if command -v infocmp >/dev/null 2>&1 set -l ssh_terminfo @@ -139,7 +134,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set ssh_terminfo (infocmp -0 -x xterm-ghostty 2>/dev/null) if test -n "$ssh_terminfo" - echo "Setting up Ghostty terminfo on $ssh_hostname..." >&2 + echo "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2 set ssh_cpath_dir (mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null; or echo "/tmp/ghostty-ssh-$ssh_user."(random)) set ssh_cpath "$ssh_cpath_dir/socket" @@ -150,7 +145,6 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 exit 1 ' 2>/dev/null - echo "Terminfo setup complete on $ssh_hostname." >&2 set ssh_term "xterm-ghostty" set -a ssh_opts -o "ControlPath=$ssh_cpath" diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 7c7ab7972..60101416e 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -276,12 +276,7 @@ _ghostty_deferred_init() { if [[ -n "$ssh_hostname" ]]; then # Check if terminfo is already cached - local ssh_cache_check_success=false - if (( $+commands[ghostty] )); then - ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 && ssh_cache_check_success=true - fi - - if [[ "$ssh_cache_check_success" == "true" ]]; then + if (( $+commands[ghostty] )) && ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif (( $+commands[infocmp] )); then local ssh_terminfo ssh_cpath_dir ssh_cpath @@ -289,7 +284,7 @@ _ghostty_deferred_init() { ssh_terminfo=$(infocmp -0 -x xterm-ghostty 2>/dev/null) if [[ -n "$ssh_terminfo" ]]; then - print "Setting up Ghostty terminfo on $ssh_hostname..." >&2 + print "Setting up xterm-ghostty terminfo on $ssh_hostname..." >&2 ssh_cpath_dir=$(mktemp -d "/tmp/ghostty-ssh-$ssh_user.XXXXXX" 2>/dev/null) || ssh_cpath_dir="/tmp/ghostty-ssh-$ssh_user.$$" ssh_cpath="$ssh_cpath_dir/socket" @@ -300,7 +295,6 @@ _ghostty_deferred_init() { mkdir -p ~/.terminfo 2>/dev/null && tic -x - 2>/dev/null && exit 0 exit 1 ' 2>/dev/null; then - print "Terminfo setup complete on $ssh_hostname." >&2 ssh_term="xterm-ghostty" ssh_opts+=(-o "ControlPath=$ssh_cpath") From 143066093303adb890c438a562275042ad19350c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 8 Jul 2025 11:56:49 -0600 Subject: [PATCH 64/86] font: constrain width of two-cell icon-height icons This stops things like folder icons from becoming over-wide. The patcher typically makes these glyphs always 1 cell wide, but since we know how it will be displayed we have the benefit of being able to make it more than 1 cell when there's room. This makes our dynamic scaling *better* than a static patched font :D --- src/font/face.zig | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/font/face.zig b/src/font/face.zig index 8c1171fb4..dc36b0286 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -205,7 +205,7 @@ pub const RenderOptions = struct { ) GlyphSize { var g = glyph; - const available_width: f64 = @floatFromInt( + var available_width: f64 = @floatFromInt( metrics.cell_width * @min( self.max_constraint_width, constraint_width, @@ -216,6 +216,22 @@ pub const RenderOptions = struct { .icon => metrics.icon_height, }); + // We make the opinionated choice here to reduce the width + // of icon-height symbols by the same amount horizontally, + // since otherwise wide aspect ratio icons like folders end + // up far too wide. + // + // But we *only* do this if the constraint width is 2, since + // otherwise it would make them way too small when sized for + // a single cell. + const is_icon_width = self.height == .icon and @min(self.max_constraint_width, constraint_width) > 1; + const orig_avail_width = available_width; + if (is_icon_width) { + const cell_height: f64 = @floatFromInt(metrics.cell_height); + const ratio = available_height / cell_height; + available_width *= ratio; + } + const w = available_width - self.pad_left * available_width - self.pad_right * available_width; @@ -327,6 +343,11 @@ pub const RenderOptions = struct { .center => g.y = (h - g.height) / 2, } + // Add offset for icon width restriction, to keep it centered. + if (is_icon_width) { + g.x += (orig_avail_width - available_width) / 2; + } + // Re-add our padding before returning. g.x += self.pad_left * available_width; g.y += self.pad_bottom * available_height; From 8b8e0bedadf2c4c4b2b08f9c78a0c333c358a68b Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 8 Jul 2025 12:00:22 -0600 Subject: [PATCH 65/86] font: add scale groups to nerd font constraints We do this by characterizing the shared bounding boxes in a static copy of the symbols only nerd font when we're doing the codegen. This allows us to get results of our scaling that are just as good as in a patched font, since related glyphs can now be sized and positioned relative to each other. --- src/font/face.zig | 29 ++ src/font/nerd_font_attributes.zig | 707 +++++++++++++++++++++++++++++- src/font/nerd_font_codegen.py | 91 +++- 3 files changed, 818 insertions(+), 9 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index dc36b0286..fc5118c3d 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -144,6 +144,23 @@ pub const RenderOptions = struct { /// Bottom padding when resizing. pad_bottom: f64 = 0.0, + // This acts as a multiple of the provided width when applying + // constraints, so if this is 1.6 for example, then a width of + // 10 would be treated as though it were 16. + group_width: f64 = 1.0, + // This acts as a multiple of the provided height when applying + // constraints, so if this is 1.6 for example, then a height of + // 10 would be treated as though it were 16. + group_height: f64 = 1.0, + // This is an x offset for the actual width within the group width. + // If this is 0.5 then the glyph will be offset so that its left + // edge sits at the halfway point of the group width. + group_x: f64 = 0.0, + // This is a y offset for the actual height within the group height. + // If this is 0.5 then the glyph will be offset so that its bottom + // edge sits at the halfway point of the group height. + group_y: f64 = 0.0, + /// Maximum ratio of width to height when resizing. max_xy_ratio: ?f64 = null, @@ -245,6 +262,10 @@ pub const RenderOptions = struct { g.x -= self.pad_left * available_width; g.y -= self.pad_bottom * available_height; + // Multiply by group width and height for better sizing. + g.width *= self.group_width; + g.height *= self.group_height; + switch (self.size_horizontal) { .none => {}, .fit => if (g.width > w) { @@ -323,6 +344,14 @@ pub const RenderOptions = struct { }, } + // Add group-relative position + g.x += self.group_x * g.width; + g.y += self.group_y * g.height; + + // Divide group width and height back out before we align. + g.width /= self.group_width; + g.height /= self.group_height; + if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) { const orig_width = g.width; g.width = g.height * ratio; diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index e72c7a00e..4ec55d2ff 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -34,7 +34,35 @@ pub fn getConstraint(cp: u21) Constraint { .pad_top = 0.1, .pad_bottom = 0.1, }, - 0x276c...0x2771, + 0x276c...0x276d, + => .{ + .size_horizontal = .cover, + .size_vertical = .fit, + .max_constraint_width = 1, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.4028056112224450, + .group_height = 1.1222570532915361, + .group_x = 0.1428571428571428, + .group_y = 0.0349162011173184, + .pad_top = 0.15, + .pad_bottom = 0.15, + }, + 0x276e...0x276f, + => .{ + .size_horizontal = .cover, + .size_vertical = .fit, + .max_constraint_width = 1, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0115606936416186, + .group_height = 1.1222570532915361, + .group_x = 0.0057142857142857, + .group_y = 0.0125698324022346, + .pad_top = 0.15, + .pad_bottom = 0.15, + }, + 0x2770...0x2771, => .{ .size_horizontal = .cover, .size_vertical = .fit, @@ -357,8 +385,46 @@ pub fn getConstraint(cp: u21) Constraint { 0x2b58, 0xe000...0xe0a9, 0xe4fa...0xe7ef, - 0xea60...0xec1e, - 0xed00...0xf847, + 0xea60, + 0xea62...0xea7c, + 0xea7e...0xea98, + 0xeaa3...0xeab3, + 0xeab8...0xead3, + 0xead7...0xeb42, + 0xeb44...0xeb6d, + 0xeb72...0xeb89, + 0xeb8b...0xeb99, + 0xeb9b...0xebd4, + 0xebd6, + 0xebd8...0xec06, + 0xec08...0xec0a, + 0xec0d...0xec1e, + 0xed00...0xf018, + 0xf01a...0xf02f, + 0xf031...0xf03c, + 0xf041...0xf043, + 0xf045...0xf049, + 0xf04b...0xf050, + 0xf054...0xf059, + 0xf05c...0xf070, + 0xf072...0xf077, + 0xf079...0xf07a, + 0xf07c...0xf080, + 0xf082...0xf08b, + 0xf08d...0xf091, + 0xf093...0xf09b, + 0xf09d...0xf09e, + 0xf0a0, + 0xf0a5...0xf0a9, + 0xf0ab...0xf0c9, + 0xf0cb...0xf0d5, + 0xf0d7...0xf0dd, + 0xf0df...0xf0e6, + 0xf0e8...0xf295, + 0xf297...0xf2c1, + 0xf2c6...0xf2ef, + 0xf2f1...0xf305, + 0xf307...0xf847, 0xf0001...0xf1af0, => .{ .size_horizontal = .fit, @@ -367,6 +433,641 @@ pub fn getConstraint(cp: u21) Constraint { .align_horizontal = .center, .align_vertical = .center, }, + 0xea61, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.3315669947009841, + .group_height = 1.0763840224246670, + .group_x = 0.0847072200113701, + .group_y = 0.0709635416666667, + }, + 0xea7d, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1913145539906103, + .group_height = 1.1428571428571428, + .group_x = 0.0916256157635468, + .group_y = 0.0415039062500000, + }, + 0xea99, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0639412997903563, + .group_height = 2.0940695296523519, + .group_x = 0.0295566502463054, + .group_y = 0.2270507812500000, + }, + 0xea9a, + 0xeaa1, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.3029525032092426, + .group_height = 1.1729667812142039, + .group_x = 0.1527093596059113, + .group_y = 0.0751953125000000, + }, + 0xea9b, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1639908256880733, + .group_height = 1.3128205128205128, + .group_x = 0.0719211822660099, + .group_y = 0.0869140625000000, + }, + 0xea9c, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1639908256880733, + .group_height = 1.3195876288659794, + .group_x = 0.0719211822660099, + .group_y = 0.0830078125000000, + }, + 0xea9d, + 0xeaa0, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 2.4457831325301207, + .group_height = 1.9692307692307693, + .group_x = 0.2857142857142857, + .group_y = 0.2763671875000000, + }, + 0xea9e...0xea9f, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.9556840077071291, + .group_height = 2.4674698795180725, + .group_x = 0.2137931034482759, + .group_y = 0.3066406250000000, + }, + 0xeaa2, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.2412121212121212, + .group_height = 1.0591799039527152, + .group_x = 0.0683593750000000, + .group_y = 0.0146484375000000, + }, + 0xeab4, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0049115913555993, + .group_height = 1.8998144712430427, + .group_y = 0.2026367187500000, + }, + 0xeab5, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.8979591836734695, + .group_height = 1.0054000981836033, + .group_x = 0.2023460410557185, + .group_y = 0.0053710937500000, + }, + 0xeab6, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.8979591836734695, + .group_height = 1.0054000981836033, + .group_x = 0.2707722385141740, + }, + 0xeab7, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0049115913555993, + .group_height = 1.8980537534754403, + .group_x = 0.0048875855327468, + .group_y = 0.2709960937500000, + }, + 0xead4...0xead5, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.4152542372881356, + .group_x = 0.1486118671747414, + }, + 0xead6, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_height = 1.1390433815350389, + .group_y = 0.0688476562500000, + }, + 0xeb43, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.3635153129161119, + .group_height = 1.0002360944082516, + .group_x = 0.1992187500000000, + .group_y = 0.0002360386808388, + }, + 0xeb6e, + 0xeb71, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_height = 2.0197238658777121, + .group_y = 0.2524414062500000, + }, + 0xeb6f, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 2.0098619329388558, + .group_x = 0.2492639842983317, + }, + 0xeb70, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 2.0098619329388558, + .group_height = 1.0039215686274510, + .group_x = 0.2492639842983317, + .group_y = 0.0039062500000000, + }, + 0xeb8a, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 2.8826979472140764, + .group_height = 2.9804097167804766, + .group_x = 0.2634791454730417, + .group_y = 0.3314678485576923, + }, + 0xeb9a, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1441340782122904, + .group_height = 1.0591799039527152, + .group_x = 0.0683593750000000, + .group_y = 0.0146484375000000, + }, + 0xebd5, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0723270440251573, + .group_height = 1.0728129910948141, + .group_y = 0.0678710937500000, + }, + 0xebd7, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_height = 1.0000418544302916, + .group_y = 0.0000418526785714, + }, + 0xec07, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 2.8615369874243446, + .group_height = 2.9789458113505249, + .group_x = 0.2609446802539727, + .group_y = 0.3313029661016949, + }, + 0xec0b, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0722513089005237, + .group_height = 1.0002360944082516, + .group_y = 0.0002360386808388, + }, + 0xec0c, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.2487804878048780, + .group_x = 0.1992187500000000, + }, + 0xf019, + 0xf08c, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0004882812500000, + }, + 0xf030, + 0xf03e, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0004882812500000, + .group_height = 1.1428571428571428, + .group_y = 0.0625000000000000, + }, + 0xf03d, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0004882812500000, + .group_height = 1.5014662756598240, + .group_y = 0.1669921875000000, + }, + 0xf03f, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.6018762826150690, + .group_x = 0.1876220107369448, + }, + 0xf040, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0006976906439684, + .group_height = 1.0001808776182035, + .group_x = 0.0006972042111134, + .group_y = 0.0001808449074074, + }, + 0xf044, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1029147024980515, + .group_height = 1.1024142703367676, + .group_x = 0.0463592039005675, + .group_y = 0.0430325010461710, + }, + 0xf04a, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0004882812500000, + .group_height = 1.3312975252838291, + .group_y = 0.1245571402616279, + }, + 0xf051, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.6007812500000000, + .group_height = 1.3312170271945341, + .group_x = 0.1874084919472914, + .group_y = 0.1245117187500000, + }, + 0xf052, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1436671384194865, + .group_height = 1.1430165816326530, + .group_x = 0.0624629273607646, + .group_y = 0.0625610266424885, + }, + 0xf053, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.8765709864847797, + .group_height = 1.0707191397207079, + .group_x = 0.2332599943628554, + .group_y = 0.0332682382480123, + }, + 0xf05a...0xf05b, + 0xf081, + 0xf092, + 0xf0aa, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0005935142780173, + .group_height = 1.0001395089285714, + .group_y = 0.0000697447342726, + }, + 0xf071, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0014662756598240, + .group_height = 1.1428571428571428, + .group_x = 0.0004880429477794, + .group_y = 0.0625000000000000, + }, + 0xf078, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0717654378877508, + .group_height = 1.8757195185766613, + .group_x = 0.0331834301604062, + .group_y = 0.1670386385827870, + }, + 0xf07b, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_height = 1.1428571428571428, + .group_y = 0.0625000000000000, + }, + 0xf09c, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_height = 1.0810546875000000, + }, + 0xf09f, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.7506617925122907, + .group_height = 1.0810546875000000, + .group_x = 0.2143937211981567, + }, + 0xf0a1, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0009775171065494, + .group_x = 0.0004882812500000, + }, + 0xf0a2, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.1433271023627367, + .group_height = 1.0001395089285714, + .group_x = 0.0624235731978609, + .group_y = 0.0000697447342726, + }, + 0xf0a3, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0005760656161586, + .group_height = 1.0001220681837999, + .group_x = 0.0004792774839344, + .group_y = 0.0000610266424885, + }, + 0xf0a4, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0005935142780173, + .group_height = 1.3335193452380951, + .group_y = 0.1250523085507044, + }, + 0xf0ca, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0005935142780173, + .group_height = 1.1922501247297521, + .group_y = 0.0806249128190822, + }, + 0xf0d6, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_height = 1.5014662756598240, + .group_y = 0.1669921875000000, + }, + 0xf0de, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.2253421114919656, + .group_height = 2.5216400911161729, + .group_x = 0.0918898809523810, + .group_y = 0.6034327009936766, + }, + 0xf0e7, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.3336843856081169, + .group_x = 0.1247597299147187, + }, + 0xf296, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0005148743038617, + .group_height = 1.0385966606705219, + .group_x = 0.0005146093447336, + .group_y = 0.0186218440507742, + }, + 0xf2c2...0xf2c3, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0000770970394737, + .group_height = 1.2864321608040201, + .group_y = 0.1113281250000000, + }, + 0xf2c4, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0344231791600214, + .group_x = 0.0166002826673519, + }, + 0xf2c5, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0004538836055876, + .group_height = 1.4840579710144928, + .group_x = 0.0004536776887225, + .group_y = 0.1630859375000000, + }, + 0xf2f0, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.0005935142780173, + .group_height = 1.0334438518091393, + .group_y = 0.0161807783512345, + }, + 0xf306, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.2427184466019416, + .group_x = 0.0976562500000000, + }, else => .none, }; } diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index 4087b9ac6..ad5cd0814 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -7,11 +7,16 @@ attributes and scaling rules. This does include an `eval` call! This is spooky, but we trust the nerd fonts code to be safe and not malicious or anything. -This script requires Python 3.12 or greater. +This script requires Python 3.12 or greater, requires that the `fontTools` +python module is installed, and requires that the path to a copy of the +SymbolsNerdFontMono font is passed as the first argument to the script. """ import ast +import sys import math +from fontTools.ttLib import TTFont +from fontTools.pens.boundsPen import BoundsPen from collections import defaultdict from contextlib import suppress from pathlib import Path @@ -19,7 +24,18 @@ from types import SimpleNamespace from typing import Literal, TypedDict, cast type PatchSetAttributes = dict[Literal["default"] | int, PatchSetAttributeEntry] -type AttributeHash = tuple[str | None, str | None, str, float, float, float] +type AttributeHash = tuple[ + str | None, + str | None, + str, + float, + float, + float, + float, + float, + float, + float, +] type ResolvedSymbol = PatchSetAttributes | PatchSetScaleRules | int | None @@ -34,6 +50,11 @@ class PatchSetAttributeEntry(TypedDict): stretch: str params: dict[str, float | bool] + group_x: float + group_y: float + group_width: float + group_height: float + class PatchSet(TypedDict): SymStart: int @@ -137,6 +158,10 @@ def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash: float(params.get("overlap", 0.0)), float(params.get("xy-ratio", -1.0)), float(params.get("ypadding", 0.0)), + float(attr.get("group_x", 0.0)), + float(attr.get("group_y", 0.0)), + float(attr.get("group_width", 1.0)), + float(attr.get("group_height", 1.0)), ) @@ -162,6 +187,11 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) stretch = attr.get("stretch", "") params = attr.get("params", {}) + group_x = attr.get("group_x", 0.0) + group_y = attr.get("group_y", 0.0) + group_width = attr.get("group_width", 1.0) + group_height = attr.get("group_height", 1.0) + overlap = params.get("overlap", 0.0) xy_ratio = params.get("xy-ratio", -1.0) y_padding = params.get("ypadding", 0.0) @@ -192,7 +222,6 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if "^" not in stretch: s += " .height = .icon,\n" - # There are two cases where we want to limit the constraint width to 1: # - If there's a `1` in the stretch mode string. # - If the stretch mode is `xy` and there's not an explicit `2`. @@ -204,6 +233,15 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) if valign is not None: s += f" .align_vertical = {valign},\n" + if group_width != 1.0: + s += f" .group_width = {group_width:.16f},\n" + if group_height != 1.0: + s += f" .group_height = {group_height:.16f},\n" + if group_x != 0.0: + s += f" .group_x = {group_x:.16f},\n" + if group_y != 0.0: + s += f" .group_y = {group_y:.16f},\n" + # `overlap` and `ypadding` are mutually exclusive, # this is asserted in the nerd fonts patcher itself. if overlap: @@ -226,16 +264,53 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry) return s -def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str: +def generate_zig_switch_arms( + patch_sets: list[PatchSet], + nerd_font: TTFont, +) -> str: + cmap = nerd_font.getBestCmap() + glyphs = nerd_font.getGlyphSet() + entries: dict[int, PatchSetAttributeEntry] = {} for entry in patch_sets: attributes = entry["Attributes"] for cp in range(entry["SymStart"], entry["SymEnd"] + 1): - entries[cp] = attributes["default"] + entries[cp] = attributes["default"].copy() entries |= {k: v for k, v in attributes.items() if isinstance(k, int)} + if entry["ScaleRules"] is not None and "ScaleGroups" in entry["ScaleRules"]: + for group in entry["ScaleRules"]["ScaleGroups"]: + xMin = math.inf + yMin = math.inf + xMax = -math.inf + yMax = -math.inf + individual_bounds: dict[int, tuple[int, int, int ,int]] = {} + for cp in group: + if cp not in cmap: + continue + glyph = glyphs[cmap[cp]] + bounds = BoundsPen(glyphSet=glyphs) + glyph.draw(bounds) + individual_bounds[cp] = bounds.bounds + xMin = min(bounds.bounds[0], xMin) + yMin = min(bounds.bounds[1], yMin) + xMax = max(bounds.bounds[2], xMax) + yMax = max(bounds.bounds[3], yMax) + group_width = xMax - xMin + group_height = yMax - yMin + for cp in group: + if cp not in cmap or cp not in entries: + continue + this_bounds = individual_bounds[cp] + this_width = this_bounds[2] - this_bounds[0] + this_height = this_bounds[3] - this_bounds[1] + entries[cp]["group_width"] = group_width / this_width + entries[cp]["group_height"] = group_height / this_height + entries[cp]["group_x"] = (this_bounds[0] - xMin) / group_width + entries[cp]["group_y"] = (this_bounds[1] - yMin) / group_height + del entries[0] # Group codepoints by attribute key @@ -256,6 +331,10 @@ def generate_zig_switch_arms(patch_sets: list[PatchSet]) -> str: if __name__ == "__main__": project_root = Path(__file__).resolve().parents[2] + nf_path = sys.argv[1] + + nerd_font = TTFont(nf_path) + patcher_path = project_root / "vendor" / "nerd-fonts" / "font-patcher.py" source = patcher_path.read_text(encoding="utf-8") patch_set = extract_patch_set_values(source) @@ -275,5 +354,5 @@ const Constraint = @import("face.zig").RenderOptions.Constraint; pub fn getConstraint(cp: u21) Constraint { return switch (cp) { """) - f.write(generate_zig_switch_arms(patch_set)) + f.write(generate_zig_switch_arms(patch_set, nerd_font)) f.write("\n else => .none,\n };\n}\n") From b8d5c1cf420d96151dc34eae522dff615941ef6b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 15:13:58 -0500 Subject: [PATCH 66/86] build: update `zig build update-translations` - The order of the arguments to xgettext influences the output. Since directorty walking does not guarantee that files will be listed in a deterministic order (especially when run on different systems) the translation files would see a lot of churn depending on who updated them last. In this update the files are sorted so that the arguments to xgettext are always in the same order. This should reduce churn in the future. - Mark all of the files as inputs so that the Zig build system caching will work properly. --- src/build/GhosttyI18n.zig | 56 ++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig index 7667d30c3..778cfabc5 100644 --- a/src/build/GhosttyI18n.zig +++ b/src/build/GhosttyI18n.zig @@ -79,24 +79,38 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step { xgettext.has_side_effects = true; inline for (gresource.blueprint_files) |blp| { - // We avoid using addFileArg here since the full, absolute file path - // would be added to the file as its location, which differs for - // everyone's checkout of the repository. - // This comes at a cost of losing per-file caching, of course. - xgettext.addArg(std.fmt.comptimePrint( + const path = std.fmt.comptimePrint( "src/apprt/gtk/ui/{[major]}.{[minor]}/{[name]s}.blp", blp, - )); + ); + // The arguments to xgettext must be the relative path in the build root + // or the resulting files will contain the absolute path. This will cause + // a lot of churn because not everyone has the Ghostty code checked out in + // exactly the same location. + xgettext.addArg(path); + // Mark the file as an input so that the Zig build system caching will work. + xgettext.addFileInput(b.path(path)); } { - var gtk_files = try b.build_root.handle.openDir( + // Iterate over all of the files underneath `src/apprt/gtk`. We store + // them in an array so that they can be sorted into a determininistic + // order. That will minimize code churn as directory walking is not + // guaranteed to happen in any particular order. + + var gtk_files: std.ArrayListUnmanaged([]const u8) = .empty; + defer { + for (gtk_files.items) |item| b.allocator.free(item); + gtk_files.deinit(b.allocator); + } + + var gtk_dir = try b.build_root.handle.openDir( "src/apprt/gtk", .{ .iterate = true }, ); - defer gtk_files.close(); + defer gtk_dir.close(); - var walk = try gtk_files.walk(b.allocator); + var walk = try gtk_dir.walk(b.allocator); defer walk.deinit(); while (try walk.next()) |src| { switch (src.kind) { @@ -109,7 +123,29 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step { else => continue, } - xgettext.addArg((b.pathJoin(&.{ "src/apprt/gtk", src.path }))); + try gtk_files.append(b.allocator, try b.allocator.dupe(u8, src.path)); + } + + std.mem.sort( + []const u8, + gtk_files.items, + {}, + struct { + fn lt(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.mem.order(u8, lhs, rhs) == .lt; + } + }.lt, + ); + + for (gtk_files.items) |item| { + const path = b.pathJoin(&.{ "src/apprt/gtk", item }); + // The arguments to xgettext must be the relative path in the build root + // or the resulting files will contain the absolute path. This will + // cause a lot of churn because not everyone has the Ghostty code + // checked out in exactly the same location. + xgettext.addArg(path); + // Mark the file as an input so that the Zig build system caching will work. + xgettext.addFileInput(b.path(path)); } } From 68c9ab63b5dd3c352df4504cbba1dd12c7535404 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 15:19:27 -0500 Subject: [PATCH 67/86] i18n: update translations --- po/bg_BG.UTF-8.po | 118 +++++++++++++++++----------- po/ca_ES.UTF-8.po | 109 ++++++++++++++++---------- po/com.mitchellh.ghostty.pot | 64 ++++++++-------- po/de_DE.UTF-8.po | 109 ++++++++++++++++---------- po/es_AR.UTF-8.po | 101 ++++++++++++++---------- po/es_BO.UTF-8.po | 109 ++++++++++++++++---------- po/fr_FR.UTF-8.po | 109 ++++++++++++++++---------- po/ga_IE.UTF-8.po | 99 +++++++++++++++--------- po/he_IL.UTF-8.po | 78 ++++++++++--------- po/id_ID.UTF-8.po | 107 +++++++++++++++++--------- po/ja_JP.UTF-8.po | 107 +++++++++++++++++--------- po/ko_KR.UTF-8.po | 144 +++++++++++++++++++++++------------ po/mk_MK.UTF-8.po | 107 +++++++++++++++++--------- po/nb_NO.UTF-8.po | 105 ++++++++++++++++--------- po/nl_NL.UTF-8.po | 109 ++++++++++++++++---------- po/pl_PL.UTF-8.po | 105 ++++++++++++++++--------- po/pt_BR.UTF-8.po | 107 +++++++++++++++++--------- po/ru_RU.UTF-8.po | 109 ++++++++++++++++---------- po/tr_TR.UTF-8.po | 109 ++++++++++++++++---------- po/uk_UA.UTF-8.po | 107 +++++++++++++++++--------- po/zh_CN.UTF-8.po | 64 ++++++++-------- 21 files changed, 1371 insertions(+), 805 deletions(-) diff --git a/po/bg_BG.UTF-8.po b/po/bg_BG.UTF-8.po index 18cadddf5..84fd455e2 100644 --- a/po/bg_BG.UTF-8.po +++ b/po/bg_BG.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-05-19 11:34+0300\n" "Last-Translator: Damyan Bogoev \n" "Language-Team: Bulgarian \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Оставете празно за възстановяване на заглавието по подразбиране." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Отказ" @@ -35,22 +36,28 @@ msgid "OK" msgstr "ОК" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Грешки в конфигурацията" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" -"One or more configuration errors were found. Please review the errors " -"below, and either reload your configuration or ignore these errors." -msgstr "Открити са една или повече грешки в конфигурацията. Моля, прегледайте грешките по-долу и или презаредете конфигурацията си, или ги игнорирайте." +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Открити са една или повече грешки в конфигурацията. Моля, прегледайте " +"грешките по-долу и или презаредете конфигурацията си, или ги игнорирайте." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Игнорирай" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Презареди конфигурацията" @@ -89,7 +96,7 @@ msgstr "Копирай" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Постави" @@ -119,7 +126,7 @@ msgstr "Раздел" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:255 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Нов раздел" @@ -160,7 +167,7 @@ msgid "Terminal Inspector" msgstr "Инспектор на терминала" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1024 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "За Ghostty" @@ -170,69 +177,64 @@ msgstr "Изход" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Разрешаване на достъп до клипборда" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." -msgstr "Приложение се опитва да чете от клипборда. Текущото съдържание на клипборда е показано по-долу." +msgstr "" +"Приложение се опитва да чете от клипборда. Текущото съдържание на клипборда " +"е показано по-долу." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Откажи" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Позволи" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." -msgstr "Приложение се опитва да запише в клипборда. Текущото съдържание на клипборда е показано по-долу." +msgstr "" +"Приложение се опитва да запише в клипборда. Текущото съдържание на клипборда " +"е показано по-долу." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Предупреждение: Потенциално опасно поставяне" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "Поставянето на този текст в терминала може да е опасно, тъй като изглежда, че може да бъдат изпълнени някои команди." - -#: src/apprt/gtk/Window.zig:208 -msgid "Main Menu" -msgstr "Главно меню" - -#: src/apprt/gtk/Window.zig:229 -msgid "View Open Tabs" -msgstr "Преглед на отворените раздели" - -#: src/apprt/gtk/Window.zig:256 -msgid "New Split" -msgstr "Ново разделяне" - -#: src/apprt/gtk/Window.zig:319 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Използвате дебъг версия на Ghostty! Производителността ще бъде намалена." - -#: src/apprt/gtk/Window.zig:765 -msgid "Reloaded the configuration" -msgstr "Конфигурацията е презаредена" - -#: src/apprt/gtk/Window.zig:1005 -msgid "Ghostty Developers" -msgstr "Разработчици на Ghostty" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Инспектор на терминала" +msgstr "" +"Поставянето на този текст в терминала може да е опасно, тъй като изглежда, " +"че може да бъдат изпълнени някои команди." #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" @@ -270,6 +272,36 @@ msgstr "Всички терминални сесии в този раздел щ msgid "The currently running process in this split will be terminated." msgstr "Текущият процес в това разделяне ще бъде прекратен." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Копирано в клипборда" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Главно меню" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Преглед на отворените раздели" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Ново разделяне" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Използвате дебъг версия на Ghostty! Производителността ще бъде намалена." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Конфигурацията е презаредена" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Разработчици на Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Инспектор на терминала" diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 653439fa2..11bc99f57 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-20 08:07+0100\n" "Last-Translator: Francesc Arpi \n" "Language-Team: \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Deixa en blanc per restaurar el títol per defecte." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Cancel·la" @@ -35,10 +36,12 @@ msgid "OK" msgstr "D'acord" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Errors de configuració" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -47,12 +50,14 @@ msgstr "" "a continuació i torna a carregar la configuració o ignora aquests errors." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Ignora" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Carrega la configuració" @@ -80,6 +85,10 @@ msgstr "Divideix a l'esquerra" msgid "Split Right" msgstr "Divideix a la dreta" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -87,7 +96,7 @@ msgstr "Copia" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Enganxa" @@ -117,7 +126,7 @@ msgstr "Pestanya" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Nova pestanya" @@ -145,29 +154,36 @@ msgid "Config" msgstr "Configuració" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Obre la configuració" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Inspector de terminal" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Sobre Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Surt" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Autoritza l'accés al porta-retalls" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -177,15 +193,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Denegar" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Permet" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -193,11 +224,11 @@ msgstr "" "Una aplicació està intentant escriure al porta-retalls. El contingut actual " "del porta-retalls es mostra a continuació." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Avís: Enganxament potencialment insegur" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -205,33 +236,6 @@ msgstr "" "Enganxar aquest text al terminal pot ser perillós, ja que sembla que es " "podrien executar algunes ordres." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Menú principal" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Mostra les pestanyes obertes" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Estàs executant una versió de depuració de Ghostty! El rendiment es veurà " -"afectat." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "S'ha tornat a carregar la configuració" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Desenvolupadors de Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tanca" @@ -268,10 +272,37 @@ msgstr "Totes les sessions del terminal en aquesta pestanya es tancaran." msgid "The currently running process in this split will be terminated." msgstr "El procés actualment en execució en aquesta divisió es tancarà." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Copiat al porta-retalls" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Mostra les pestanyes obertes" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Estàs executant una versió de depuració de Ghostty! El rendiment es veurà " +"afectat." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "S'ha tornat a carregar la configuració" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Desenvolupadors de Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de terminal" diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index 7691f91b5..584f843b6 100644 --- a/po/com.mitchellh.ghostty.pot +++ b/po/com.mitchellh.ghostty.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-06-28 17:01+0200\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -124,7 +124,7 @@ msgstr "" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:263 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "" @@ -165,7 +165,7 @@ msgid "Terminal Inspector" msgstr "" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1036 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "" @@ -228,35 +228,6 @@ msgid "" "commands may be executed." msgstr "" -#: src/apprt/gtk/Window.zig:216 -msgid "Main Menu" -msgstr "" - -#: src/apprt/gtk/Window.zig:238 -msgid "View Open Tabs" -msgstr "" - -#: src/apprt/gtk/Window.zig:264 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:327 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" - -#: src/apprt/gtk/Window.zig:773 -msgid "Reloaded the configuration" -msgstr "" - -#: src/apprt/gtk/Window.zig:1017 -msgid "Ghostty Developers" -msgstr "" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "" @@ -296,3 +267,32 @@ msgstr "" #: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 2d3b96d81..fcca71101 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-06 14:57+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" @@ -27,7 +27,8 @@ msgid "Leave blank to restore the default title." msgstr "Leer lassen, um den Standardtitel wiederherzustellen." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Abbrechen" @@ -36,22 +37,26 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Konfiguration neu laden" @@ -79,6 +84,10 @@ msgstr "Fenter nach links teilen" msgid "Split Right" msgstr "Fenster nach rechts teilen" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -86,7 +95,7 @@ msgstr "Kopieren" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Einfügen" @@ -116,7 +125,7 @@ msgstr "Tab" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Neuer Tab" @@ -144,29 +153,36 @@ msgid "Config" msgstr "Konfiguration" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Konfiguration öffnen" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Terminalinspektor" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Über Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Beenden" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Zugriff auf die Zwischenablage gewähren" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -176,15 +192,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Nicht erlauben" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Erlauben" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -192,11 +223,11 @@ msgstr "" "Eine Anwendung versucht in die Zwischenablage zu schreiben. Der aktuelle " "Inhalt der Zwischenablage wird unten angezeigt." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Achtung: Möglicherweise unsicheres Einfügen" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -204,33 +235,6 @@ msgstr "" "Diesen Text in das Terminal einzufügen könnte möglicherweise gefährlich " "sein. Es scheint, dass Anweisungen ausgeführt werden könnten." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Hauptmenü" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Offene Tabs einblenden" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Du verwendest einen Debug Build von Ghostty! Die Leistung wird reduziert " -"sein." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Konfiguration wurde neu geladen" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Ghostty-Entwickler" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Schließen" @@ -267,10 +271,37 @@ msgstr "Alle Terminalsitzungen in diesem Tab werden beendet." msgid "The currently running process in this split will be terminated." msgstr "Der aktuell laufende Prozess in diesem geteilten Fenster wird beendet." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "In die Zwischenablage kopiert" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Hauptmenü" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Offene Tabs einblenden" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Du verwendest einen Debug Build von Ghostty! Die Leistung wird reduziert " +"sein." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Konfiguration wurde neu geladen" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty-Entwickler" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "" diff --git a/po/es_AR.UTF-8.po b/po/es_AR.UTF-8.po index 3cd0625c8..9b3b68693 100644 --- a/po/es_AR.UTF-8.po +++ b/po/es_AR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-05-19 20:17-0300\n" "Last-Translator: Alan Moyano \n" "Language-Team: Argentinian \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Dejar en blanco para restaurar el título predeterminado." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Cancelar" @@ -35,10 +36,12 @@ msgid "OK" msgstr "Aceptar" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Errores de configuración" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -47,12 +50,14 @@ msgstr "" "errores a continuación, y recargá tu configuración o ignorá estos errores." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Ignorar" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Recargar configuración" @@ -91,7 +96,7 @@ msgstr "Copiar" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Pegar" @@ -121,7 +126,7 @@ msgstr "Pestaña" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:255 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Nueva pestaña" @@ -162,7 +167,7 @@ msgid "Terminal Inspector" msgstr "Inspector de la terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1024 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Acerca de Ghostty" @@ -172,10 +177,13 @@ msgstr "Salir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Autorizar acceso al portapapeles" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -185,15 +193,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Denegar" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Permitir" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -201,11 +224,11 @@ msgstr "" "Una aplicación está intentando escribir en el portapapeles. El contenido " "actual del portapapeles se muestra a continuación." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Advertencia: Pegado potencialmente inseguro" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -213,37 +236,6 @@ msgstr "" "Pegar este texto en la terminal puede ser peligroso ya que parece que " "algunos comandos podrían ejecutarse." -#: src/apprt/gtk/Window.zig:208 -msgid "Main Menu" -msgstr "Menú principal" - -#: src/apprt/gtk/Window.zig:229 -msgid "View Open Tabs" -msgstr "Ver pestañas abiertas" - -#: src/apprt/gtk/Window.zig:256 -msgid "New Split" -msgstr "Nueva división" - -#: src/apprt/gtk/Window.zig:319 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Estás ejecutando una versión de depuración de Ghostty. El rendimiento no " -"será óptimo." - -#: src/apprt/gtk/Window.zig:765 -msgid "Reloaded the configuration" -msgstr "Configuración recargada" - -#: src/apprt/gtk/Window.zig:1005 -msgid "Ghostty Developers" -msgstr "Desarrolladores de Ghostty" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspector de la terminal" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Cerrar" @@ -280,6 +272,37 @@ msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." msgid "The currently running process in this split will be terminated." msgstr "El proceso actualmente en ejecución en esta división será terminado." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Nueva división" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Estás ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de la terminal" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index 077b7dfa1..c89b53f61 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Dejar en blanco para restaurar el título predeterminado." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Cancelar" @@ -35,10 +36,12 @@ msgid "OK" msgstr "Aceptar" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Errores de configuración" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -47,12 +50,14 @@ msgstr "" "errores a continuación, y recargue su configuración o ignore estos errores." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Ignorar" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Recargar configuración" @@ -80,6 +85,10 @@ msgstr "Dividir a la izquierda" msgid "Split Right" msgstr "Dividir a la derecha" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -87,7 +96,7 @@ msgstr "Copiar" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Pegar" @@ -117,7 +126,7 @@ msgstr "Pestaña" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Nueva pestaña" @@ -145,29 +154,36 @@ msgid "Config" msgstr "Configuración" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Abrir configuración" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Inspector de la terminal" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Acerca de Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Salir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Autorizar acceso al portapapeles" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -177,15 +193,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Denegar" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Permitir" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -193,11 +224,11 @@ msgstr "" "Una aplicación está intentando escribir en el portapapeles. El contenido " "actual del portapapeles se muestra a continuación." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Advertencia: Pegado potencialmente inseguro" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -205,33 +236,6 @@ msgstr "" "Pegar este texto en la terminal puede ser peligroso ya que parece que " "algunos comandos podrían ejecutarse." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Menú principal" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Ver pestañas abiertas" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " -"será óptimo." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Configuración recargada" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Desarrolladores de Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Cerrar" @@ -268,10 +272,37 @@ msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." msgid "The currently running process in this split will be terminated." msgstr "El proceso actualmente en ejecución en esta división será terminado." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Copiado al portapapeles" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de la terminal" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index aef0d96ac..2c227edaf 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-22 09:31+0100\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Laisser vide pour restaurer le titre par défaut." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Annuler" @@ -35,10 +36,12 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Erreurs de configuration" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -48,12 +51,14 @@ msgstr "" "erreurs." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Ignorer" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Recharger la configuration" @@ -81,6 +86,10 @@ msgstr "Panneau à gauche" msgid "Split Right" msgstr "Panneau à droite" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,7 +97,7 @@ msgstr "Copier" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Coller" @@ -118,7 +127,7 @@ msgstr "Onglet" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Nouvel onglet" @@ -146,29 +155,36 @@ msgid "Config" msgstr "Config" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Ouvrir la configuration" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Inspecteur de terminal" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "À propos de Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Quitter" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Autoriser l'accès au presse-papiers" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -178,15 +194,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Refuser" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Autoriser" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -194,11 +225,11 @@ msgstr "" "Une application essaie d'écrire dans le presse-papiers.Le contenu actuel du " "presse-papiers est affiché ci-dessous." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Attention: Collage potentiellement dangereux" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -206,33 +237,6 @@ msgstr "" "Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " "certaines commandes pourraient être exécutées." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Menu principal" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Voir les onglets ouverts" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Vous utilisez une version de débogage de Ghostty ! Les performances seront " -"dégradées." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Recharger la configuration" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Les développeurs de Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Fermer" @@ -269,10 +273,37 @@ msgstr "Toutes les sessions de cet onglet vont être arrêtées." msgid "The currently running process in this split will be terminated." msgstr "Le processus en cours dans ce panneau va être arrêté." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Copié dans le presse-papiers" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menu principal" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Voir les onglets ouverts" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Vous utilisez une version de débogage de Ghostty ! Les performances seront " +"dégradées." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Recharger la configuration" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Les développeurs de Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspecteur" diff --git a/po/ga_IE.UTF-8.po b/po/ga_IE.UTF-8.po index 686d22d76..3c8018ca0 100644 --- a/po/ga_IE.UTF-8.po +++ b/po/ga_IE.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-23 16:58+0800\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-06-29 21:15+0100\n" "Last-Translator: Aindriú Mac Giolla Eoin \n" "Language-Team: Irish \n" @@ -27,7 +27,8 @@ msgid "Leave blank to restore the default title." msgstr "Fág bán chun an teideal réamhshocraithe a athbhunú." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Cealaigh" @@ -36,10 +37,12 @@ msgid "OK" msgstr "Ceart go leor" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Earráidí cumraíochta" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -48,12 +51,14 @@ msgstr "" "thíos, agus athlódáil do chumraíocht nó déan neamhaird de na hearráidí seo." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Déan neamhaird de" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Athlódáil cumraíocht" @@ -92,7 +97,7 @@ msgstr "Cóipeáil" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Greamaigh" @@ -122,7 +127,7 @@ msgstr "Táb" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:255 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Táb nua" @@ -163,7 +168,7 @@ msgid "Terminal Inspector" msgstr "Cigire teirminéil" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1024 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Maidir le Ghostty" @@ -173,10 +178,13 @@ msgstr "Scoir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Údarú rochtain ar an ngearrthaisce" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -186,15 +194,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Diúltaigh" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Ceadaigh" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -202,11 +225,11 @@ msgstr "" "Tá feidhmchlár ag iarraidh scríobh chuig an ngearrthaisce. Taispeántar ábhar " "reatha an ghearrthaisce thíos." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Rabhadh: Greamaigh a d'fhéadfadh a bheith neamhshábháilte" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -214,36 +237,6 @@ msgstr "" "D’fhéadfadh sé a bheith contúirteach an téacs seo a ghreamú isteach sa " "teirminéal, toisc go d'fhéadfadh roinnt orduithe a fhorghníomhú." -#: src/apprt/gtk/Window.zig:208 -msgid "Main Menu" -msgstr "Príomh-Roghchlár" - -#: src/apprt/gtk/Window.zig:229 -msgid "View Open Tabs" -msgstr "Féach ar na táib oscailte" - -#: src/apprt/gtk/Window.zig:256 -msgid "New Split" -msgstr "Scoilt nua" - -#: src/apprt/gtk/Window.zig:319 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht." - -#: src/apprt/gtk/Window.zig:765 -msgid "Reloaded the configuration" -msgstr "Tá an chumraíocht athlódáilte" - -#: src/apprt/gtk/Window.zig:1005 -msgid "Ghostty Developers" -msgstr "Forbróirí Ghostty" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Cigire teirminéil" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Dún" @@ -281,6 +274,36 @@ msgid "The currently running process in this split will be terminated." msgstr "" "Cuirfear deireadh leis an bpróiseas atá ar siúl faoi láthair sa scoilt seo." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Cóipeáilte chuig an ghearrthaisce" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Príomh-Roghchlár" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Féach ar na táib oscailte" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Scoilt nua" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Tá leagan dífhabhtaithe de Ghostty á rith agat! Laghdófar an fheidhmíocht." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Tá an chumraíocht athlódáilte" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Forbróirí Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Cigire teirminéil" diff --git a/po/he_IL.UTF-8.po b/po/he_IL.UTF-8.po index 636bf46e3..7ca417908 100644 --- a/po/he_IL.UTF-8.po +++ b/po/he_IL.UTF-8.po @@ -7,9 +7,10 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-06-28 17:01+0200\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-13 00:00+0000\n" -"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" +"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd \n" "Language-Team: Hebrew \n" "Language: he\n" "MIME-Version: 1.0\n" @@ -45,7 +46,9 @@ msgstr "שגיאות בהגדרות" msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." -msgstr "נמצאו אחת או יותר שגיאות בהגדרות. אנא בדוק/י את השגיאות המופיעות מטה ולאחר מכן טען/י את ההגדרות מחדש או התעלם/י מהשגיאות." +msgstr "" +"נמצאו אחת או יותר שגיאות בהגדרות. אנא בדוק/י את השגיאות המופיעות מטה ולאחר " +"מכן טען/י את ההגדרות מחדש או התעלם/י מהשגיאות." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 #: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 @@ -124,7 +127,7 @@ msgstr "כרטיסייה" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:263 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "כרטיסייה חדשה" @@ -165,7 +168,7 @@ msgid "Terminal Inspector" msgstr "בודק המסוף" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1036 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "אודות Ghostty" @@ -216,7 +219,8 @@ msgstr "טען/י את ההגדרות מחדש כדי להציג את הבקשה msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." -msgstr "יש אפליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." +msgstr "" +"יש אפליקציה שמנסה לכתוב לתוך לוח ההעתקה. התוכן הנוכחי של הלוח מופיע למטה." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" @@ -226,36 +230,9 @@ msgstr "אזהרה: ההדבקה עלולה להיות מסוכנת" msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "הדבקת טקסט זה במסוף עלולה להיות מסוכנת, מכיוון שככל הנראה היא תוביל להרצה של פקודות מסוימות." - -#: src/apprt/gtk/Window.zig:216 -msgid "Main Menu" -msgstr "תפריט ראשי" - -#: src/apprt/gtk/Window.zig:238 -msgid "View Open Tabs" -msgstr "הצג/י כרטיסיות פתוחות" - -#: src/apprt/gtk/Window.zig:264 -msgid "New Split" -msgstr "פיצול חדש" - -#: src/apprt/gtk/Window.zig:327 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ את/ה מריץ/ה גרסת ניפוי שגיאות של Ghostty! הביצועים יהיו ירודים." - -#: src/apprt/gtk/Window.zig:773 -msgid "Reloaded the configuration" -msgstr "ההגדרות הוטענו מחדש" - -#: src/apprt/gtk/Window.zig:1017 -msgid "Ghostty Developers" -msgstr "המפתחים של Ghostty" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: בודק המסוף" +msgstr "" +"הדבקת טקסט זה במסוף עלולה להיות מסוכנת, מכיוון שככל הנראה היא תוביל להרצה של " +"פקודות מסוימות." #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" @@ -296,3 +273,32 @@ msgstr "התהליך שרץ כרגע בפיצול זה יסתיים." #: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "הועתק ללוח ההעתקה" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "תפריט ראשי" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "הצג/י כרטיסיות פתוחות" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "פיצול חדש" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ את/ה מריץ/ה גרסת ניפוי שגיאות של Ghostty! הביצועים יהיו ירודים." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "ההגדרות הוטענו מחדש" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "המפתחים של Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: בודק המסוף" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index f82ec6197..51b4bce60 100644 --- a/po/id_ID.UTF-8.po +++ b/po/id_ID.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-20 15:19+0700\n" "Last-Translator: Satrio Bayu Aji \n" "Language-Team: Indonesian \n" @@ -25,7 +25,8 @@ msgid "Leave blank to restore the default title." msgstr "Biarkan kosong untuk mengembalikan judul bawaan." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Batal" @@ -34,10 +35,12 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Kesalahan konfigurasi" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -46,12 +49,14 @@ msgstr "" "bawah ini, dan muat ulang konfigurasi anda atau abaikan kesalahan ini." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Abaikan" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Muat ulang konfigurasi" @@ -79,6 +84,10 @@ msgstr "Belah kiri" msgid "Split Right" msgstr "Belah kanan" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -86,7 +95,7 @@ msgstr "Salin" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Tempel" @@ -116,7 +125,7 @@ msgstr "Tab" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Tab baru" @@ -144,29 +153,36 @@ msgid "Config" msgstr "Konfigurasi" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Buka konfigurasi" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Inspektur terminal" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Tentang Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Keluar" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Mengesahkan akses papan klip" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -176,15 +192,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Menyangkal" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Izinkan" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -192,11 +223,11 @@ msgstr "" "Aplikasi sedang mencoba menulis ke papan klip. Isi papan klip saat ini " "ditampilkan di bawah ini." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Peringatan: Tempelan yang berpotensi tidak aman" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -204,32 +235,6 @@ msgstr "" "Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya " "beberapa perintah mungkin dijalankan." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Menu utama" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Lihat tab terbuka" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Memuat ulang konfigurasi" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Pengembang Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tutup" @@ -266,10 +271,36 @@ msgstr "Semua sesi terminal di tab ini akan diakhiri." msgid "The currently running process in this split will be terminated." msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Disalin ke papan klip" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menu utama" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Lihat tab terbuka" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Memuat ulang konfigurasi" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Pengembang Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspektur terminal" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index 73ddd9f5a..c965ea29f 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-21 00:08+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" @@ -27,7 +27,8 @@ msgid "Leave blank to restore the default title." msgstr "空白にした場合、デフォルトのタイトルを使用します。" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "キャンセル" @@ -36,10 +37,12 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "設定ファイルにエラーがあります" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -48,12 +51,14 @@ msgstr "" "みをするか、無視してください。" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "無視" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "設定ファイルの再読み込み" @@ -81,6 +86,10 @@ msgstr "左に分割" msgid "Split Right" msgstr "右に分割" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,7 +97,7 @@ msgstr "コピー" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "貼り付け" @@ -118,7 +127,7 @@ msgstr "タブ" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "新しいタブ" @@ -146,29 +155,36 @@ msgid "Config" msgstr "設定" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "設定ファイルを開く" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "端末インスペクター" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Ghostty について" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "終了" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "クリップボードへのアクセスを承認" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -178,15 +194,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "拒否" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "許可" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -194,11 +225,11 @@ msgstr "" "アプリケーションがクリップボードに書き込もうとしています。現在のクリップボー" "ドの内容は以下の通りです。" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "警告: 危険な可能性のある貼り付け" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -206,32 +237,6 @@ msgstr "" "このテキストには実行可能なコマンドが含まれており、ターミナルに貼り付けるのは" "危険な可能性があります。" -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "メインメニュー" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "開いているすべてのタブを表示" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "設定を再読み込みしました" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Ghostty 開発者" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "閉じる" @@ -268,10 +273,36 @@ msgstr "タブ内のすべてのターミナルセッションが終了します msgid "The currently running process in this split will be terminated." msgstr "分割ウィンドウ内のすべてのプロセスが終了します。" -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "クリップボードにコピーしました" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "メインメニュー" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "開いているすべてのタブを表示" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "設定を再読み込みしました" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty 開発者" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: 端末インスペクター" diff --git a/po/ko_KR.UTF-8.po b/po/ko_KR.UTF-8.po index 42cb2682f..9aa4aad5e 100644 --- a/po/ko_KR.UTF-8.po +++ b/po/ko_KR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-31 03:08+0200\n" "Last-Translator: Ruben Engelbrecht \n" "Language-Team: Korean \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "제목란을 비워 두면 기본값으로 복원됩니다." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "취소" @@ -35,25 +36,59 @@ msgid "OK" msgstr "확인" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "설정 오류" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." -msgstr "설정에 하나 이상의 문제가 발견되었습니다. 아래 오류(를)들을 확인한 후 설정을 다시 불러오거나 무시하세요." +msgstr "" +"설정에 하나 이상의 문제가 발견되었습니다. 아래 오류(를)들을 확인한 후 설정을 " +"다시 불러오거나 무시하세요." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "무시" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "설정 값 다시 불러오기" +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 +msgid "Split Up" +msgstr "위로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 +msgid "Split Down" +msgstr "아래로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 +msgid "Split Left" +msgstr "왼쪽으로 창 나누기" + +#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 +msgid "Split Right" +msgstr "오른쪽으로 창 나누기" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -61,7 +96,7 @@ msgstr "복사" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "붙여넣기" @@ -85,33 +120,13 @@ msgstr "나누기" msgid "Change Title…" msgstr "제목 변경…" -#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 -msgid "Split Up" -msgstr "위로 창 나누기" - -#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55 -msgid "Split Down" -msgstr "아래로 창 나누기" - -#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60 -msgid "Split Left" -msgstr "왼쪽으로 창 나누기" - -#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65 -msgid "Split Right" -msgstr "오른쪽으로 창 나누기" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" msgstr "탭" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:246 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "새 탭" @@ -139,67 +154,87 @@ msgid "Config" msgstr "설정" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "설정 열기" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "터미널 인스펙터" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Ghostty 정보" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "종료" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "클립보드 액세스 권한 부여" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." -msgstr "응용 프로그램이 클립보드에서 읽기를 시도하고 있습니다. 현재 클립보드 내용은 아래와 같습니다." +msgstr "" +"응용 프로그램이 클립보드에서 읽기를 시도하고 있습니다. 현재 클립보드 내용은 " +"아래와 같습니다." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "거부" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "허용" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." -msgstr "응용 프로그램이 클립보드에 쓰기를 시도하고 있습니다. 현재 클립보드 내용은 아래와 같습니다." +msgstr "" +"응용 프로그램이 클립보드에 쓰기를 시도하고 있습니다. 현재 클립보드 내용은 아" +"래와 같습니다." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "경고: 잠재적으로 안전하지 않은 붙여넣기" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "이 텍스트를 터미널에 붙여넣는 것은 위험할 수 있습니다. 일부 명령이 실행될 수 있는 것으로 보입니다." - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: 터미널 인스펙터" - -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "클립보드에 복사됨" +msgstr "" +"이 텍스트를 터미널에 붙여넣는 것은 위험할 수 있습니다. 일부 명령이 실행될 수 " +"있는 것으로 보입니다." #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" @@ -237,23 +272,36 @@ msgstr "이 탭의 모든 터미널 세션이 종료됩니다." msgid "The currently running process in this split will be terminated." msgstr "이 분할에서 현재 실행 중인 프로세스가 종료됩니다." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1257 +msgid "Copied to clipboard" +msgstr "클립보드에 복사됨" + +#: src/apprt/gtk/Window.zig:216 msgid "Main Menu" msgstr "메인 메뉴" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:239 msgid "View Open Tabs" msgstr "열린 탭 보기" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:266 +#, fuzzy +msgid "New Split" +msgstr "나누기" + +#: src/apprt/gtk/Window.zig:329 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "⚠️ Ghostty 디버그 빌드로 실행 중입니다! 성능이 저하됩니다." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:775 msgid "Reloaded the configuration" msgstr "설정값을 다시 불러왔습니다" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:1019 msgid "Ghostty Developers" msgstr "Ghostty 개발자들" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: 터미널 인스펙터" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 20a43572e..75bb81e00 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-23 14:17+0100\n" "Last-Translator: Andrej Daskalov \n" "Language-Team: Macedonian\n" @@ -25,7 +25,8 @@ msgid "Leave blank to restore the default title." msgstr "Оставете празно за враќање на стандарсниот наслов." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Откажи" @@ -34,10 +35,12 @@ msgid "OK" msgstr "Во ред" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Грешки во конфигурацијата" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -47,12 +50,14 @@ msgstr "" "овие грешки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Игнорирај" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Одново вчитај конфигурација" @@ -80,6 +85,10 @@ msgstr "Подели налево" msgid "Split Right" msgstr "Подели надесно" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -87,7 +96,7 @@ msgstr "Копирај" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Вметни" @@ -117,7 +126,7 @@ msgstr "Јазиче" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Ново јазиче" @@ -145,29 +154,36 @@ msgid "Config" msgstr "Конфигурација" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Отвори конфигурација" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Инспектор на терминал" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "За Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Излез" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Авторизирај пристап до привремена меморија" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -177,15 +193,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Одбиј" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Дозволи" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -193,11 +224,11 @@ msgstr "" "Апликација се обидува да запише во привремената меморија. Содржината е " "прикажана подолу." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Предупредување: Потенцијално небезбедно вметнување" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -205,32 +236,6 @@ msgstr "" "Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи " "изгледа како да ќе се извршат одредени команди." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Главно мени" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Прегледај отворени јазичиња" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Конфигурацијата е одново вчитана" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Развивачи на Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Затвори" @@ -267,10 +272,36 @@ msgstr "Сите сесии во ова јазиче ќе бидат преки msgid "The currently running process in this split will be terminated." msgstr "Процесот кој моментално се извршува во оваа поделба ќе биде прекинат." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Копирано во привремена меморија" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Главно мени" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Прегледај отворени јазичиња" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Конфигурацијата е одново вчитана" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Развивачи на Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Инспектор на терминал" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 045d47a80..28c1bc559 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -10,7 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-04-14 16:25+0200\n" "Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" @@ -29,7 +29,8 @@ msgid "Leave blank to restore the default title." msgstr "Blank verdi gjenoppretter standardtittelen." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Avbryt" @@ -38,10 +39,12 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Konfigurasjonsfeil" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -50,12 +53,14 @@ msgstr "" "under, og enten last konfigurasjonen din på nytt eller ignorer disse feilene." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Ignorer" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Last konfigurasjon på nytt" @@ -83,6 +88,10 @@ msgstr "Del til venstre" msgid "Split Right" msgstr "Del til høyre" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -90,7 +99,7 @@ msgstr "Kopier" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Lim inn" @@ -120,7 +129,7 @@ msgstr "Fane" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Ny fane" @@ -148,29 +157,36 @@ msgid "Config" msgstr "Konfigurasjon" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Åpne konfigurasjon" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Terminalinspektør" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Om Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Avslutt" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Gi tilgang til utklippstavlen" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -180,15 +196,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Avslå" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Tillat" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -196,11 +227,11 @@ msgstr "" "En applikasjon forsøker å skrive til utklippstavlen. Gjeldende " "utklippstavleinnhold er vist nedenfor." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Adarsel: Lim inn kan være utrygt" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -208,31 +239,6 @@ msgstr "" "Det ser ut som at kommandoer vil bli kjørt hvis du limer inn dette, vurder " "om du mener det er trygt." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Hovedmeny" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Se åpne faner" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "Del opp vindu" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Du kjører et debug-bygg av Ghostty. Debug-bygg har redusert ytelse." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Konfigurasjonen ble lastet på nytt" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Ghostty-utviklere" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Lukk" @@ -269,10 +275,35 @@ msgstr "Alle terminaløkter i denne fanen vil bli avsluttet." msgid "The currently running process in this split will be terminated." msgstr "Den kjørende prosessen for denne splitten vil bli avsluttet." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Kopiert til utklippstavlen" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Hovedmeny" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Se åpne faner" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Del opp vindu" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Du kjører et debug-bygg av Ghostty. Debug-bygg har redusert ytelse." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Konfigurasjonen ble lastet på nytt" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty-utviklere" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Terminalinspektør" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 355bc4a57..d64592f6d 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" "Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Laat leeg om de standaard titel te herstellen." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Annuleren" @@ -35,10 +36,12 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Configuratiefouten" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -47,12 +50,14 @@ msgstr "" "fouten en herlaad je configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Negeer" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Herlaad configuratie" @@ -80,6 +85,10 @@ msgstr "Splits naar links" msgid "Split Right" msgstr "Splits naar rechts" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -87,7 +96,7 @@ msgstr "Kopiëren" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Plakken" @@ -117,7 +126,7 @@ msgstr "Tabblad" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Nieuw tabblad" @@ -145,29 +154,36 @@ msgid "Config" msgstr "Configuratie" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Open configuratie" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Terminal inspecteur" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Over Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Afsluiten" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Verleen toegang tot klembord" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -177,15 +193,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Weigeren" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Toestaan" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -193,11 +224,11 @@ msgstr "" "Een applicatie probeert de inhoud van het klembord te wijzigen. De huidige " "inhoud van het klembord wordt hieronder weergegeven." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Waarschuwing: mogelijk onveilige plakactie" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -205,33 +236,6 @@ msgstr "" "Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " "lijkt op een commando dat uitgevoerd kan worden." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Hoofdmenu" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Open tabbladen bekijken" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan " -"normaal." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "De configuratie is herladen" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Ghostty ontwikkelaars" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Afsluiten" @@ -269,10 +273,37 @@ msgid "The currently running process in this split will be terminated." msgstr "" "Alle processen die nu draaien in deze splitsing zullen worden beëindigd." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Gekopieerd naar klembord" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Hoofdmenu" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Open tabbladen bekijken" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan " +"normaal." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "De configuratie is herladen" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty ontwikkelaars" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: terminal inspecteur" diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index a68d56818..4f281b415 100644 --- a/po/pl_PL.UTF-8.po +++ b/po/pl_PL.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-17 12:15+0100\n" "Last-Translator: Bartosz Sokorski \n" "Language-Team: Polish \n" @@ -28,7 +28,8 @@ msgid "Leave blank to restore the default title." msgstr "Pozostaw puste by przywrócić domyślny tytuł." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Anuluj" @@ -37,10 +38,12 @@ msgid "OK" msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Błędy konfiguracji" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -49,12 +52,14 @@ msgstr "" "poniżej i przeładuj konfigurację lub zignoruj je." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Zignoruj" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Przeładuj konfigurację" @@ -82,6 +87,10 @@ msgstr "Podziel w lewo" msgid "Split Right" msgstr "Podziel w prawo" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -89,7 +98,7 @@ msgstr "Kopiuj" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Wklej" @@ -119,7 +128,7 @@ msgstr "Karta" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Nowa karta" @@ -147,29 +156,36 @@ msgid "Config" msgstr "Konfiguracja" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Otwórz konfigurację" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Inspektor terminala" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "O Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Zamknij" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Udziel dostępu do schowka" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -179,15 +195,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Odmów" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Zezwól" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -195,11 +226,11 @@ msgstr "" "Aplikacja próbuje zapisać do schowka. Obecna zawartość schowka pokazana " "poniżej." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Uwaga: potencjalnie niebezpieczne wklejenie ze schowka" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -207,31 +238,6 @@ msgstr "" "Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " "spowodować wykonanie komend." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Menu główne" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Zobacz otwarte karty" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Używasz wersji Ghostty do debugowania! Wydajność będzie obniżona." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Przeładowano konfigurację" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Twórcy Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Zamknij" @@ -268,10 +274,35 @@ msgstr "Wszystkie sesje terminala w obecnej karcie zostaną zakończone." msgid "The currently running process in this split will be terminated." msgstr "Wszyskie trwające procesy w obecnym podziale zostaną zakończone." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Skopiowano do schowka" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menu główne" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Zobacz otwarte karty" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Używasz wersji Ghostty do debugowania! Wydajność będzie obniżona." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Przeładowano konfigurację" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Twórcy Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Inspektor terminala Ghostty" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index ba13f4460..2979248f2 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-06-20 10:19-0300\n" "Last-Translator: Mário Victor Ribeiro Silva \n" "Language-Team: Brazilian Portuguese \n" "Language-Team: Russian \n" @@ -28,7 +28,8 @@ msgid "Leave blank to restore the default title." msgstr "Оставьте пустым, чтобы восстановить исходный заголовок." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Отмена" @@ -37,10 +38,12 @@ msgid "OK" msgstr "ОК" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Ошибки конфигурации" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -49,12 +52,14 @@ msgstr "" "конфигурацию, либо проигнорируйте ошибки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Игнорировать" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Обновить конфигурацию" @@ -82,6 +87,10 @@ msgstr "Сплит влево" msgid "Split Right" msgstr "Сплит вправо" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -89,7 +98,7 @@ msgstr "Копировать" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Вставить" @@ -119,7 +128,7 @@ msgstr "Вкладка" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Новая вкладка" @@ -147,29 +156,36 @@ msgid "Config" msgstr "Конфигурация" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Открыть конфигурационный файл" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Инспектор терминала" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "О Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Выход" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Разрешить доступ к буферу обмена" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -179,26 +195,41 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Отклонить" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Разрешить" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" "Приложение пытается записать данные в буфер обмена. Эти данные показаны ниже." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Внимание! Вставляемые данные могут нанести вред вашей системе" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -206,33 +237,6 @@ msgstr "" "Вставка этого текста в терминал может быть опасной. Это выглядит как " "команды, которые могут быть исполнены." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Главное меню" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Просмотреть открытые вкладки" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на " -"производительность." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Конфигурация была обновлена" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Разработчики Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрыть" @@ -269,10 +273,37 @@ msgstr "Все сессии терминала в этой вкладке буд msgid "The currently running process in this split will be terminated." msgstr "Процесс, работающий в этой сплит-области, будет остановлен." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Скопировано в буфер обмена" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Главное меню" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Просмотреть открытые вкладки" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на " +"производительность." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Конфигурация была обновлена" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Разработчики Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: инспектор терминала" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index 5d761f6a4..7d8d055f8 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-24 22:01+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" @@ -26,7 +26,8 @@ msgid "Leave blank to restore the default title." msgstr "Öntanımlı başlığı geri yüklemek için boş bırakın." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "İptal" @@ -35,10 +36,12 @@ msgid "OK" msgstr "Tamam" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Yapılandırma Hataları" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -48,12 +51,14 @@ msgstr "" "hataları yok sayın." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Yok Say" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Yapılandırmayı Yeniden Yükle" @@ -81,6 +86,10 @@ msgstr "Sola Doğru Böl" msgid "Split Right" msgstr "Sağa Doğru Böl" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,7 +97,7 @@ msgstr "Kopyala" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Yapıştır" @@ -118,7 +127,7 @@ msgstr "Sekme" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Yeni Sekme" @@ -146,29 +155,36 @@ msgid "Config" msgstr "Yapılandırma" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Yapılandırmayı Aç" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Uçbirim Denetçisi" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Ghostty Hakkında" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Çık" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Pano Erişimine İzin Ver" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -178,15 +194,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Reddet" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "İzin Ver" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -194,11 +225,11 @@ msgstr "" "Bir uygulama panoya yazmaya çalışıyor. Geçerli pano içeriği aşağıda " "gösterilmektedir." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Uyarı: Tehlikeli Olabilecek Yapıştırma" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -206,33 +237,6 @@ msgstr "" "Bu metni uçbirime yapıştırmak tehlikeli olabilir; çünkü bir komut " "yürütülebilecekmiş gibi duruyor." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Ana Menü" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Açık Sekmeleri Görüntüle" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "Yeni Bölme" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Ghostty’nin hata ayıklama amaçlı yapılmış bir sürümünü kullanıyorsunuz! " -"Başarım normale göre daha düşük olacaktır." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Yapılandırma yeniden yüklendi" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Ghostty Geliştiricileri" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Kapat" @@ -269,10 +273,37 @@ msgstr "Bu sekmedeki tüm uçbirim oturumları sonlandırılacaktır." msgid "The currently running process in this split will be terminated." msgstr "Bu bölmedeki şu anda çalışan süreç sonlandırılacaktır." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Panoya kopyalandı" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Ana Menü" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Açık Sekmeleri Görüntüle" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Yeni Bölme" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Ghostty’nin hata ayıklama amaçlı yapılmış bir sürümünü kullanıyorsunuz! " +"Başarım normale göre daha düşük olacaktır." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Yapılandırma yeniden yüklendi" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty Geliştiricileri" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Uçbirim Denetçisi" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index bde975fc4..2d01b3932 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-03-16 20:16+0200\n" "Last-Translator: Danylo Zalizchuk \n" "Language-Team: Ukrainian \n" @@ -27,7 +27,8 @@ msgid "Leave blank to restore the default title." msgstr "Залиште порожнім, щоб відновити назву за замовчуванням." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/CloseDialog.zig:44 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 msgid "Cancel" msgstr "Відмінити" @@ -36,10 +37,12 @@ msgid "OK" msgstr "ОК" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 msgid "Configuration Errors" msgstr "Помилки конфігурації" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." @@ -49,12 +52,14 @@ msgstr "" "ці помилки." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 msgid "Ignore" msgstr "Ігнорувати" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 msgid "Reload Configuration" msgstr "Перезавантажити конфігурацію" @@ -82,6 +87,10 @@ msgstr "Розділити панель ліворуч" msgid "Split Right" msgstr "Розділити панель праворуч" +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -89,7 +98,7 @@ msgstr "Скопіювати" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:11 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:11 -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:11 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 msgid "Paste" msgstr "Вставити" @@ -119,7 +128,7 @@ msgstr "Вкладка" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:248 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "Нова вкладка" @@ -147,29 +156,36 @@ msgid "Config" msgstr "Конфігурація" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Open Configuration" msgstr "Відкрити конфігурацію" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" msgstr "Інспектор терміналу" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:1003 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "Про Ghostty" -#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 msgid "Quit" msgstr "Завершити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" msgstr "Дозволити доступ до буфера обміну" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." @@ -179,15 +195,30 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 msgid "Deny" msgstr "Відхилити" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 msgid "Allow" msgstr "Дозволити" +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "" + #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." @@ -195,11 +226,11 @@ msgstr "" "Програма намагається записати дані до буфера обміну. Нижче показано поточний " "вміст буфера обміну." -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" msgstr "Увага: потенційно небезпечна вставка" -#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." @@ -207,32 +238,6 @@ msgstr "" "Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає " "так, ніби деякі команди можуть бути виконані." -#: src/apprt/gtk/Window.zig:201 -msgid "Main Menu" -msgstr "Головне меню" - -#: src/apprt/gtk/Window.zig:222 -msgid "View Open Tabs" -msgstr "Переглянути відкриті вкладки" - -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Ви використовуєте відладочну збірку Ghostty! Продуктивність буде погіршено." - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "Конфігурацію перезавантажено" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" -msgstr "Розробники Ghostty" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрити" @@ -270,10 +275,36 @@ msgid "The currently running process in this split will be terminated." msgstr "" "Поточний процес, що виконується в цій розділеній панелі, буде завершено." -#: src/apprt/gtk/Surface.zig:1243 +#: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "Скопійовано в буфер обміну" +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Головне меню" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Переглянути відкриті вкладки" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Ви використовуєте відладочну збірку Ghostty! Продуктивність буде погіршено." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Конфігурацію перезавантажено" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Розробники Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Інспектор терміналу" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 17a6dc921..2b5f9f3a1 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-06-28 17:01+0200\n" +"POT-Creation-Date: 2025-07-08 15:13-0500\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -125,7 +125,7 @@ msgstr "标签页" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:62 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 -#: src/apprt/gtk/Window.zig:263 +#: src/apprt/gtk/Window.zig:265 msgid "New Tab" msgstr "新建标签页" @@ -166,7 +166,7 @@ msgid "Terminal Inspector" msgstr "终端调试器" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 -#: src/apprt/gtk/Window.zig:1036 +#: src/apprt/gtk/Window.zig:1038 msgid "About Ghostty" msgstr "关于 Ghostty" @@ -229,35 +229,6 @@ msgid "" "commands may be executed." msgstr "将以下内容粘贴至终端内将可能执行有害命令。" -#: src/apprt/gtk/Window.zig:216 -msgid "Main Menu" -msgstr "主菜单" - -#: src/apprt/gtk/Window.zig:238 -msgid "View Open Tabs" -msgstr "浏览标签页" - -#: src/apprt/gtk/Window.zig:264 -msgid "New Split" -msgstr "新建分屏" - -#: src/apprt/gtk/Window.zig:327 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" - -#: src/apprt/gtk/Window.zig:773 -msgid "Reloaded the configuration" -msgstr "已重新加载配置" - -#: src/apprt/gtk/Window.zig:1017 -msgid "Ghostty Developers" -msgstr "Ghostty 开发团队" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty 终端调试器" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "关闭" @@ -297,3 +268,32 @@ msgstr "分屏内正在运行中的进程将被终止。" #: src/apprt/gtk/Surface.zig:1257 msgid "Copied to clipboard" msgstr "已复制至剪贴板" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "主菜单" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "浏览标签页" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "新建分屏" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "已重新加载配置" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Ghostty 开发团队" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty 终端调试器" From 1a3a03577bf7ecbeafda6d39ee37f23960167050 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 11:36:53 -0500 Subject: [PATCH 68/86] core: document which release added config entries For each config entry, add a comment specifying in which release it was first added. In some cases I note when certain aspects of each config entry were modified. --- src/config/Config.zig | 321 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 314 insertions(+), 7 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 8ca8d3154..29e70a289 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -126,6 +126,8 @@ pub const compatibility = std.StaticStringMap( /// /// Changing this configuration at runtime will only affect new terminals, i.e. /// new windows, tabs, etc. +/// +/// Available since: 1.0.0 @"font-family": RepeatableString = .{}, @"font-family-bold": RepeatableString = .{}, @"font-family-italic": RepeatableString = .{}, @@ -143,6 +145,8 @@ pub const compatibility = std.StaticStringMap( /// These are only valid if its corresponding font-family is also specified. If /// no font-family is specified, then the font-style is ignored unless you're /// disabling the font style. +/// +/// Available since: 1.0.0 @"font-style": FontStyle = .{ .default = {} }, @"font-style-bold": FontStyle = .{ .default = {} }, @"font-style-italic": FontStyle = .{ .default = {} }, @@ -178,6 +182,8 @@ pub const compatibility = std.StaticStringMap( /// explicitly disable it. You cannot partially disable `bold-italic`. /// /// By default, synthetic styles are enabled. +/// +/// Available since: 1.0.0 @"font-synthetic-style": FontSyntheticStyle = .{}, /// Apply a font feature. To enable multiple font features you can repeat @@ -202,6 +208,8 @@ pub const compatibility = std.StaticStringMap( /// [fontdrop.info](https://fontdrop.info). /// /// To generally disable most ligatures, use `-calt, -liga, -dlig`. +/// +/// Available since: 1.0.0 @"font-feature": RepeatableString = .{}, /// Font size in points. This value can be a non-integer and the nearest integer @@ -219,6 +227,8 @@ pub const compatibility = std.StaticStringMap( /// On Linux with GTK, font size is scaled according to both display-wide and /// text-specific scaling factors, which are often managed by your desktop /// environment (e.g. the GNOME display scale and large text settings). +/// +/// Available since: 1.0.0 @"font-size": f32 = switch (builtin.os.tag) { // On macOS we default a little bigger since this tends to look better. This // is purely subjective but this is easy to modify. @@ -244,6 +254,8 @@ pub const compatibility = std.StaticStringMap( /// /// Common axes are: `wght` (weight), `slnt` (slant), `ital` (italic), `opsz` /// (optical size), `wdth` (width), `GRAD` (gradient), etc. +/// +/// Available since: 1.0.0 @"font-variation": RepeatableFontVariation = .{}, @"font-variation-bold": RepeatableFontVariation = .{}, @"font-variation-italic": RepeatableFontVariation = .{}, @@ -265,10 +277,14 @@ pub const compatibility = std.StaticStringMap( /// /// Changing this configuration at runtime will only affect new terminals, /// i.e. new windows, tabs, etc. +/// +/// Available since: 1.0.0 @"font-codepoint-map": RepeatableCodepointMap = .{}, /// Draw fonts with a thicker stroke, if supported. /// This is currently only supported on macOS. +/// +/// Available since: 1.0.0 @"font-thicken": bool = false, /// Strength of thickening when `font-thicken` is enabled. @@ -279,6 +295,8 @@ pub const compatibility = std.StaticStringMap( /// Has no effect when `font-thicken` is set to `false`. /// /// This is currently only supported on macOS. +/// +/// Available since: 1.0.0 @"font-thicken-strength": u8 = 255, /// Locations to break font shaping into multiple runs. @@ -305,6 +323,7 @@ pub const compatibility = std.StaticStringMap( /// /// * `cursor` - Break runs under the cursor. /// +/// Available since: 1.2.0 @"font-shaping-break": FontShapingBreak = .{}, /// What color space to use when performing alpha blending. @@ -329,6 +348,8 @@ pub const compatibility = std.StaticStringMap( /// * `linear-corrected` - Same as `linear`, but with a correction step applied /// for text that makes it look nearly or completely identical to `native`, /// but without any of the darkening artifacts. +/// +/// Available since: 1.1.0 @"alpha-blending": AlphaBlending = if (builtin.os.tag == .macos) .native @@ -359,42 +380,64 @@ pub const compatibility = std.StaticStringMap( /// /// * Powerline glyphs will be adjusted along with the cell height so /// that things like status lines continue to look aligned. +/// +/// Available since: 1.0.0 @"adjust-cell-width": ?MetricModifier = null, @"adjust-cell-height": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the bottom of the cell to the text baseline. /// Increase to move baseline UP, decrease to move baseline DOWN. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-font-baseline": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the top of the cell to the top of the underline. /// Increase to move underline DOWN, decrease to move underline UP. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-underline-position": ?MetricModifier = null, /// Thickness in pixels of the underline. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-underline-thickness": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the top of the cell to the top of the strikethrough. /// Increase to move strikethrough DOWN, decrease to move strikethrough UP. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-strikethrough-position": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of the strikethrough. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-strikethrough-thickness": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the top of the cell to the top of the overline. /// Increase to move overline DOWN, decrease to move overline UP. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-overline-position": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of the overline. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-overline-thickness": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of the bar cursor and outlined rect cursor. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-cursor-thickness": ?MetricModifier = null, /// Height in pixels or percentage adjustment of the cursor. Currently applies to all cursor types: /// bar, rect, and outlined rect. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-cursor-height": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of box drawing characters. /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available since: 1.0.0 @"adjust-box-thickness": ?MetricModifier = null, /// Height in pixels or percentage adjustment of maximum height for nerd font icons. /// @@ -407,6 +450,8 @@ pub const compatibility = std.StaticStringMap( /// roughly the same height as capital letters. /// /// See the notes about adjustments in `adjust-cell-width`. +/// +/// Available in: 1.2.0 @"adjust-icon-height": ?MetricModifier = null, /// The method to use for calculating the cell width of a grapheme cluster. @@ -434,6 +479,8 @@ pub const compatibility = std.StaticStringMap( /// /// This configuration can be changed at runtime but will not affect existing /// terminals. Only new terminals will use the new configuration. +/// +/// Available since: 1.0.0 @"grapheme-width-method": GraphemeWidthMethod = .unicode, /// FreeType load flags to enable. The format of this is a list of flags to @@ -460,6 +507,8 @@ pub const compatibility = std.StaticStringMap( /// * `autohint` - Enable the freetype auto-hinter. Enabled by default. /// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` +/// +/// Available since: 1.0.0 @"freetype-load-flags": FreetypeLoadFlags = .{}, /// A theme to use. This can be a built-in theme name, a custom theme @@ -516,14 +565,19 @@ pub const compatibility = std.StaticStringMap( /// /// - macOS: titlebar tabs style is not updated when switching themes. /// +/// Available since: 1.0.0 theme: ?Theme = null, /// Background color for the window. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.0.0 background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Foreground color for the window. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.0.0 foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Background image for the terminal. @@ -539,6 +593,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// For sufficiently large images, this could lead to a large increase in /// memory usage (specifically VRAM usage). A future Ghostty improvement /// will resolve this by sharing image textures across terminals. +/// +/// Available since: 1.2.0 @"background-image": ?Path = null, /// Background image opacity. @@ -558,6 +614,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// configured `background-opacity` is `0.5` and `background-image-opacity` /// is set to `1.5`, then the final opacity of the background image will be /// `0.5 * 1.5 = 0.75`. +/// +/// Available since: 1.2.0 @"background-image-opacity": f32 = 1.0, /// Background image position. @@ -574,6 +632,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// * `bottom-right` /// /// The default value is `center`. +/// +/// Available since: 1.2.0 @"background-image-position": BackgroundImagePosition = .center, /// Background image fit. @@ -602,6 +662,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Don't scale the background image. /// /// The default value is `contain`. +/// +/// Available since: 1.2.0 @"background-image-fit": BackgroundImageFit = .contain, /// Whether to repeat the background image or not. @@ -611,6 +673,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// fill the terminal area. /// /// The default value is `false`. +/// +/// Available since: 1.2.0 @"background-image-repeat": bool = false, /// The foreground and background color for selection. If this is not set, then @@ -620,6 +684,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. +/// +/// Available since: 1.0.0 @"selection-foreground": ?TerminalColor = null, @"selection-background": ?TerminalColor = null, @@ -636,6 +702,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// /// If this is `false`, then the selection can still be manually /// cleared by clicking once or by pressing `escape`. +/// +/// Available since: 1.2.0 @"selection-clear-on-typing": bool = true, /// The minimum contrast ratio between the foreground and background colors. @@ -649,6 +717,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// that text will become black or white. /// /// This value does not apply to Emoji or images. +/// +/// Available since: 1.0.0 @"minimum-contrast": f64 = 1, /// Color palette for the 256 color form that many terminal applications use. @@ -662,6 +732,8 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// /// For definitions on the color indices and what they canonically map to, /// [see this cheat sheet](https://www.ditig.com/256-colors-cheat-sheet). +/// +/// Available since: 1.0.0 palette: Palette = .{}, /// The color of the cursor. If this is not set, a default will be chosen. @@ -678,6 +750,7 @@ palette: Palette = .{}, /// * `cell-background` - Match the cell background color. /// (Available since version 1.2.0) /// +/// Available since: 1.0.0 @"cursor-color": ?TerminalColor = null, /// The opacity level (opposite of transparency) of the cursor. A value of 1 @@ -703,6 +776,7 @@ palette: Palette = .{}, /// * `underline` /// * `block_hollow` /// +/// Available since: 1.0.0 @"cursor-style": terminal.CursorStyle = .block, /// Sets the default blinking state of the cursor. This is just the default @@ -723,6 +797,7 @@ palette: Palette = .{}, /// * `true` /// * `false` /// +/// Available since: 1.0.0 @"cursor-style-blink": ?bool = null, /// The color of the text under the cursor. If this is not set, a default will @@ -731,6 +806,8 @@ palette: Palette = .{}, /// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. +/// +/// Available since: 1.0.0 @"cursor-text": ?TerminalColor = null, /// Enables the ability to move the cursor at prompts by using `alt+click` on @@ -746,12 +823,16 @@ palette: Palette = .{}, /// behavior around edge cases are to be expected. This is unfortunately how /// this feature is implemented across terminals because there isn't any other /// way to implement it. +/// +/// Available since: 1.0.0 @"cursor-click-to-move": bool = true, /// Hide the mouse immediately when typing. The mouse becomes visible again /// when the mouse is used (button, movement, etc.). Platform-specific behavior /// may dictate other scenarios where the mouse is shown. For example on macOS, /// the mouse is shown again when a new window, tab, or split is created. +/// +/// Available since: 1.0.0 @"mouse-hide-while-typing": bool = false, /// Determines whether running programs can detect the shift key pressed with a @@ -780,6 +861,7 @@ palette: Palette = .{}, /// * `always` /// * `never` /// +/// Available since: 1.0.0 @"mouse-shift-capture": MouseShiftCapture = .false, /// Multiplier for scrolling distance with the mouse wheel. Any value less @@ -787,6 +869,8 @@ palette: Palette = .{}, /// value. /// /// A value of "3" (default) scrolls 3 lines per tick. +/// +/// Available since: 1.2.0 @"mouse-scroll-multiplier": f64 = 3.0, /// The opacity level (opposite of transparency) of the background. A value of @@ -798,6 +882,8 @@ palette: Palette = .{}, /// widgets to show through which isn't generally desirable. /// /// On macOS, changing this configuration requires restarting Ghostty completely. +/// +/// Available since: 1.0.0 @"background-opacity": f64 = 1.0, /// Whether to blur the background when `background-opacity` is less than 1. @@ -831,6 +917,8 @@ palette: Palette = .{}, /// need to set environment-specific settings and/or install third-party plugins /// in order to support background blur, as there isn't a unified interface for /// doing so. +/// +/// Available since: 1.0.0 @"background-blur": BackgroundBlur = .false, /// The opacity level (opposite of transparency) of an unfocused split. @@ -842,6 +930,8 @@ palette: Palette = .{}, /// is 0.15. This value still looks weird but you can at least see what's going /// on. A value outside of the range 0.15 to 1 will be clamped to the nearest /// valid value. +/// +/// Available since: 1.0.0 @"unfocused-split-opacity": f64 = 0.7, /// The color to dim the unfocused split. Unfocused splits are dimmed by @@ -851,10 +941,14 @@ palette: Palette = .{}, /// This will default to the background color. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.0.0 @"unfocused-split-fill": ?Color = null, /// The color of the split divider. If this is not set, a default will be chosen. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.1.0 @"split-divider-color": ?Color = null, /// The command to run, usually a shell. If this is not an absolute path, it'll @@ -869,14 +963,14 @@ palette: Palette = .{}, /// arguments are provided, the command will be executed using `/bin/sh -c` /// to offload shell argument expansion. /// -/// To avoid shell expansion altogether, prefix the command with `direct:`, -/// e.g. `direct:nvim foo`. This will avoid the roundtrip to `/bin/sh` but will -/// also not support any shell parsing such as arguments with spaces, filepaths -/// with `~`, globs, etc. +/// To avoid shell expansion altogether, prefix the command with `direct:`, e.g. +/// `direct:nvim foo`. This will avoid the roundtrip to `/bin/sh` but will also +/// not support any shell parsing such as arguments with spaces, filepaths with +/// `~`, globs, etc. (Available since: 1.2.0) /// -/// You can also explicitly prefix the command with `shell:` to always -/// wrap the command in a shell. This can be used to ensure our heuristics -/// to choose the right mode are not used in case they are wrong. +/// You can also explicitly prefix the command with `shell:` to always wrap the +/// command in a shell. This can be used to ensure our heuristics to choose the +/// right mode are not used in case they are wrong. (Available since: 1.2.0) /// /// This command will be used for all new terminal surfaces, i.e. new windows, /// tabs, etc. If you want to run a command only for the first terminal surface @@ -886,6 +980,8 @@ palette: Palette = .{}, /// arguments. For example, `ghostty -e fish --with --custom --args`. /// This flag sets the `initial-command` configuration, see that for more /// information. +/// +/// Available since: 1.0.0 command: ?Command = null, /// This is the same as "command", but only applies to the first terminal @@ -923,6 +1019,7 @@ command: ?Command = null, /// name your binary appropriately or source the shell integration script /// manually. /// +/// Available since: 1.0.0 @"initial-command": ?Command = null, /// Extra environment variables to pass to commands launched in a terminal @@ -959,6 +1056,8 @@ command: ?Command = null, /// These environment variables _will not_ be passed to commands run by Ghostty /// for other purposes, like `open` or `xdg-open` used to open URLs in your /// browser. +/// +/// Available since: 1.2.0 env: RepeatableStringMap = .{}, /// Data to send as input to the command on startup. @@ -1000,6 +1099,8 @@ env: RepeatableStringMap = .{}, /// /// Changing this configuration at runtime will only affect new /// terminals. +/// +/// Available since: 1.2.0 input: RepeatableReadableIO = .{}, /// If true, keep the terminal open after the command exits. Normally, the @@ -1008,6 +1109,8 @@ input: RepeatableReadableIO = .{}, /// received. /// /// This is primarily useful for scripts or debugging. +/// +/// Available since: 1.0.0 @"wait-after-command": bool = false, /// The number of milliseconds of runtime below which we consider a process exit @@ -1017,6 +1120,8 @@ input: RepeatableReadableIO = .{}, /// On Linux, this must be paired with a non-zero exit code. On macOS, we allow /// any exit code because of the way shell processes are launched via the login /// command. +/// +/// Available since: 1.0.0 @"abnormal-command-exit-runtime": u32 = 250, /// The size of the scrollback buffer in bytes. This also includes the active @@ -1038,6 +1143,8 @@ input: RepeatableReadableIO = .{}, /// This is a future planned feature. /// /// This can be changed at runtime but will only affect new terminal surfaces. +/// +/// Available since: 1.0.0 @"scrollback-limit": usize = 10_000_000, // 10MB /// Match a regular expression against the terminal text and associate clicking @@ -1052,6 +1159,8 @@ input: RepeatableReadableIO = .{}, /// exists. This can be disabled using `link-url`. /// /// TODO: This can't currently be set! +/// +/// Available since: 1.0.0 link: RepeatableLink = .{}, /// Enable URL matching. URLs are matched on hover with control (Linux) or @@ -1060,6 +1169,8 @@ link: RepeatableLink = .{}, /// /// The URL matcher is always lowest priority of any configured links (see /// `link`). If you want to customize URL matching, use `link` and disable this. +/// +/// Available since: 1.0.0 @"link-url": bool = true, /// Show link previews for a matched URL. @@ -1068,11 +1179,15 @@ link: RepeatableLink = .{}, /// previews are never shown. When set to "osc8", link previews are only shown /// for hyperlinks created with the OSC 8 sequence (in this case, the link text /// can differ from the link destination). +/// +/// Available since: 1.2.0 @"link-previews": LinkPreviews = .true, /// Whether to start the window in a maximized state. This setting applies /// to new windows and does not apply to tabs, splits, etc. However, this setting /// will apply to all new windows, not just the first one. +/// +/// Available since: 1.1.0 maximize: bool = false, /// Start new windows in fullscreen. This setting applies to new windows and @@ -1082,6 +1197,8 @@ maximize: bool = false, /// On macOS, this setting does not work if window-decoration is set to /// "false", because native fullscreen on macOS requires window decorations /// to be set. +/// +/// Available since: 1.0.0 fullscreen: bool = false, /// The title Ghostty will use for the window. This will force the title of the @@ -1098,6 +1215,8 @@ fullscreen: bool = false, /// sequence will be honored but previous changes will not retroactively /// be set. This latter case may require you to restart programs such as Neovim /// to get the new title. +/// +/// Available since: 1.0.0 title: ?[:0]const u8 = null, /// The setting that will change the application class value. @@ -1120,6 +1239,8 @@ title: ?[:0]const u8 = null, /// The default is `com.mitchellh.ghostty`. /// /// This only affects GTK builds. +/// +/// Available since: 1.0.0 class: ?[:0]const u8 = null, /// This controls the instance name field of the `WM_CLASS` X11 property when @@ -1128,6 +1249,8 @@ class: ?[:0]const u8 = null, /// The default is `ghostty`. /// /// This only affects GTK builds. +/// +/// Available since: 1.0.0 @"x11-instance-name": ?[:0]const u8 = null, /// The directory to change to after starting the command. @@ -1149,6 +1272,8 @@ class: ?[:0]const u8 = null, /// * `home` - The home directory of the executing user. /// /// * `inherit` - The working directory of the launching process. +/// +/// Available since: 1.0.0 @"working-directory": ?[]const u8 = null, /// Key bindings. The format is `trigger=action`. Duplicate triggers will @@ -1297,6 +1422,8 @@ class: ?[:0]const u8 = null, /// applies to actions that are surface-specific. For actions that /// are already global (e.g. `quit`), this prefix has no effect. /// +/// Available since: 1.0.0 +/// /// * `global:` - Make the keybind global. By default, keybinds only work /// within Ghostty and under the right conditions (application focused, /// sometimes terminal focused, etc.). If you want a keybind to work @@ -1305,6 +1432,9 @@ class: ?[:0]const u8 = null, /// work in all environments; see the additional notes below for more /// information. /// +/// Available since: 1.0.0 (on macOS) +/// Available since: 1.2.0 (on GTK) +/// /// * `unconsumed:` - Do not consume the input. By default, a keybind /// will consume the input, meaning that the associated encoding (if /// any) will not be sent to the running program in the terminal. If @@ -1315,6 +1445,8 @@ class: ?[:0]const u8 = null, /// Since they are not associated with a specific terminal surface, /// they're never encoded. /// +/// Available since: 1.0.0 +/// /// * `performable:` - Only consume the input if the action is able to be /// performed. For example, the `copy_to_clipboard` action will only /// consume the input if there is a selection to copy. If there is no @@ -1330,6 +1462,8 @@ class: ?[:0]const u8 = null, /// Performable keybinds will still work, they just won't appear as /// a shortcut label in the menu. /// +/// Available since: 1.1.0 +/// /// Keybind triggers are not unique per prefix combination. For example, /// `ctrl+a` and `global:ctrl+a` are not two separate keybinds. The keybind /// set later will overwrite the keybind set earlier. In this case, the @@ -1366,6 +1500,8 @@ class: ?[:0]const u8 = null, /// /// - Notably, global shortcuts have not been implemented on wlroots-based /// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). +/// +/// Available since: 1.0.0 keybind: Keybinds = .{}, /// Horizontal window padding. This applies padding between the terminal cells @@ -1385,6 +1521,8 @@ keybind: Keybinds = .{}, /// left padding to 2 and the right padding to 4. If you want to set both /// paddings to the same value, you can use a single value. For example, /// `window-padding-x = 2` will set both paddings to 2. +/// +/// Available since: 1.0.0 @"window-padding-x": WindowPadding = .{ .top_left = 2, .bottom_right = 2 }, /// Vertical window padding. This applies padding between the terminal cells and @@ -1404,6 +1542,8 @@ keybind: Keybinds = .{}, /// top padding to 2 and the bottom padding to 4. If you want to set both /// paddings to the same value, you can use a single value. For example, /// `window-padding-y = 2` will set both paddings to 2. +/// +/// Available since: 1.0.0 @"window-padding-y": WindowPadding = .{ .top_left = 2, .bottom_right = 2 }, /// The viewport dimensions are usually not perfectly divisible by the cell @@ -1418,6 +1558,8 @@ keybind: Keybinds = .{}, /// apply. The other padding is applied first and may affect how many grid cells /// actually exist, and this is applied last in order to balance the padding /// given a certain viewport size and grid cell size. +/// +/// Available since: 1.0.0 @"window-padding-balance": bool = false, /// The color of the padding area of the window. Valid values are: @@ -1440,6 +1582,7 @@ keybind: Keybinds = .{}, /// * The nearest row contains a perfect fit powerline character. These /// don't look good extended. /// +/// Available since: 1.0.0 @"window-padding-color": WindowPaddingColor = .background, /// Synchronize rendering with the screen refresh rate. If true, this will @@ -1455,17 +1598,23 @@ keybind: Keybinds = .{}, /// Changing this value at runtime will only affect new terminals. /// /// This setting is only supported currently on macOS. +/// +/// Available since: 1.0.0 @"window-vsync": bool = true, /// If true, new windows and tabs will inherit the working directory of the /// previously focused window. If no window was previously focused, the default /// working directory will be used (the `working-directory` option). +/// +/// Available since: 1.0.0 @"window-inherit-working-directory": bool = true, /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default font size /// will be used. If this is false, the default font size specified in the /// configuration `font-size` will be used. +/// +/// Available since: 1.0.0 @"window-inherit-font-size": bool = true, /// Configure a preference for window decorations. This setting specifies @@ -1486,6 +1635,8 @@ keybind: Keybinds = .{}, /// /// * `client` - Prefer client-side decorations. /// +/// Available since: 1.1.0 +/// /// * `server` - Prefer server-side decorations. This is only relevant /// on Linux with GTK, either on X11, or Wayland on a compositor that /// supports the `org_kde_kwin_server_decoration` protocol (e.g. KDE Plasma, @@ -1494,6 +1645,8 @@ keybind: Keybinds = .{}, /// If `server` is set but the environment doesn't support server-side /// decorations, client-side decorations will be used instead. /// +/// Available since: 1.1.0 +/// /// The default value is `auto`. /// /// For the sake of backwards compatibility and convenience, this setting also @@ -1508,6 +1661,8 @@ keybind: Keybinds = .{}, /// /// macOS: To hide the titlebar without removing the native window borders /// or rounded corners, use `macos-titlebar-style = hidden` instead. +/// +/// Available since: 1.0.0 @"window-decoration": WindowDecoration = .auto, /// The font that will be used for the application's window and tab titles. @@ -1516,6 +1671,9 @@ keybind: Keybinds = .{}, /// /// Note: any font available on the system may be used, this font is not /// required to be a fixed-width font. +/// +/// Available since: 1.0.0 (macOS) +/// Available since: 1.1.0 (GTK) @"window-title-font-family": ?[:0]const u8 = null, /// The text that will be displayed in the subtitle of the window. Valid values: @@ -1525,6 +1683,8 @@ keybind: Keybinds = .{}, /// surface. /// /// This feature is only supported on GTK. +/// +/// Available since: 1.1.0 @"window-subtitle": WindowSubtitle = .false, /// The theme to use for the windows. Valid values: @@ -1545,6 +1705,8 @@ keybind: Keybinds = .{}, /// non-terminal windows within Ghostty. /// /// This is currently only supported on macOS and Linux. +/// +/// Available since: 1.0.0 @"window-theme": WindowTheme = .auto, /// The color space to use when interpreting terminal colors. "Terminal colors" @@ -1557,6 +1719,8 @@ keybind: Keybinds = .{}, /// * `display-p3` - Interpret colors in the Display P3 color space. /// /// This setting is currently only supported on macOS. +/// +/// Available since: 1.0.0 @"window-colorspace": WindowColorspace = .srgb, /// The initial window size. This size is in terminal grid cells by default. @@ -1586,6 +1750,8 @@ keybind: Keybinds = .{}, /// `window-decoration`), then this will work as expected. /// /// Windows smaller than 10 wide by 4 high are not allowed. +/// +/// Available since: 1.0.0 @"window-height": u32 = 0, @"window-width": u32 = 0, @@ -1612,6 +1778,8 @@ keybind: Keybinds = .{}, /// Note: this is only supported on macOS. The GTK runtime does not support /// setting the window position, as windows are only allowed position /// themselves in X11 and not Wayland. +/// +/// Available since: 1.0.0 @"window-position-x": ?i16 = null, @"window-position-y": ?i16 = null, @@ -1645,11 +1813,15 @@ keybind: Keybinds = .{}, /// The default value is `default`. /// /// This is currently only supported on macOS. This has no effect on Linux. +/// +/// Available since: 1.0.0 @"window-save-state": WindowSaveState = .default, /// Resize the window in discrete increments of the focused surface's cell size. /// If this is disabled, surfaces are resized in pixel increments. Currently /// only supported on macOS. +/// +/// Available since: 1.0.0 @"window-step-resize": bool = false, /// The position where new tabs are created. Valid values: @@ -1658,6 +1830,8 @@ keybind: Keybinds = .{}, /// or at the end if there are no focused tabs. /// /// * `end` - Insert the new tab at the end of the tab list. +/// +/// Available since: 1.0.0 @"window-new-tab-position": WindowNewTabPosition = .current, /// Whether to show the tab bar. @@ -1668,6 +1842,8 @@ keybind: Keybinds = .{}, /// /// Always display the tab bar, even when there's only one tab. /// +/// Available since: 1.2.0 +/// /// - `auto` *(default)* /// /// Automatically show and hide the tab bar. The tab bar is only @@ -1679,6 +1855,8 @@ keybind: Keybinds = .{}, /// overview or by keybind actions. /// /// Currently only supported on Linux (GTK). +/// +/// Available since: 1.0.0 @"window-show-tab-bar": WindowShowTabBar = .auto, /// Background color for the window titlebar. This only takes effect if @@ -1686,6 +1864,8 @@ keybind: Keybinds = .{}, /// runtime. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.0.0 @"window-titlebar-background": ?Color = null, /// Foreground color for the window titlebar. This only takes effect if @@ -1693,6 +1873,8 @@ keybind: Keybinds = .{}, /// runtime. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.0.0 @"window-titlebar-foreground": ?Color = null, /// This controls when resize overlays are shown. Resize overlays are a @@ -1706,6 +1888,8 @@ keybind: Keybinds = .{}, /// subsequently resized. /// /// The default is `after-first`. +/// +/// Available since: 1.0.0 @"resize-overlay": ResizeOverlay = .@"after-first", /// If resize overlays are enabled, this controls the position of the overlay. @@ -1720,6 +1904,8 @@ keybind: Keybinds = .{}, /// * `bottom-right` /// /// The default is `center`. +/// +/// Available since: 1.0.0 @"resize-overlay-position": ResizeOverlayPosition = .center, /// If resize overlays are enabled, this controls how long the overlay is @@ -1752,6 +1938,8 @@ keybind: Keybinds = .{}, /// /// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any /// value larger than this will be clamped to the maximum value. +/// +/// Available since 1.0.0 @"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms }, /// If true, when there are multiple split panes, the mouse selects the pane @@ -1797,6 +1985,8 @@ keybind: Keybinds = .{}, /// Warning: This can expose sensitive information at best and enable /// arbitrary code execution at worst (with a maliciously crafted title /// and a minor amount of user interaction). +/// +/// Available since: 1.0.1 @"title-report": bool = false, /// The total amount of bytes that can be used for image data (e.g. the Kitty @@ -1806,6 +1996,8 @@ keybind: Keybinds = .{}, /// /// This value is separate for primary and alternate screens so the effective /// limit per surface is double. +/// +/// Available since: 1.0.0 @"image-storage-limit": u32 = 320 * 1000 * 1000, /// Whether to automatically copy selected text to the clipboard. `true` @@ -1819,6 +2011,8 @@ keybind: Keybinds = .{}, /// paste is always enabled even if this is `false`. /// /// The default value is true on Linux and macOS. +/// +/// Available since: 1.0.0 @"copy-on-select": CopyOnSelect = switch (builtin.os.tag) { .linux => .true, .macos => .true, @@ -1829,6 +2023,8 @@ keybind: Keybinds = .{}, /// (double, triple, etc.) or an entirely new single click. A value of zero will /// use a platform-specific default. The default on macOS is determined by the /// OS settings. On every other platform it is 500ms. +/// +/// Available since: 1.0.0 @"click-repeat-interval": u32 = 0, /// Additional configuration files to read. This configuration can be repeated @@ -1858,6 +2054,8 @@ keybind: Keybinds = .{}, /// If "foo" contains `a = 2`, the final value of `a` will be 2, because /// `foo` is loaded after the configuration file that configures the /// nested `config-file` value. +/// +/// Available since: 1.0.0 @"config-file": RepeatablePath = .{}, /// When this is true, the default configuration file paths will be loaded. @@ -1871,6 +2069,8 @@ keybind: Keybinds = .{}, /// This is a CLI-only configuration. Setting this in a configuration file /// will have no effect. It is not an error, but it will not do anything. /// This configuration can only be set via CLI arguments. +/// +/// Available since: 1.0.0 @"config-default-files": bool = true, /// Confirms that a surface should be closed before closing it. @@ -1879,6 +2079,8 @@ keybind: Keybinds = .{}, /// any confirmation. This can also be set to `always`, which will always /// confirm closing a surface, even if shell integration says a process isn't /// running. +/// +/// Available since: 1.0.0 @"confirm-close-surface": ConfirmCloseSurface = .true, /// Whether or not to quit after the last surface is closed. @@ -1890,6 +2092,8 @@ keybind: Keybinds = .{}, /// On Linux, if this is `true`, Ghostty can delay quitting fully until a /// configurable amount of time has passed after the last window is closed. /// See the documentation of `quit-after-last-window-closed-delay`. +/// +/// Available since: 1.0.0 @"quit-after-last-window-closed": bool = builtin.os.tag == .linux, /// Controls how long Ghostty will stay running after the last open surface has @@ -1931,6 +2135,8 @@ keybind: Keybinds = .{}, /// `quit-after-last-window-closed` is `true`. /// /// Only implemented on Linux. +/// +/// Available since: 1.0.0 @"quit-after-last-window-closed-delay": ?Duration = null, /// This controls whether an initial window is created when Ghostty @@ -1938,6 +2144,8 @@ keybind: Keybinds = .{}, /// `quit-after-last-window-closed-delay` is set, setting `initial-window` to /// `false` will mean that Ghostty will quit after the configured delay if no /// window is ever created. Only implemented on Linux and macOS. +/// +/// Available since: 1.0.0 @"initial-window": bool = true, /// The duration that undo operations remain available. After this @@ -1984,6 +2192,8 @@ keybind: Keybinds = .{}, /// This configuration is only supported on macOS. Linux doesn't /// support undo operations at all so this configuration has no /// effect. +/// +/// Available since: 1.2.0 @"undo-timeout": Duration = .{ .duration = 5 * std.time.ns_per_s }, /// The position of the "quick" terminal window. To learn more about the @@ -2003,6 +2213,8 @@ keybind: Keybinds = .{}, /// /// Note: There is no default keybind for toggling the quick terminal. /// To enable this feature, bind the `toggle_quick_terminal` action to a key. +/// +/// Available since: 1.0.0 @"quick-terminal-position": QuickTerminalPosition = .top, /// The size of the quick terminal. @@ -2024,6 +2236,8 @@ keybind: Keybinds = .{}, /// from the first by a comma (`,`). Percentage and pixel sizes can be mixed /// together: for instance, a size of `50%,500px` for a top-positioned quick /// terminal would be half a screen tall, and 500 pixels wide. +/// +/// Available since: 1.0.0 @"quick-terminal-size": QuickTerminalSize = .{}, /// The screen where the quick terminal should show up. @@ -2046,6 +2260,8 @@ keybind: Keybinds = .{}, /// by the operating system. /// /// Only implemented on macOS. +/// +/// Available since: 1.0.0 @"quick-terminal-screen": QuickTerminalScreen = .main, /// Duration (in seconds) of the quick terminal enter and exit animation. @@ -2053,6 +2269,8 @@ keybind: Keybinds = .{}, /// runtime. /// /// Only implemented on macOS. +/// +/// Available since: 1.0.0 @"quick-terminal-animation-duration": f64 = 0.2, /// Automatically hide the quick terminal when focus shifts to another window. @@ -2063,6 +2281,8 @@ keybind: Keybinds = .{}, /// accessible than on macOS, meaning that it is more preferable to keep the /// quick terminal open until the user has completed their task. /// This default may change in the future. +/// +/// Available since: 1.0.0 @"quick-terminal-autohide": bool = switch (builtin.os.tag) { .linux => false, .macos => true, @@ -2087,6 +2307,8 @@ keybind: Keybinds = .{}, /// /// Only implemented on macOS. /// On Linux the behavior is always equivalent to `move`. +/// +/// Available since: 1.1.0 @"quick-terminal-space-behavior": QuickTerminalSpaceBehavior = .move, /// Determines under which circumstances that the quick terminal should receive @@ -2115,6 +2337,8 @@ keybind: Keybinds = .{}, /// /// Only has an effect on Linux Wayland. /// On macOS the behavior is always equivalent to `on-demand`. +/// +/// Available since: 1.2.0 @"quick-terminal-keyboard-interactivity": QuickTerminalKeyboardInteractivity = .@"on-demand", /// Whether to enable shell integration auto-injection or not. Shell integration @@ -2141,6 +2365,8 @@ keybind: Keybinds = .{}, /// * `bash`, `elvish`, `fish`, `zsh` - Use this specific shell injection scheme. /// /// The default value is `detect`. +/// +/// Available since: 1.0.0 @"shell-integration": ShellIntegration = .detect, /// Shell integration features to enable. These require our shell integration @@ -2160,6 +2386,8 @@ keybind: Keybinds = .{}, /// * `title` - Set the window title via shell integration. /// /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` +/// +/// Available since: 1.0.0 @"shell-integration-features": ShellIntegrationFeatures = .{}, /// Custom entries into the command palette. @@ -2182,6 +2410,8 @@ keybind: Keybinds = .{}, /// ```ini /// command-palette-entry = /// ``` +/// +/// Available since: 1.2.0 @"command-palette-entry": RepeatableCommand = .{}, /// Sets the reporting format for OSC sequences that request color information. @@ -2201,6 +2431,8 @@ keybind: Keybinds = .{}, /// * `16-bit` - Color components are returned scaled, e.g. `rrrr/gggg/bbbb` /// /// The default value is `16-bit`. +/// +/// Available since: 1.0.0 @"osc-color-report-format": OSCColorReportFormat = .@"16-bit", /// If true, allows the "KAM" mode (ANSI mode 2) to be used within @@ -2209,6 +2441,8 @@ keybind: Keybinds = .{}, /// to be enabled. This will not be documented further because /// if you know you need KAM, you know. If you don't know if you /// need KAM, you don't need it. +/// +/// Available since: 1.0.0 @"vt-kam-allowed": bool = false, /// Custom shaders to run after the default shaders. This is a file path @@ -2282,6 +2516,8 @@ keybind: Keybinds = .{}, /// will be run in the order they are specified. /// /// This can be changed at runtime and will affect all open terminals. +/// +/// Available since: 1.0.0 @"custom-shader": RepeatablePath = .{}, /// If `true` (default), the focused terminal surface will run an animation @@ -2300,6 +2536,8 @@ keybind: Keybinds = .{}, /// depending on the shader and your terminal usage. /// /// This can be changed at runtime and will affect all open terminals. +/// +/// Available since: 1.0.0 @"custom-shader-animation": CustomShaderAnimation = .true, /// Bell features to enable if bell support is available in your runtime. Not @@ -2346,6 +2584,8 @@ keybind: Keybinds = .{}, /// Only implemented on macOS. /// /// Example: `audio`, `no-audio`, `system`, `no-system` +/// +/// Available since: 1.2.0 @"bell-features": BellFeatures = .{}, /// If `audio` is an enabled bell feature, this is a path to an audio file. If @@ -2353,12 +2593,16 @@ keybind: Keybinds = .{}, /// configuration file that it is referenced from, or from the current working /// directory if this is used as a CLI flag. The path may be prefixed with `~/` /// to reference the user's home directory. (GTK only) +/// +/// Available since: 1.2.0 @"bell-audio-path": ?Path = null, /// If `audio` is an enabled bell feature, this is the volume to play the audio /// file at (relative to the system volume). This is a floating point number /// ranging from 0.0 (silence) to 1.0 (as loud as possible). The default is 0.5. /// (GTK only) +/// +/// Available since: 1.2.0 @"bell-audio-volume": f64 = 0.5, /// Control the in-app notifications that Ghostty shows. @@ -2384,6 +2628,8 @@ keybind: Keybinds = .{}, /// enable all notifications. /// /// This configuration only applies to GTK. +/// +/// Available since: 1.1.0 @"app-notifications": AppNotifications = .{}, /// If anything other than false, fullscreen mode on macOS will not use the @@ -2415,6 +2661,8 @@ keybind: Keybinds = .{}, /// Changing this option at runtime works, but will only apply to the next /// time the window is made fullscreen. If a window is already fullscreen, /// it will retain the previous setting until fullscreen is exited. +/// +/// Available since: 1.0.0 @"macos-non-native-fullscreen": NonNativeFullscreen = .false, /// Whether the window buttons in the macOS titlebar are visible. The window @@ -2434,6 +2682,8 @@ keybind: Keybinds = .{}, /// The default value is `visible`. /// /// Changing this option at runtime only applies to new windows. +/// +/// Available since: 1.2.0 @"macos-window-buttons": MacWindowButtons = .visible, /// The style of the macOS titlebar. Available values are: "native", @@ -2476,6 +2726,8 @@ keybind: Keybinds = .{}, /// most cases. /// /// Changing this option at runtime only applies to new windows. +/// +/// Available since: 1.0.0 @"macos-titlebar-style": MacTitlebarStyle = .transparent, /// Whether the proxy icon in the macOS titlebar is visible. The proxy icon @@ -2497,6 +2749,8 @@ keybind: Keybinds = .{}, /// Therefore, to make this work after changing the setting, you must /// usually `cd` to a different directory, open a different file in an /// editor, etc. +/// +/// Available since: 1.0.0 @"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible, /// macOS doesn't have a distinct "alt" key and instead has the "option" @@ -2531,11 +2785,15 @@ keybind: Keybinds = .{}, /// /// The values `left` or `right` enable this for the left or right *Option* /// key, respectively. +/// +/// Available since: 1.0.0 @"macos-option-as-alt": ?OptionAsAlt = null, /// Whether to enable the macOS window shadow. The default value is true. /// With some window managers and window transparency settings, you may /// find false more visually appealing. +/// +/// Available since: 1.0.0 @"macos-window-shadow": bool = true, /// If true, the macOS icon in the dock and app switcher will be hidden. This is @@ -2556,6 +2814,8 @@ keybind: Keybinds = .{}, /// /// Note: When the macOS application is hidden, keyboard layout changes /// will no longer be automatic. This is a limitation of macOS. +/// +/// Available since: 1.2.0 @"macos-hidden": MacHidden = .never, /// If true, Ghostty on macOS will automatically enable the "Secure Input" @@ -2574,6 +2834,8 @@ keybind: Keybinds = .{}, /// with legitimate accessibility software (or software that uses the /// accessibility APIs), since secure input prevents any application from /// reading keyboard events. +/// +/// Available since: 1.0.0 @"macos-auto-secure-input": bool = true, /// If true, Ghostty will show a graphical indication when secure input is @@ -2584,6 +2846,8 @@ keybind: Keybinds = .{}, /// or it is manually (and typically temporarily) enabled. However, if you /// always have secure input enabled, the indication can be distracting and /// you may want to disable it. +/// +/// Available since: 1.0.0 @"macos-secure-input-indication": bool = true, /// Customize the macOS app icon. @@ -2618,6 +2882,7 @@ keybind: Keybinds = .{}, /// separate framework and cannot be customized without significant /// effort. /// +/// Available since: 1.0.0 @"macos-icon": MacAppIcon = .official, /// The material to use for the frame of the macOS app icon. @@ -2632,6 +2897,7 @@ keybind: Keybinds = .{}, /// Note: This configuration is required when `macos-icon` is set to /// `custom-style`. /// +/// Available since: 1.0.0 @"macos-icon-frame": MacAppIconFrame = .aluminum, /// The color of the ghost in the macOS app icon. @@ -2640,6 +2906,8 @@ keybind: Keybinds = .{}, /// `custom-style`. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. +/// +/// Available since: 1.0.0 @"macos-icon-ghost-color": ?Color = null, /// The color of the screen in the macOS app icon. @@ -2653,6 +2921,7 @@ keybind: Keybinds = .{}, /// Note: This configuration is required when `macos-icon` is set to /// `custom-style`. /// +/// Available since: 1.0.0 @"macos-icon-screen-color": ?ColorList = null, /// Whether macOS Shortcuts are allowed to control Ghostty. @@ -2676,6 +2945,7 @@ keybind: Keybinds = .{}, /// /// * `deny` - Deny Shortcuts from controlling Ghostty. /// +/// Available since: 1.2.0 @"macos-shortcuts": MacShortcuts = .ask, /// Put every surface (tab, split, window) into a dedicated Linux cgroup. @@ -2704,6 +2974,7 @@ keybind: Keybinds = .{}, /// * `single-instance` - Enable cgroups only for Ghostty instances launched /// as single-instance applications (see gtk-single-instance). /// +/// Available since: 1.0.0 @"linux-cgroup": LinuxCgroup = if (builtin.os.tag == .linux) .@"single-instance" else @@ -2716,6 +2987,8 @@ else /// controller, which is a soft limit. You should configure something like /// systemd-oom to handle killing processes that have too much memory /// pressure. +/// +/// Available since: 1.0.0 @"linux-cgroup-memory-limit": ?u64 = null, /// Number of processes limit for any individual terminal process (tab, split, @@ -2723,6 +2996,8 @@ else /// /// Note that this sets the "pids.max" configuration for the process number /// controller, which is a hard limit. +/// +/// Available since: 1.0.0 @"linux-cgroup-processes-limit": ?u64 = null, /// If this is false, then any cgroup initialization (for linux-cgroup) @@ -2736,10 +3011,14 @@ else /// /// Note: This currently only affects cgroup initialization. Subprocesses /// must always be able to move themselves into an isolated cgroup. +/// +/// Available since: 1.0.0 @"linux-cgroup-hard-fail": bool = false, /// Enable or disable GTK's OpenGL debugging logs. The default is `true` for /// debug builds, `false` for all others. +/// +/// Available since: 1.1.0 @"gtk-opengl-debug": bool = builtin.mode == .Debug, /// If `true`, the Ghostty GTK application will run in single-instance mode: @@ -2755,6 +3034,8 @@ else /// /// Note that debug builds of Ghostty have a separate single-instance ID /// so you can test single instance without conflicting with release builds. +/// +/// Available since: 1.0.0 @"gtk-single-instance": GtkSingleInstance = .desktop, /// When enabled, the full GTK titlebar is displayed instead of your window @@ -2763,6 +3044,8 @@ else /// /// This option does nothing when `window-decoration` is false or when running /// under macOS. +/// +/// Available since: 1.0.0 @"gtk-titlebar": bool = true, /// Determines the side of the screen that the GTK tab bar will stick to. @@ -2773,10 +3056,14 @@ else /// tabs. Alternatively, you can use the `toggle_tab_overview` action in a /// keybind if your window doesn't have a title bar, or you can switch tabs /// with keybinds. +/// +/// Available since: 1.0.0 @"gtk-tabs-location": GtkTabsLocation = .top, /// If this is `true`, the titlebar will be hidden when the window is maximized, /// and shown when the titlebar is unmaximized. GTK only. +/// +/// Available since: 1.1.0 @"gtk-titlebar-hide-when-maximized": bool = false, /// Determines the appearance of the top and bottom bars tab bar. @@ -2787,12 +3074,16 @@ else /// * `raised` - Top and bottom bars cast a shadow on the terminal area. /// * `raised-border` - Similar to `raised` but the shadow is replaced with a /// more subtle border. +/// +/// Available since: 1.0.0 @"gtk-toolbar-style": GtkToolbarStyle = .raised, /// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs /// are the new typical Gnome style where tabs fill their available space. /// If you set this to `false` then tabs will only take up space they need, /// which is the old style. +/// +/// Available since: 1.0.0 @"gtk-wide-tabs": bool = true, /// Custom CSS files to be loaded. @@ -2814,10 +3105,14 @@ else /// not exist. If you want to include a file that begins with a literal ? /// character, surround the file path in double quotes ("). /// The file size limit for a single stylesheet is 5MiB. +/// +/// Available since: 1.1.0 @"gtk-custom-css": RepeatablePath = .{}, /// If `true` (default), applications running in the terminal can show desktop /// notifications using certain escape sequences such as OSC 9 or OSC 777. +/// +/// Available since: 1.0.0 @"desktop-notifications": bool = true, /// Modifies the color used for bold text in the terminal. @@ -2845,10 +3140,14 @@ else /// features. An option exists in vim to modify this: `:set /// keyprotocol=ghostty:kitty`, however a bug in the implementation prevents it /// from working properly. https://github.com/vim/vim/pull/13211 fixes this. +/// +/// Available since: 1.0.0 term: []const u8 = "xterm-ghostty", /// String to send when we receive `ENQ` (`0x05`) from the command that we are /// running. Defaults to an empty string if not set. +/// +/// Available since: 1.0.0 @"enquiry-response": []const u8 = "", /// The mechanism used to launch Ghostty. This should generally not be @@ -2866,6 +3165,8 @@ term: []const u8 = "xterm-ghostty", /// this isn't intended to be modified by users, the documentation is /// lighter than the other configurations and users are expected to /// refer to the code for details. +/// +/// Available since: 1.2.0 @"launched-from": ?LaunchSource = null, /// Configures the low-level API to use for async IO, eventing, etc. @@ -2894,6 +3195,8 @@ term: []const u8 = "xterm-ghostty", /// /// This is only supported on Linux, since this is the only platform /// where we have multiple options. On macOS, we always use `kqueue`. +/// +/// Available since: 1.2.0 @"async-backend": AsyncBackend = .auto, /// Control the auto-update functionality of Ghostty. This is only supported @@ -2919,6 +3222,8 @@ term: []const u8 = "xterm-ghostty", /// preference stored in the standard user defaults (`defaults(1)`). /// /// Changing this value at runtime works after a small delay. +/// +/// Available since: 1.0.0 @"auto-update": ?AutoUpdate = null, /// The release channel to use for auto-updates. @@ -2941,6 +3246,8 @@ term: []const u8 = "xterm-ghostty", /// Ghostty to take effect. /// /// This only works on macOS since only macOS has an auto-update feature. +/// +/// Available since: 1.0.0 @"auto-update-channel": ?build_config.ReleaseChannel = null, /// This is set by the CLI parser for deinit. From 527dcea266094ca690949693b5da7e9c2712f8e6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 22:06:18 -0500 Subject: [PATCH 69/86] core: avalability of config entry since 1.0.0 can be assumed --- src/config/Config.zig | 237 +----------------------------------------- 1 file changed, 1 insertion(+), 236 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 29e70a289..93bcfe09f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -126,8 +126,6 @@ pub const compatibility = std.StaticStringMap( /// /// Changing this configuration at runtime will only affect new terminals, i.e. /// new windows, tabs, etc. -/// -/// Available since: 1.0.0 @"font-family": RepeatableString = .{}, @"font-family-bold": RepeatableString = .{}, @"font-family-italic": RepeatableString = .{}, @@ -145,8 +143,6 @@ pub const compatibility = std.StaticStringMap( /// These are only valid if its corresponding font-family is also specified. If /// no font-family is specified, then the font-style is ignored unless you're /// disabling the font style. -/// -/// Available since: 1.0.0 @"font-style": FontStyle = .{ .default = {} }, @"font-style-bold": FontStyle = .{ .default = {} }, @"font-style-italic": FontStyle = .{ .default = {} }, @@ -182,8 +178,6 @@ pub const compatibility = std.StaticStringMap( /// explicitly disable it. You cannot partially disable `bold-italic`. /// /// By default, synthetic styles are enabled. -/// -/// Available since: 1.0.0 @"font-synthetic-style": FontSyntheticStyle = .{}, /// Apply a font feature. To enable multiple font features you can repeat @@ -208,8 +202,6 @@ pub const compatibility = std.StaticStringMap( /// [fontdrop.info](https://fontdrop.info). /// /// To generally disable most ligatures, use `-calt, -liga, -dlig`. -/// -/// Available since: 1.0.0 @"font-feature": RepeatableString = .{}, /// Font size in points. This value can be a non-integer and the nearest integer @@ -227,8 +219,6 @@ pub const compatibility = std.StaticStringMap( /// On Linux with GTK, font size is scaled according to both display-wide and /// text-specific scaling factors, which are often managed by your desktop /// environment (e.g. the GNOME display scale and large text settings). -/// -/// Available since: 1.0.0 @"font-size": f32 = switch (builtin.os.tag) { // On macOS we default a little bigger since this tends to look better. This // is purely subjective but this is easy to modify. @@ -254,8 +244,6 @@ pub const compatibility = std.StaticStringMap( /// /// Common axes are: `wght` (weight), `slnt` (slant), `ital` (italic), `opsz` /// (optical size), `wdth` (width), `GRAD` (gradient), etc. -/// -/// Available since: 1.0.0 @"font-variation": RepeatableFontVariation = .{}, @"font-variation-bold": RepeatableFontVariation = .{}, @"font-variation-italic": RepeatableFontVariation = .{}, @@ -277,14 +265,10 @@ pub const compatibility = std.StaticStringMap( /// /// Changing this configuration at runtime will only affect new terminals, /// i.e. new windows, tabs, etc. -/// -/// Available since: 1.0.0 @"font-codepoint-map": RepeatableCodepointMap = .{}, /// Draw fonts with a thicker stroke, if supported. /// This is currently only supported on macOS. -/// -/// Available since: 1.0.0 @"font-thicken": bool = false, /// Strength of thickening when `font-thicken` is enabled. @@ -295,8 +279,6 @@ pub const compatibility = std.StaticStringMap( /// Has no effect when `font-thicken` is set to `false`. /// /// This is currently only supported on macOS. -/// -/// Available since: 1.0.0 @"font-thicken-strength": u8 = 255, /// Locations to break font shaping into multiple runs. @@ -380,64 +362,42 @@ pub const compatibility = std.StaticStringMap( /// /// * Powerline glyphs will be adjusted along with the cell height so /// that things like status lines continue to look aligned. -/// -/// Available since: 1.0.0 @"adjust-cell-width": ?MetricModifier = null, @"adjust-cell-height": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the bottom of the cell to the text baseline. /// Increase to move baseline UP, decrease to move baseline DOWN. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-font-baseline": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the top of the cell to the top of the underline. /// Increase to move underline DOWN, decrease to move underline UP. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-underline-position": ?MetricModifier = null, /// Thickness in pixels of the underline. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-underline-thickness": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the top of the cell to the top of the strikethrough. /// Increase to move strikethrough DOWN, decrease to move strikethrough UP. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-strikethrough-position": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of the strikethrough. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-strikethrough-thickness": ?MetricModifier = null, /// Distance in pixels or percentage adjustment from the top of the cell to the top of the overline. /// Increase to move overline DOWN, decrease to move overline UP. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-overline-position": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of the overline. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-overline-thickness": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of the bar cursor and outlined rect cursor. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-cursor-thickness": ?MetricModifier = null, /// Height in pixels or percentage adjustment of the cursor. Currently applies to all cursor types: /// bar, rect, and outlined rect. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-cursor-height": ?MetricModifier = null, /// Thickness in pixels or percentage adjustment of box drawing characters. /// See the notes about adjustments in `adjust-cell-width`. -/// -/// Available since: 1.0.0 @"adjust-box-thickness": ?MetricModifier = null, /// Height in pixels or percentage adjustment of maximum height for nerd font icons. /// @@ -479,8 +439,6 @@ pub const compatibility = std.StaticStringMap( /// /// This configuration can be changed at runtime but will not affect existing /// terminals. Only new terminals will use the new configuration. -/// -/// Available since: 1.0.0 @"grapheme-width-method": GraphemeWidthMethod = .unicode, /// FreeType load flags to enable. The format of this is a list of flags to @@ -507,8 +465,6 @@ pub const compatibility = std.StaticStringMap( /// * `autohint` - Enable the freetype auto-hinter. Enabled by default. /// /// Example: `hinting`, `no-hinting`, `force-autohint`, `no-force-autohint` -/// -/// Available since: 1.0.0 @"freetype-load-flags": FreetypeLoadFlags = .{}, /// A theme to use. This can be a built-in theme name, a custom theme @@ -564,20 +520,14 @@ pub const compatibility = std.StaticStringMap( /// be fixed in a future update: /// /// - macOS: titlebar tabs style is not updated when switching themes. -/// -/// Available since: 1.0.0 theme: ?Theme = null, /// Background color for the window. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// -/// Available since: 1.0.0 background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, /// Foreground color for the window. /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// -/// Available since: 1.0.0 foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Background image for the terminal. @@ -684,8 +634,6 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. -/// -/// Available since: 1.0.0 @"selection-foreground": ?TerminalColor = null, @"selection-background": ?TerminalColor = null, @@ -717,8 +665,6 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// that text will become black or white. /// /// This value does not apply to Emoji or images. -/// -/// Available since: 1.0.0 @"minimum-contrast": f64 = 1, /// Color palette for the 256 color form that many terminal applications use. @@ -732,8 +678,6 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// /// For definitions on the color indices and what they canonically map to, /// [see this cheat sheet](https://www.ditig.com/256-colors-cheat-sheet). -/// -/// Available since: 1.0.0 palette: Palette = .{}, /// The color of the cursor. If this is not set, a default will be chosen. @@ -749,8 +693,6 @@ palette: Palette = .{}, /// /// * `cell-background` - Match the cell background color. /// (Available since version 1.2.0) -/// -/// Available since: 1.0.0 @"cursor-color": ?TerminalColor = null, /// The opacity level (opposite of transparency) of the cursor. A value of 1 @@ -775,8 +717,6 @@ palette: Palette = .{}, /// * `bar` /// * `underline` /// * `block_hollow` -/// -/// Available since: 1.0.0 @"cursor-style": terminal.CursorStyle = .block, /// Sets the default blinking state of the cursor. This is just the default @@ -796,8 +736,6 @@ palette: Palette = .{}, /// * ` ` (blank) /// * `true` /// * `false` -/// -/// Available since: 1.0.0 @"cursor-style-blink": ?bool = null, /// The color of the text under the cursor. If this is not set, a default will @@ -806,8 +744,6 @@ palette: Palette = .{}, /// Since version 1.2.0, this can also be set to `cell-foreground` to match /// the cell foreground color, or `cell-background` to match the cell /// background color. -/// -/// Available since: 1.0.0 @"cursor-text": ?TerminalColor = null, /// Enables the ability to move the cursor at prompts by using `alt+click` on @@ -823,16 +759,12 @@ palette: Palette = .{}, /// behavior around edge cases are to be expected. This is unfortunately how /// this feature is implemented across terminals because there isn't any other /// way to implement it. -/// -/// Available since: 1.0.0 @"cursor-click-to-move": bool = true, /// Hide the mouse immediately when typing. The mouse becomes visible again /// when the mouse is used (button, movement, etc.). Platform-specific behavior /// may dictate other scenarios where the mouse is shown. For example on macOS, /// the mouse is shown again when a new window, tab, or split is created. -/// -/// Available since: 1.0.0 @"mouse-hide-while-typing": bool = false, /// Determines whether running programs can detect the shift key pressed with a @@ -860,8 +792,6 @@ palette: Palette = .{}, /// * `false` /// * `always` /// * `never` -/// -/// Available since: 1.0.0 @"mouse-shift-capture": MouseShiftCapture = .false, /// Multiplier for scrolling distance with the mouse wheel. Any value less @@ -882,8 +812,6 @@ palette: Palette = .{}, /// widgets to show through which isn't generally desirable. /// /// On macOS, changing this configuration requires restarting Ghostty completely. -/// -/// Available since: 1.0.0 @"background-opacity": f64 = 1.0, /// Whether to blur the background when `background-opacity` is less than 1. @@ -917,8 +845,6 @@ palette: Palette = .{}, /// need to set environment-specific settings and/or install third-party plugins /// in order to support background blur, as there isn't a unified interface for /// doing so. -/// -/// Available since: 1.0.0 @"background-blur": BackgroundBlur = .false, /// The opacity level (opposite of transparency) of an unfocused split. @@ -930,8 +856,6 @@ palette: Palette = .{}, /// is 0.15. This value still looks weird but you can at least see what's going /// on. A value outside of the range 0.15 to 1 will be clamped to the nearest /// valid value. -/// -/// Available since: 1.0.0 @"unfocused-split-opacity": f64 = 0.7, /// The color to dim the unfocused split. Unfocused splits are dimmed by @@ -941,8 +865,6 @@ palette: Palette = .{}, /// This will default to the background color. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// -/// Available since: 1.0.0 @"unfocused-split-fill": ?Color = null, /// The color of the split divider. If this is not set, a default will be chosen. @@ -980,8 +902,6 @@ palette: Palette = .{}, /// arguments. For example, `ghostty -e fish --with --custom --args`. /// This flag sets the `initial-command` configuration, see that for more /// information. -/// -/// Available since: 1.0.0 command: ?Command = null, /// This is the same as "command", but only applies to the first terminal @@ -1018,8 +938,6 @@ command: ?Command = null, /// shell integration with a `-e`-executed command, you must either /// name your binary appropriately or source the shell integration script /// manually. -/// -/// Available since: 1.0.0 @"initial-command": ?Command = null, /// Extra environment variables to pass to commands launched in a terminal @@ -1109,8 +1027,6 @@ input: RepeatableReadableIO = .{}, /// received. /// /// This is primarily useful for scripts or debugging. -/// -/// Available since: 1.0.0 @"wait-after-command": bool = false, /// The number of milliseconds of runtime below which we consider a process exit @@ -1120,8 +1036,6 @@ input: RepeatableReadableIO = .{}, /// On Linux, this must be paired with a non-zero exit code. On macOS, we allow /// any exit code because of the way shell processes are launched via the login /// command. -/// -/// Available since: 1.0.0 @"abnormal-command-exit-runtime": u32 = 250, /// The size of the scrollback buffer in bytes. This also includes the active @@ -1143,8 +1057,6 @@ input: RepeatableReadableIO = .{}, /// This is a future planned feature. /// /// This can be changed at runtime but will only affect new terminal surfaces. -/// -/// Available since: 1.0.0 @"scrollback-limit": usize = 10_000_000, // 10MB /// Match a regular expression against the terminal text and associate clicking @@ -1159,8 +1071,6 @@ input: RepeatableReadableIO = .{}, /// exists. This can be disabled using `link-url`. /// /// TODO: This can't currently be set! -/// -/// Available since: 1.0.0 link: RepeatableLink = .{}, /// Enable URL matching. URLs are matched on hover with control (Linux) or @@ -1169,8 +1079,6 @@ link: RepeatableLink = .{}, /// /// The URL matcher is always lowest priority of any configured links (see /// `link`). If you want to customize URL matching, use `link` and disable this. -/// -/// Available since: 1.0.0 @"link-url": bool = true, /// Show link previews for a matched URL. @@ -1197,8 +1105,6 @@ maximize: bool = false, /// On macOS, this setting does not work if window-decoration is set to /// "false", because native fullscreen on macOS requires window decorations /// to be set. -/// -/// Available since: 1.0.0 fullscreen: bool = false, /// The title Ghostty will use for the window. This will force the title of the @@ -1215,8 +1121,6 @@ fullscreen: bool = false, /// sequence will be honored but previous changes will not retroactively /// be set. This latter case may require you to restart programs such as Neovim /// to get the new title. -/// -/// Available since: 1.0.0 title: ?[:0]const u8 = null, /// The setting that will change the application class value. @@ -1239,8 +1143,6 @@ title: ?[:0]const u8 = null, /// The default is `com.mitchellh.ghostty`. /// /// This only affects GTK builds. -/// -/// Available since: 1.0.0 class: ?[:0]const u8 = null, /// This controls the instance name field of the `WM_CLASS` X11 property when @@ -1249,8 +1151,6 @@ class: ?[:0]const u8 = null, /// The default is `ghostty`. /// /// This only affects GTK builds. -/// -/// Available since: 1.0.0 @"x11-instance-name": ?[:0]const u8 = null, /// The directory to change to after starting the command. @@ -1272,8 +1172,6 @@ class: ?[:0]const u8 = null, /// * `home` - The home directory of the executing user. /// /// * `inherit` - The working directory of the launching process. -/// -/// Available since: 1.0.0 @"working-directory": ?[]const u8 = null, /// Key bindings. The format is `trigger=action`. Duplicate triggers will @@ -1500,8 +1398,6 @@ class: ?[:0]const u8 = null, /// /// - Notably, global shortcuts have not been implemented on wlroots-based /// compositors like Sway (see [upstream issue](https://github.com/emersion/xdg-desktop-portal-wlr/issues/240)). -/// -/// Available since: 1.0.0 keybind: Keybinds = .{}, /// Horizontal window padding. This applies padding between the terminal cells @@ -1521,8 +1417,6 @@ keybind: Keybinds = .{}, /// left padding to 2 and the right padding to 4. If you want to set both /// paddings to the same value, you can use a single value. For example, /// `window-padding-x = 2` will set both paddings to 2. -/// -/// Available since: 1.0.0 @"window-padding-x": WindowPadding = .{ .top_left = 2, .bottom_right = 2 }, /// Vertical window padding. This applies padding between the terminal cells and @@ -1542,8 +1436,6 @@ keybind: Keybinds = .{}, /// top padding to 2 and the bottom padding to 4. If you want to set both /// paddings to the same value, you can use a single value. For example, /// `window-padding-y = 2` will set both paddings to 2. -/// -/// Available since: 1.0.0 @"window-padding-y": WindowPadding = .{ .top_left = 2, .bottom_right = 2 }, /// The viewport dimensions are usually not perfectly divisible by the cell @@ -1558,8 +1450,6 @@ keybind: Keybinds = .{}, /// apply. The other padding is applied first and may affect how many grid cells /// actually exist, and this is applied last in order to balance the padding /// given a certain viewport size and grid cell size. -/// -/// Available since: 1.0.0 @"window-padding-balance": bool = false, /// The color of the padding area of the window. Valid values are: @@ -1581,8 +1471,6 @@ keybind: Keybinds = .{}, /// do not look good extended. /// * The nearest row contains a perfect fit powerline character. These /// don't look good extended. -/// -/// Available since: 1.0.0 @"window-padding-color": WindowPaddingColor = .background, /// Synchronize rendering with the screen refresh rate. If true, this will @@ -1598,23 +1486,17 @@ keybind: Keybinds = .{}, /// Changing this value at runtime will only affect new terminals. /// /// This setting is only supported currently on macOS. -/// -/// Available since: 1.0.0 @"window-vsync": bool = true, /// If true, new windows and tabs will inherit the working directory of the /// previously focused window. If no window was previously focused, the default /// working directory will be used (the `working-directory` option). -/// -/// Available since: 1.0.0 @"window-inherit-working-directory": bool = true, /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default font size /// will be used. If this is false, the default font size specified in the /// configuration `font-size` will be used. -/// -/// Available since: 1.0.0 @"window-inherit-font-size": bool = true, /// Configure a preference for window decorations. This setting specifies @@ -1661,8 +1543,6 @@ keybind: Keybinds = .{}, /// /// macOS: To hide the titlebar without removing the native window borders /// or rounded corners, use `macos-titlebar-style = hidden` instead. -/// -/// Available since: 1.0.0 @"window-decoration": WindowDecoration = .auto, /// The font that will be used for the application's window and tab titles. @@ -1672,8 +1552,7 @@ keybind: Keybinds = .{}, /// Note: any font available on the system may be used, this font is not /// required to be a fixed-width font. /// -/// Available since: 1.0.0 (macOS) -/// Available since: 1.1.0 (GTK) +/// Available since: 1.1.0 (on GTK) @"window-title-font-family": ?[:0]const u8 = null, /// The text that will be displayed in the subtitle of the window. Valid values: @@ -1705,8 +1584,6 @@ keybind: Keybinds = .{}, /// non-terminal windows within Ghostty. /// /// This is currently only supported on macOS and Linux. -/// -/// Available since: 1.0.0 @"window-theme": WindowTheme = .auto, /// The color space to use when interpreting terminal colors. "Terminal colors" @@ -1719,8 +1596,6 @@ keybind: Keybinds = .{}, /// * `display-p3` - Interpret colors in the Display P3 color space. /// /// This setting is currently only supported on macOS. -/// -/// Available since: 1.0.0 @"window-colorspace": WindowColorspace = .srgb, /// The initial window size. This size is in terminal grid cells by default. @@ -1750,8 +1625,6 @@ keybind: Keybinds = .{}, /// `window-decoration`), then this will work as expected. /// /// Windows smaller than 10 wide by 4 high are not allowed. -/// -/// Available since: 1.0.0 @"window-height": u32 = 0, @"window-width": u32 = 0, @@ -1778,8 +1651,6 @@ keybind: Keybinds = .{}, /// Note: this is only supported on macOS. The GTK runtime does not support /// setting the window position, as windows are only allowed position /// themselves in X11 and not Wayland. -/// -/// Available since: 1.0.0 @"window-position-x": ?i16 = null, @"window-position-y": ?i16 = null, @@ -1813,15 +1684,11 @@ keybind: Keybinds = .{}, /// The default value is `default`. /// /// This is currently only supported on macOS. This has no effect on Linux. -/// -/// Available since: 1.0.0 @"window-save-state": WindowSaveState = .default, /// Resize the window in discrete increments of the focused surface's cell size. /// If this is disabled, surfaces are resized in pixel increments. Currently /// only supported on macOS. -/// -/// Available since: 1.0.0 @"window-step-resize": bool = false, /// The position where new tabs are created. Valid values: @@ -1830,8 +1697,6 @@ keybind: Keybinds = .{}, /// or at the end if there are no focused tabs. /// /// * `end` - Insert the new tab at the end of the tab list. -/// -/// Available since: 1.0.0 @"window-new-tab-position": WindowNewTabPosition = .current, /// Whether to show the tab bar. @@ -1855,8 +1720,6 @@ keybind: Keybinds = .{}, /// overview or by keybind actions. /// /// Currently only supported on Linux (GTK). -/// -/// Available since: 1.0.0 @"window-show-tab-bar": WindowShowTabBar = .auto, /// Background color for the window titlebar. This only takes effect if @@ -1864,8 +1727,6 @@ keybind: Keybinds = .{}, /// runtime. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// -/// Available since: 1.0.0 @"window-titlebar-background": ?Color = null, /// Foreground color for the window titlebar. This only takes effect if @@ -1873,8 +1734,6 @@ keybind: Keybinds = .{}, /// runtime. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// -/// Available since: 1.0.0 @"window-titlebar-foreground": ?Color = null, /// This controls when resize overlays are shown. Resize overlays are a @@ -1888,8 +1747,6 @@ keybind: Keybinds = .{}, /// subsequently resized. /// /// The default is `after-first`. -/// -/// Available since: 1.0.0 @"resize-overlay": ResizeOverlay = .@"after-first", /// If resize overlays are enabled, this controls the position of the overlay. @@ -1904,8 +1761,6 @@ keybind: Keybinds = .{}, /// * `bottom-right` /// /// The default is `center`. -/// -/// Available since: 1.0.0 @"resize-overlay-position": ResizeOverlayPosition = .center, /// If resize overlays are enabled, this controls how long the overlay is @@ -1996,8 +1851,6 @@ keybind: Keybinds = .{}, /// /// This value is separate for primary and alternate screens so the effective /// limit per surface is double. -/// -/// Available since: 1.0.0 @"image-storage-limit": u32 = 320 * 1000 * 1000, /// Whether to automatically copy selected text to the clipboard. `true` @@ -2011,8 +1864,6 @@ keybind: Keybinds = .{}, /// paste is always enabled even if this is `false`. /// /// The default value is true on Linux and macOS. -/// -/// Available since: 1.0.0 @"copy-on-select": CopyOnSelect = switch (builtin.os.tag) { .linux => .true, .macos => .true, @@ -2023,8 +1874,6 @@ keybind: Keybinds = .{}, /// (double, triple, etc.) or an entirely new single click. A value of zero will /// use a platform-specific default. The default on macOS is determined by the /// OS settings. On every other platform it is 500ms. -/// -/// Available since: 1.0.0 @"click-repeat-interval": u32 = 0, /// Additional configuration files to read. This configuration can be repeated @@ -2054,8 +1903,6 @@ keybind: Keybinds = .{}, /// If "foo" contains `a = 2`, the final value of `a` will be 2, because /// `foo` is loaded after the configuration file that configures the /// nested `config-file` value. -/// -/// Available since: 1.0.0 @"config-file": RepeatablePath = .{}, /// When this is true, the default configuration file paths will be loaded. @@ -2069,8 +1916,6 @@ keybind: Keybinds = .{}, /// This is a CLI-only configuration. Setting this in a configuration file /// will have no effect. It is not an error, but it will not do anything. /// This configuration can only be set via CLI arguments. -/// -/// Available since: 1.0.0 @"config-default-files": bool = true, /// Confirms that a surface should be closed before closing it. @@ -2079,8 +1924,6 @@ keybind: Keybinds = .{}, /// any confirmation. This can also be set to `always`, which will always /// confirm closing a surface, even if shell integration says a process isn't /// running. -/// -/// Available since: 1.0.0 @"confirm-close-surface": ConfirmCloseSurface = .true, /// Whether or not to quit after the last surface is closed. @@ -2092,8 +1935,6 @@ keybind: Keybinds = .{}, /// On Linux, if this is `true`, Ghostty can delay quitting fully until a /// configurable amount of time has passed after the last window is closed. /// See the documentation of `quit-after-last-window-closed-delay`. -/// -/// Available since: 1.0.0 @"quit-after-last-window-closed": bool = builtin.os.tag == .linux, /// Controls how long Ghostty will stay running after the last open surface has @@ -2135,8 +1976,6 @@ keybind: Keybinds = .{}, /// `quit-after-last-window-closed` is `true`. /// /// Only implemented on Linux. -/// -/// Available since: 1.0.0 @"quit-after-last-window-closed-delay": ?Duration = null, /// This controls whether an initial window is created when Ghostty @@ -2144,8 +1983,6 @@ keybind: Keybinds = .{}, /// `quit-after-last-window-closed-delay` is set, setting `initial-window` to /// `false` will mean that Ghostty will quit after the configured delay if no /// window is ever created. Only implemented on Linux and macOS. -/// -/// Available since: 1.0.0 @"initial-window": bool = true, /// The duration that undo operations remain available. After this @@ -2213,8 +2050,6 @@ keybind: Keybinds = .{}, /// /// Note: There is no default keybind for toggling the quick terminal. /// To enable this feature, bind the `toggle_quick_terminal` action to a key. -/// -/// Available since: 1.0.0 @"quick-terminal-position": QuickTerminalPosition = .top, /// The size of the quick terminal. @@ -2236,8 +2071,6 @@ keybind: Keybinds = .{}, /// from the first by a comma (`,`). Percentage and pixel sizes can be mixed /// together: for instance, a size of `50%,500px` for a top-positioned quick /// terminal would be half a screen tall, and 500 pixels wide. -/// -/// Available since: 1.0.0 @"quick-terminal-size": QuickTerminalSize = .{}, /// The screen where the quick terminal should show up. @@ -2260,8 +2093,6 @@ keybind: Keybinds = .{}, /// by the operating system. /// /// Only implemented on macOS. -/// -/// Available since: 1.0.0 @"quick-terminal-screen": QuickTerminalScreen = .main, /// Duration (in seconds) of the quick terminal enter and exit animation. @@ -2269,8 +2100,6 @@ keybind: Keybinds = .{}, /// runtime. /// /// Only implemented on macOS. -/// -/// Available since: 1.0.0 @"quick-terminal-animation-duration": f64 = 0.2, /// Automatically hide the quick terminal when focus shifts to another window. @@ -2281,8 +2110,6 @@ keybind: Keybinds = .{}, /// accessible than on macOS, meaning that it is more preferable to keep the /// quick terminal open until the user has completed their task. /// This default may change in the future. -/// -/// Available since: 1.0.0 @"quick-terminal-autohide": bool = switch (builtin.os.tag) { .linux => false, .macos => true, @@ -2365,8 +2192,6 @@ keybind: Keybinds = .{}, /// * `bash`, `elvish`, `fish`, `zsh` - Use this specific shell injection scheme. /// /// The default value is `detect`. -/// -/// Available since: 1.0.0 @"shell-integration": ShellIntegration = .detect, /// Shell integration features to enable. These require our shell integration @@ -2386,8 +2211,6 @@ keybind: Keybinds = .{}, /// * `title` - Set the window title via shell integration. /// /// Example: `cursor`, `no-cursor`, `sudo`, `no-sudo`, `title`, `no-title` -/// -/// Available since: 1.0.0 @"shell-integration-features": ShellIntegrationFeatures = .{}, /// Custom entries into the command palette. @@ -2431,8 +2254,6 @@ keybind: Keybinds = .{}, /// * `16-bit` - Color components are returned scaled, e.g. `rrrr/gggg/bbbb` /// /// The default value is `16-bit`. -/// -/// Available since: 1.0.0 @"osc-color-report-format": OSCColorReportFormat = .@"16-bit", /// If true, allows the "KAM" mode (ANSI mode 2) to be used within @@ -2441,8 +2262,6 @@ keybind: Keybinds = .{}, /// to be enabled. This will not be documented further because /// if you know you need KAM, you know. If you don't know if you /// need KAM, you don't need it. -/// -/// Available since: 1.0.0 @"vt-kam-allowed": bool = false, /// Custom shaders to run after the default shaders. This is a file path @@ -2516,8 +2335,6 @@ keybind: Keybinds = .{}, /// will be run in the order they are specified. /// /// This can be changed at runtime and will affect all open terminals. -/// -/// Available since: 1.0.0 @"custom-shader": RepeatablePath = .{}, /// If `true` (default), the focused terminal surface will run an animation @@ -2536,8 +2353,6 @@ keybind: Keybinds = .{}, /// depending on the shader and your terminal usage. /// /// This can be changed at runtime and will affect all open terminals. -/// -/// Available since: 1.0.0 @"custom-shader-animation": CustomShaderAnimation = .true, /// Bell features to enable if bell support is available in your runtime. Not @@ -2661,8 +2476,6 @@ keybind: Keybinds = .{}, /// Changing this option at runtime works, but will only apply to the next /// time the window is made fullscreen. If a window is already fullscreen, /// it will retain the previous setting until fullscreen is exited. -/// -/// Available since: 1.0.0 @"macos-non-native-fullscreen": NonNativeFullscreen = .false, /// Whether the window buttons in the macOS titlebar are visible. The window @@ -2726,8 +2539,6 @@ keybind: Keybinds = .{}, /// most cases. /// /// Changing this option at runtime only applies to new windows. -/// -/// Available since: 1.0.0 @"macos-titlebar-style": MacTitlebarStyle = .transparent, /// Whether the proxy icon in the macOS titlebar is visible. The proxy icon @@ -2749,8 +2560,6 @@ keybind: Keybinds = .{}, /// Therefore, to make this work after changing the setting, you must /// usually `cd` to a different directory, open a different file in an /// editor, etc. -/// -/// Available since: 1.0.0 @"macos-titlebar-proxy-icon": MacTitlebarProxyIcon = .visible, /// macOS doesn't have a distinct "alt" key and instead has the "option" @@ -2785,15 +2594,11 @@ keybind: Keybinds = .{}, /// /// The values `left` or `right` enable this for the left or right *Option* /// key, respectively. -/// -/// Available since: 1.0.0 @"macos-option-as-alt": ?OptionAsAlt = null, /// Whether to enable the macOS window shadow. The default value is true. /// With some window managers and window transparency settings, you may /// find false more visually appealing. -/// -/// Available since: 1.0.0 @"macos-window-shadow": bool = true, /// If true, the macOS icon in the dock and app switcher will be hidden. This is @@ -2834,8 +2639,6 @@ keybind: Keybinds = .{}, /// with legitimate accessibility software (or software that uses the /// accessibility APIs), since secure input prevents any application from /// reading keyboard events. -/// -/// Available since: 1.0.0 @"macos-auto-secure-input": bool = true, /// If true, Ghostty will show a graphical indication when secure input is @@ -2846,8 +2649,6 @@ keybind: Keybinds = .{}, /// or it is manually (and typically temporarily) enabled. However, if you /// always have secure input enabled, the indication can be distracting and /// you may want to disable it. -/// -/// Available since: 1.0.0 @"macos-secure-input-indication": bool = true, /// Customize the macOS app icon. @@ -2881,8 +2682,6 @@ keybind: Keybinds = .{}, /// This is because the update dialog is managed through a /// separate framework and cannot be customized without significant /// effort. -/// -/// Available since: 1.0.0 @"macos-icon": MacAppIcon = .official, /// The material to use for the frame of the macOS app icon. @@ -2896,8 +2695,6 @@ keybind: Keybinds = .{}, /// /// Note: This configuration is required when `macos-icon` is set to /// `custom-style`. -/// -/// Available since: 1.0.0 @"macos-icon-frame": MacAppIconFrame = .aluminum, /// The color of the ghost in the macOS app icon. @@ -2906,8 +2703,6 @@ keybind: Keybinds = .{}, /// `custom-style`. /// /// Specified as either hex (`#RRGGBB` or `RRGGBB`) or a named X11 color. -/// -/// Available since: 1.0.0 @"macos-icon-ghost-color": ?Color = null, /// The color of the screen in the macOS app icon. @@ -2920,8 +2715,6 @@ keybind: Keybinds = .{}, /// /// Note: This configuration is required when `macos-icon` is set to /// `custom-style`. -/// -/// Available since: 1.0.0 @"macos-icon-screen-color": ?ColorList = null, /// Whether macOS Shortcuts are allowed to control Ghostty. @@ -2973,8 +2766,6 @@ keybind: Keybinds = .{}, /// * `always` - Always use cgroups. /// * `single-instance` - Enable cgroups only for Ghostty instances launched /// as single-instance applications (see gtk-single-instance). -/// -/// Available since: 1.0.0 @"linux-cgroup": LinuxCgroup = if (builtin.os.tag == .linux) .@"single-instance" else @@ -2987,8 +2778,6 @@ else /// controller, which is a soft limit. You should configure something like /// systemd-oom to handle killing processes that have too much memory /// pressure. -/// -/// Available since: 1.0.0 @"linux-cgroup-memory-limit": ?u64 = null, /// Number of processes limit for any individual terminal process (tab, split, @@ -2996,8 +2785,6 @@ else /// /// Note that this sets the "pids.max" configuration for the process number /// controller, which is a hard limit. -/// -/// Available since: 1.0.0 @"linux-cgroup-processes-limit": ?u64 = null, /// If this is false, then any cgroup initialization (for linux-cgroup) @@ -3011,8 +2798,6 @@ else /// /// Note: This currently only affects cgroup initialization. Subprocesses /// must always be able to move themselves into an isolated cgroup. -/// -/// Available since: 1.0.0 @"linux-cgroup-hard-fail": bool = false, /// Enable or disable GTK's OpenGL debugging logs. The default is `true` for @@ -3034,8 +2819,6 @@ else /// /// Note that debug builds of Ghostty have a separate single-instance ID /// so you can test single instance without conflicting with release builds. -/// -/// Available since: 1.0.0 @"gtk-single-instance": GtkSingleInstance = .desktop, /// When enabled, the full GTK titlebar is displayed instead of your window @@ -3044,8 +2827,6 @@ else /// /// This option does nothing when `window-decoration` is false or when running /// under macOS. -/// -/// Available since: 1.0.0 @"gtk-titlebar": bool = true, /// Determines the side of the screen that the GTK tab bar will stick to. @@ -3056,8 +2837,6 @@ else /// tabs. Alternatively, you can use the `toggle_tab_overview` action in a /// keybind if your window doesn't have a title bar, or you can switch tabs /// with keybinds. -/// -/// Available since: 1.0.0 @"gtk-tabs-location": GtkTabsLocation = .top, /// If this is `true`, the titlebar will be hidden when the window is maximized, @@ -3074,16 +2853,12 @@ else /// * `raised` - Top and bottom bars cast a shadow on the terminal area. /// * `raised-border` - Similar to `raised` but the shadow is replaced with a /// more subtle border. -/// -/// Available since: 1.0.0 @"gtk-toolbar-style": GtkToolbarStyle = .raised, /// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs /// are the new typical Gnome style where tabs fill their available space. /// If you set this to `false` then tabs will only take up space they need, /// which is the old style. -/// -/// Available since: 1.0.0 @"gtk-wide-tabs": bool = true, /// Custom CSS files to be loaded. @@ -3111,8 +2886,6 @@ else /// If `true` (default), applications running in the terminal can show desktop /// notifications using certain escape sequences such as OSC 9 or OSC 777. -/// -/// Available since: 1.0.0 @"desktop-notifications": bool = true, /// Modifies the color used for bold text in the terminal. @@ -3140,14 +2913,10 @@ else /// features. An option exists in vim to modify this: `:set /// keyprotocol=ghostty:kitty`, however a bug in the implementation prevents it /// from working properly. https://github.com/vim/vim/pull/13211 fixes this. -/// -/// Available since: 1.0.0 term: []const u8 = "xterm-ghostty", /// String to send when we receive `ENQ` (`0x05`) from the command that we are /// running. Defaults to an empty string if not set. -/// -/// Available since: 1.0.0 @"enquiry-response": []const u8 = "", /// The mechanism used to launch Ghostty. This should generally not be @@ -3222,8 +2991,6 @@ term: []const u8 = "xterm-ghostty", /// preference stored in the standard user defaults (`defaults(1)`). /// /// Changing this value at runtime works after a small delay. -/// -/// Available since: 1.0.0 @"auto-update": ?AutoUpdate = null, /// The release channel to use for auto-updates. @@ -3246,8 +3013,6 @@ term: []const u8 = "xterm-ghostty", /// Ghostty to take effect. /// /// This only works on macOS since only macOS has an auto-update feature. -/// -/// Available since: 1.0.0 @"auto-update-channel": ?build_config.ReleaseChannel = null, /// This is set by the CLI parser for deinit. From 13805f7cc5d2169ab7d6943efe9b659249f52d85 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Jul 2025 22:17:20 -0500 Subject: [PATCH 70/86] build: disable fuzzy matching for msgmerge CI is currently configured to fail if there are any fuzzy matches in translation files. This change prevents `msgmerge` from creating any fuzzy matches when translations are updated. --- src/build/GhosttyI18n.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig index 778cfabc5..9dcc67a31 100644 --- a/src/build/GhosttyI18n.zig +++ b/src/build/GhosttyI18n.zig @@ -156,7 +156,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step { ); inline for (internal_os.i18n.locales) |locale| { - const msgmerge = b.addSystemCommand(&.{ "msgmerge", "-q" }); + const msgmerge = b.addSystemCommand(&.{ "msgmerge", "--quiet", "--no-fuzzy-matching" }); msgmerge.addFileArg(b.path("po/" ++ locale ++ ".po")); msgmerge.addFileArg(xgettext.captureStdOut()); usf.addCopyFileToSource(msgmerge.captureStdOut(), "po/" ++ locale ++ ".po"); From 57fdfe76bb46a98a823996d32483d128f3b6bb5c Mon Sep 17 00:00:00 2001 From: Robert Ian Hawdon Date: Wed, 9 Jul 2025 11:33:12 +0100 Subject: [PATCH 71/86] Changed behaviour of bold-color --- src/terminal/style.zig | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/terminal/style.zig b/src/terminal/style.zig index 78afcdf39..deb2b8ec5 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -154,15 +154,12 @@ pub const Style = struct { .palette => |idx| palette: { if (self.flags.bold) { - if (opts.bold) |bold| switch (bold) { - .color => |v| break :palette v.toTerminalRGB(), - .bright => { - const bright_offset = @intFromEnum(color.Name.bright_black); - if (idx < bright_offset) { - break :palette opts.palette[idx + bright_offset]; - } - }, - }; + if (opts.bold) |_| { + const bright_offset = @intFromEnum(color.Name.bright_black); + if (idx < bright_offset) { + break :palette opts.palette[idx + bright_offset]; + } + } } break :palette opts.palette[idx]; From 94cca0cc17f385b5ca52450ecf9f688df501167a Mon Sep 17 00:00:00 2001 From: Robert Ian Hawdon Date: Wed, 9 Jul 2025 11:35:44 +0100 Subject: [PATCH 72/86] Updated comment --- src/config/Config.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 8ca8d3154..f0514a18e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2825,8 +2825,8 @@ else /// This can be set to a specific color, using the same format as /// `background` or `foreground` (e.g. `#RRGGBB` but other formats /// are also supported; see the aforementioned documentation). If a -/// specific color is set, this color will always be used for all -/// bold text regardless of the terminal's color scheme. +/// specific color is set, this color will always be used for the default +/// bold text color. It will set the rest of the bold colors to `bright`. /// /// This can also be set to `bright`, which uses the bright color palette /// for bold text. For example, if the text is red, then the bold will From bcb4e624a45a647b9001994a0fe84500b28cc4c5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 06:45:26 -0700 Subject: [PATCH 73/86] cli: fix macOS builds --- src/cli/ssh_cache.zig | 125 ++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 54 deletions(-) diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 112f3a5c5..468fc7b3c 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -82,7 +82,8 @@ fn getCachePath(allocator: Allocator) ![]const u8 { // Supports both standalone hostnames and user@hostname format fn isValidCacheKey(key: []const u8) bool { - if (key.len == 0 or key.len > 320) return false; // 253 + 1 + 64 for user@hostname + // 253 + 1 + 64 for user@hostname + if (key.len == 0 or key.len > 320) return false; // Check for user@hostname format if (std.mem.indexOf(u8, key, "@")) |at_pos| { @@ -94,7 +95,8 @@ fn isValidCacheKey(key: []const u8) bool { return isValidHostname(key); } -// Basic hostname validation - accepts domains and IPs (including IPv6 in brackets) +// Basic hostname validation - accepts domains and IPs +// (including IPv6 in brackets) fn isValidHostname(host: []const u8) bool { if (host.len == 0 or host.len > 253) return false; @@ -438,24 +440,27 @@ pub fn run(alloc_gpa: Allocator) !u8 { } if (opts.add) |host| { - const result = addHost(alloc, host) catch |err| switch (err) { - CacheError.InvalidCacheKey => { - try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); - try stderr.print("Expected format: hostname or user@hostname\n", .{}); - return 1; - }, - CacheError.CacheLocked => { - try stderr.print("Error: Cache is busy, try again\n", .{}); - return 1; - }, - error.AccessDenied, error.PermissionDenied => { - try stderr.print("Error: Permission denied\n", .{}); - return 1; - }, - else => { - try stderr.print("Error: Unable to add '{s}' to cache\n", .{host}); - return 1; - }, + const result = addHost(alloc, host) catch |err| { + const Error = error{PermissionDenied} || @TypeOf(err); + switch (@as(Error, err)) { + CacheError.InvalidCacheKey => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + CacheError.CacheLocked => { + try stderr.print("Error: Cache is busy, try again\n", .{}); + return 1; + }, + error.AccessDenied, error.PermissionDenied => { + try stderr.print("Error: Permission denied\n", .{}); + return 1; + }, + else => { + try stderr.print("Error: Unable to add '{s}' to cache\n", .{host}); + return 1; + }, + } }; switch (result) { @@ -466,51 +471,63 @@ pub fn run(alloc_gpa: Allocator) !u8 { } if (opts.remove) |host| { - removeHost(alloc, host) catch |err| switch (err) { - CacheError.InvalidCacheKey => { - try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); - try stderr.print("Expected format: hostname or user@hostname\n", .{}); - return 1; - }, - CacheError.CacheLocked => { - try stderr.print("Error: Cache is busy, try again\n", .{}); - return 1; - }, - error.AccessDenied, error.PermissionDenied => { - try stderr.print("Error: Permission denied\n", .{}); - return 1; - }, - else => { - try stderr.print("Error: Unable to remove '{s}' from cache\n", .{host}); - return 1; - }, + removeHost(alloc, host) catch |err| { + const Error = error{PermissionDenied} || @TypeOf(err); + switch (@as(Error, err)) { + CacheError.InvalidCacheKey => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + CacheError.CacheLocked => { + try stderr.print("Error: Cache is busy, try again\n", .{}); + return 1; + }, + error.AccessDenied, error.PermissionDenied => { + try stderr.print("Error: Permission denied\n", .{}); + return 1; + }, + else => { + try stderr.print("Error: Unable to remove '{s}' from cache\n", .{host}); + return 1; + }, + } }; try stdout.print("Removed '{s}' from cache.\n", .{host}); return 0; } if (opts.host) |host| { - const cached = checkHost(alloc, host) catch |err| switch (err) { - CacheError.InvalidCacheKey => { - try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); - try stderr.print("Expected format: hostname or user@hostname\n", .{}); - return 1; - }, - error.AccessDenied, error.PermissionDenied => { - try stderr.print("Error: Permission denied\n", .{}); - return 1; - }, - else => { - try stderr.print("Error: Unable to check host '{s}' in cache\n", .{host}); - return 1; - }, + const cached = checkHost(alloc, host) catch |err| { + const Error = error{PermissionDenied} || @TypeOf(err); + switch (@as(Error, err)) { + CacheError.InvalidCacheKey => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + error.AccessDenied, error.PermissionDenied => { + try stderr.print("Error: Permission denied\n", .{}); + return 1; + }, + else => { + try stderr.print("Error: Unable to check host '{s}' in cache\n", .{host}); + return 1; + }, + } }; if (cached) { - try stdout.print("'{s}' has Ghostty terminfo installed.\n", .{host}); + try stdout.print( + "'{s}' has Ghostty terminfo installed.\n", + .{host}, + ); return 0; } else { - try stdout.print("'{s}' does not have Ghostty terminfo installed.\n", .{host}); + try stdout.print( + "'{s}' does not have Ghostty terminfo installed.\n", + .{host}, + ); return 1; } } From 7cfb026e84ae1d93d123a8b42930b152b4fb9305 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 06:47:15 -0700 Subject: [PATCH 74/86] cli: ssh-cache stylistic changes --- src/cli/ssh_cache.zig | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 468fc7b3c..2912addc1 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -50,7 +50,7 @@ const CacheEntry = struct { return null; }; - return CacheEntry{ + return .{ .hostname = hostname, .timestamp = timestamp, .terminfo_version = terminfo_version, @@ -107,9 +107,8 @@ fn isValidHostname(host: []const u8) bool { var has_colon = false; for (ipv6_part) |c| { switch (c) { - 'a'...'f', 'A'...'F', '0'...'9', ':' => { - if (c == ':') has_colon = true; - }, + 'a'...'f', 'A'...'F', '0'...'9' => {}, + ':' => has_colon = true, else => return false, } } @@ -178,7 +177,7 @@ fn readCacheFile( const gop = try entries.getOrPut(hostname_copy); if (!gop.found_existing) { const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); - gop.value_ptr.* = CacheEntry{ + gop.value_ptr.* = .{ .hostname = hostname_copy, .timestamp = entry.timestamp, .terminfo_version = terminfo_copy, From 8ab3010bb8d2f14a36b6f0f6b3f26fd9e26bbb5b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 07:28:56 -0700 Subject: [PATCH 75/86] cli: rewrite ssh-cache diskcache and test IO --- src/cli/ssh-cache/DiskCache.zig | 549 ++++++++++++++++++++++ src/cli/ssh-cache/Entry.zig | 154 +++++++ src/cli/ssh_cache.zig | 755 +++++-------------------------- src/termio/shell_integration.zig | 26 +- 4 files changed, 829 insertions(+), 655 deletions(-) create mode 100644 src/cli/ssh-cache/DiskCache.zig create mode 100644 src/cli/ssh-cache/Entry.zig diff --git a/src/cli/ssh-cache/DiskCache.zig b/src/cli/ssh-cache/DiskCache.zig new file mode 100644 index 000000000..10f38a27c --- /dev/null +++ b/src/cli/ssh-cache/DiskCache.zig @@ -0,0 +1,549 @@ +/// An SSH terminfo entry cache that stores its cache data on +/// disk. The cache only stores metadata (hostname, terminfo value, +/// etc.) and does not store any sensitive data. +const DiskCache = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const xdg = @import("../../os/main.zig").xdg; +const TempDir = @import("../../os/main.zig").TempDir; +const Entry = @import("Entry.zig"); + +// 512KB - sufficient for approximately 10k entries +const MAX_CACHE_SIZE = 512 * 1024; + +/// Path to a file where the cache is stored. +path: []const u8, + +pub const DefaultPathError = Allocator.Error || error{ + /// The general error that is returned for any filesystem error + /// that may have resulted in the XDG lookup failing. + XdgLookupFailed, +}; + +pub const Error = error{ CacheIsLocked, HostnameIsInvalid }; + +/// Returns the default path for the cache for a given program. +/// +/// On all platforms, this is `${XDG_STATE_HOME}/ghostty/ssh_cache`. +/// +/// The returned value is allocated and must be freed by the caller. +pub fn defaultPath( + alloc: Allocator, + program: []const u8, +) DefaultPathError![]const u8 { + const state_dir: []const u8 = xdg.state( + alloc, + .{ .subdir = program }, + ) catch |err| return switch (err) { + error.OutOfMemory => error.OutOfMemory, + else => error.XdgLookupFailed, + }; + defer alloc.free(state_dir); + return try std.fs.path.join(alloc, &.{ state_dir, "ssh_cache" }); +} + +/// Clear all cache data stored in the disk cache. +/// This removes the cache file from disk, effectively clearing all cached +/// SSH terminfo entries. +pub fn clear(self: DiskCache) !void { + std.fs.cwd().deleteFile(self.path) catch |err| switch (err) { + error.FileNotFound => {}, + else => return err, + }; +} + +pub const AddResult = enum { added, updated }; + +/// Add or update a hostname entry in the cache. +/// Returns AddResult.added for new entries or AddResult.updated for existing ones. +/// The cache file is created if it doesn't exist with secure permissions (0600). +pub fn add( + self: DiskCache, + alloc: Allocator, + hostname: []const u8, +) !AddResult { + if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; + + // Create cache directory if needed + if (std.fs.path.dirname(self.path)) |dir| { + std.fs.makeDirAbsolute(dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + } + + // Open or create cache file with secure permissions + const file = std.fs.createFileAbsolute(self.path, .{ + .read = true, + .truncate = false, + .mode = 0o600, + }) catch |err| switch (err) { + error.PathAlreadyExists => blk: { + const existing_file = try std.fs.openFileAbsolute( + self.path, + .{ .mode = .read_write }, + ); + errdefer existing_file.close(); + try fixupPermissions(existing_file); + break :blk existing_file; + }, + else => return err, + }; + defer file.close(); + + // Lock + _ = file.tryLock(.exclusive) catch return error.CacheIsLocked; + defer file.unlock(); + + var entries = try readEntries(alloc, file); + defer deinitEntries(alloc, &entries); + + // Add or update entry + const gop = try entries.getOrPut(hostname); + const result: AddResult = if (!gop.found_existing) add: { + const hostname_copy = try alloc.dupe(u8, hostname); + errdefer alloc.free(hostname_copy); + const terminfo_copy = try alloc.dupe(u8, "xterm-ghostty"); + errdefer alloc.free(terminfo_copy); + + gop.key_ptr.* = hostname_copy; + gop.value_ptr.* = .{ + .hostname = gop.key_ptr.*, + .timestamp = std.time.timestamp(), + .terminfo_version = terminfo_copy, + }; + break :add .added; + } else update: { + // Update timestamp for existing entry + gop.value_ptr.timestamp = std.time.timestamp(); + break :update .updated; + }; + + try self.writeCacheFile(alloc, entries, null); + return result; +} + +/// Remove a hostname entry from the cache. +/// No error is returned if the hostname doesn't exist or the cache file is missing. +pub fn remove( + self: DiskCache, + alloc: Allocator, + hostname: []const u8, +) !void { + if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; + + // Open our file + const file = std.fs.openFileAbsolute( + self.path, + .{ .mode = .read_write }, + ) catch |err| switch (err) { + error.FileNotFound => return, + else => return err, + }; + defer file.close(); + try fixupPermissions(file); + + // Acquire exclusive lock + _ = file.tryLock(.exclusive) catch return error.CacheIsLocked; + defer file.unlock(); + + // Read existing entries + var entries = try readEntries(alloc, file); + defer deinitEntries(alloc, &entries); + + // Remove the entry if it exists and ensure we free the memory + if (entries.fetchRemove(hostname)) |kv| { + assert(kv.key.ptr == kv.value.hostname.ptr); + alloc.free(kv.value.hostname); + alloc.free(kv.value.terminfo_version); + } + + try self.writeCacheFile(alloc, entries, null); +} + +/// Check if a hostname exists in the cache. +/// Returns false if the cache file doesn't exist. +pub fn contains( + self: DiskCache, + alloc: Allocator, + hostname: []const u8, +) !bool { + if (!isValidCacheKey(hostname)) return error.HostnameIsInvalid; + + // Open our file + const file = std.fs.openFileAbsolute( + self.path, + .{ .mode = .read_write }, + ) catch |err| switch (err) { + error.FileNotFound => return false, + else => return err, + }; + defer file.close(); + try fixupPermissions(file); + + // Read existing entries + var entries = try readEntries(alloc, file); + defer deinitEntries(alloc, &entries); + + return entries.contains(hostname); +} + +fn fixupPermissions(file: std.fs.File) !void { + // Ensure file has correct permissions (readable/writable by + // owner only) + const stat = try file.stat(); + if (stat.mode & 0o777 != 0o600) { + try file.chmod(0o600); + } +} + +fn writeCacheFile( + self: DiskCache, + alloc: Allocator, + entries: std.StringHashMap(Entry), + expire_days: ?u32, +) !void { + var td: TempDir = try .init(); + defer td.deinit(); + + const tmp_file = try td.dir.createFile("ssh-cache", .{ .mode = 0o600 }); + defer tmp_file.close(); + const tmp_path = try td.dir.realpathAlloc(alloc, "ssh-cache"); + defer alloc.free(tmp_path); + + const writer = tmp_file.writer(); + var iter = entries.iterator(); + while (iter.next()) |kv| { + // Only write non-expired entries + if (kv.value_ptr.isExpired(expire_days)) continue; + try kv.value_ptr.format(writer); + } + + // Atomic replace + try std.fs.renameAbsolute(tmp_path, self.path); +} + +/// List all entries in the cache. +/// The returned HashMap must be freed using `deinitEntries`. +/// Returns an empty map if the cache file doesn't exist. +pub fn list( + self: DiskCache, + alloc: Allocator, +) !std.StringHashMap(Entry) { + // Open our file + const file = std.fs.openFileAbsolute( + self.path, + .{}, + ) catch |err| switch (err) { + error.FileNotFound => return .init(alloc), + else => return err, + }; + defer file.close(); + return readEntries(alloc, file); +} + +/// Free memory allocated by the `list` function. +/// This must be called to properly deallocate all entry data. +pub fn deinitEntries( + alloc: Allocator, + entries: *std.StringHashMap(Entry), +) void { + // All our entries we dupe the memory owned by the hostname and the + // terminfo, and we always match the hostname key and value. + var it = entries.iterator(); + while (it.next()) |entry| { + assert(entry.key_ptr.*.ptr == entry.value_ptr.hostname.ptr); + alloc.free(entry.value_ptr.hostname); + alloc.free(entry.value_ptr.terminfo_version); + } + entries.deinit(); +} + +fn readEntries( + alloc: Allocator, + file: std.fs.File, +) !std.StringHashMap(Entry) { + const content = try file.readToEndAlloc(alloc, MAX_CACHE_SIZE); + defer alloc.free(content); + + var entries = std.StringHashMap(Entry).init(alloc); + var lines = std.mem.tokenizeScalar(u8, content, '\n'); + while (lines.next()) |line| { + const trimmed = std.mem.trim(u8, line, " \t\r"); + const entry = Entry.parse(trimmed) orelse continue; + + // Always allocate hostname first to avoid key pointer confusion + const hostname = try alloc.dupe(u8, entry.hostname); + errdefer alloc.free(hostname); + + const gop = try entries.getOrPut(hostname); + if (!gop.found_existing) { + const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); + gop.value_ptr.* = .{ + .hostname = hostname, + .timestamp = entry.timestamp, + .terminfo_version = terminfo_copy, + }; + } else { + // Don't need the copy since entry already exists + alloc.free(hostname); + + // Handle duplicate entries - keep newer timestamp + if (entry.timestamp > gop.value_ptr.timestamp) { + gop.value_ptr.timestamp = entry.timestamp; + if (!std.mem.eql( + u8, + gop.value_ptr.terminfo_version, + entry.terminfo_version, + )) { + alloc.free(gop.value_ptr.terminfo_version); + const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); + gop.value_ptr.terminfo_version = terminfo_copy; + } + } + } + } + + return entries; +} + +// Supports both standalone hostnames and user@hostname format +fn isValidCacheKey(key: []const u8) bool { + // 253 + 1 + 64 for user@hostname + if (key.len == 0 or key.len > 320) return false; + + // Check for user@hostname format + if (std.mem.indexOf(u8, key, "@")) |at_pos| { + const user = key[0..at_pos]; + const hostname = key[at_pos + 1 ..]; + return isValidUser(user) and isValidHostname(hostname); + } + + return isValidHostname(key); +} + +// Basic hostname validation - accepts domains and IPs +// (including IPv6 in brackets) +fn isValidHostname(host: []const u8) bool { + if (host.len == 0 or host.len > 253) return false; + + // Handle IPv6 addresses in brackets + if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') { + const ipv6_part = host[1 .. host.len - 1]; + if (ipv6_part.len == 0) return false; + var has_colon = false; + for (ipv6_part) |c| { + switch (c) { + 'a'...'f', 'A'...'F', '0'...'9' => {}, + ':' => has_colon = true, + else => return false, + } + } + return has_colon; + } + + // Standard hostname/domain validation + for (host) |c| { + switch (c) { + 'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {}, + else => return false, + } + } + + // No leading/trailing dots or hyphens, no consecutive dots + if (host[0] == '.' or host[0] == '-' or + host[host.len - 1] == '.' or host[host.len - 1] == '-') + { + return false; + } + + return std.mem.indexOf(u8, host, "..") == null; +} + +fn isValidUser(user: []const u8) bool { + if (user.len == 0 or user.len > 64) return false; + for (user) |c| { + switch (c) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-', '.' => {}, + else => return false, + } + } + return true; +} + +test "disk cache default path" { + const testing = std.testing; + const alloc = std.testing.allocator; + + const path = try DiskCache.defaultPath(alloc, "ghostty"); + defer alloc.free(path); + try testing.expect(path.len > 0); +} + +test "disk cache clear" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create our path + var td: TempDir = try .init(); + defer td.deinit(); + { + var file = try td.dir.createFile("cache", .{}); + defer file.close(); + try file.writer().writeAll("HELLO!"); + } + const path = try td.dir.realpathAlloc(alloc, "cache"); + defer alloc.free(path); + + // Setup our cache + const cache: DiskCache = .{ .path = path }; + try cache.clear(); + + // Verify the file is gone + try testing.expectError( + error.FileNotFound, + td.dir.openFile("cache", .{}), + ); +} + +test "disk cache operations" { + const testing = std.testing; + const alloc = testing.allocator; + + // Create our path + var td: TempDir = try .init(); + defer td.deinit(); + { + var file = try td.dir.createFile("cache", .{}); + defer file.close(); + try file.writer().writeAll("HELLO!"); + } + const path = try td.dir.realpathAlloc(alloc, "cache"); + defer alloc.free(path); + + // Setup our cache + const cache: DiskCache = .{ .path = path }; + try testing.expectEqual( + AddResult.added, + try cache.add(alloc, "example.com"), + ); + try testing.expectEqual( + AddResult.updated, + try cache.add(alloc, "example.com"), + ); + try testing.expect( + try cache.contains(alloc, "example.com"), + ); + + // List + var entries = try cache.list(alloc); + deinitEntries(alloc, &entries); + + // Remove + try cache.remove(alloc, "example.com"); + try testing.expect( + !(try cache.contains(alloc, "example.com")), + ); + try testing.expectEqual( + AddResult.added, + try cache.add(alloc, "example.com"), + ); +} + +// Tests +test "hostname validation - valid cases" { + const testing = std.testing; + try testing.expect(isValidHostname("example.com")); + try testing.expect(isValidHostname("sub.example.com")); + try testing.expect(isValidHostname("host-name.domain.org")); + try testing.expect(isValidHostname("192.168.1.1")); + try testing.expect(isValidHostname("a")); + try testing.expect(isValidHostname("1")); +} + +test "hostname validation - IPv6 addresses" { + const testing = std.testing; + try testing.expect(isValidHostname("[::1]")); + try testing.expect(isValidHostname("[2001:db8::1]")); + try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported + try testing.expect(!isValidHostname("[]")); // Empty IPv6 + try testing.expect(!isValidHostname("[invalid]")); // No colons +} + +test "hostname validation - invalid cases" { + const testing = std.testing; + try testing.expect(!isValidHostname("")); + try testing.expect(!isValidHostname("host\nname")); + try testing.expect(!isValidHostname(".example.com")); + try testing.expect(!isValidHostname("example.com.")); + try testing.expect(!isValidHostname("host..domain")); + try testing.expect(!isValidHostname("-hostname")); + try testing.expect(!isValidHostname("hostname-")); + try testing.expect(!isValidHostname("host name")); + try testing.expect(!isValidHostname("host_name")); + try testing.expect(!isValidHostname("host@domain")); + try testing.expect(!isValidHostname("host:port")); + + // Too long + const long_host = "a" ** 254; + try testing.expect(!isValidHostname(long_host)); +} + +test "user validation - valid cases" { + const testing = std.testing; + try testing.expect(isValidUser("user")); + try testing.expect(isValidUser("deploy")); + try testing.expect(isValidUser("test-user")); + try testing.expect(isValidUser("user_name")); + try testing.expect(isValidUser("user.name")); + try testing.expect(isValidUser("user123")); + try testing.expect(isValidUser("a")); +} + +test "user validation - complex realistic cases" { + const testing = std.testing; + try testing.expect(isValidUser("git")); + try testing.expect(isValidUser("ubuntu")); + try testing.expect(isValidUser("root")); + try testing.expect(isValidUser("service.account")); + try testing.expect(isValidUser("user-with-dashes")); +} + +test "user validation - invalid cases" { + const testing = std.testing; + try testing.expect(!isValidUser("")); + try testing.expect(!isValidUser("user name")); + try testing.expect(!isValidUser("user@domain")); + try testing.expect(!isValidUser("user:group")); + try testing.expect(!isValidUser("user\nname")); + + // Too long + const long_user = "a" ** 65; + try testing.expect(!isValidUser(long_user)); +} + +test "cache key validation - hostname format" { + const testing = std.testing; + try testing.expect(isValidCacheKey("example.com")); + try testing.expect(isValidCacheKey("sub.example.com")); + try testing.expect(isValidCacheKey("192.168.1.1")); + try testing.expect(isValidCacheKey("[::1]")); + try testing.expect(!isValidCacheKey("")); + try testing.expect(!isValidCacheKey(".invalid.com")); +} + +test "cache key validation - user@hostname format" { + const testing = std.testing; + try testing.expect(isValidCacheKey("user@example.com")); + try testing.expect(isValidCacheKey("deploy@prod.server.com")); + try testing.expect(isValidCacheKey("test-user@192.168.1.1")); + try testing.expect(isValidCacheKey("user_name@host.domain.org")); + try testing.expect(isValidCacheKey("git@github.com")); + try testing.expect(isValidCacheKey("ubuntu@[::1]")); + try testing.expect(!isValidCacheKey("@example.com")); + try testing.expect(!isValidCacheKey("user@")); + try testing.expect(!isValidCacheKey("user@@host")); + try testing.expect(!isValidCacheKey("user@.invalid.com")); +} diff --git a/src/cli/ssh-cache/Entry.zig b/src/cli/ssh-cache/Entry.zig new file mode 100644 index 000000000..3a691be80 --- /dev/null +++ b/src/cli/ssh-cache/Entry.zig @@ -0,0 +1,154 @@ +/// A single entry within our SSH entry cache. Our SSH entry cache +/// stores which hosts we've sent our terminfo to so that we don't have +/// to send it again. It doesn't store any sensitive information. +const Entry = @This(); + +const std = @import("std"); + +hostname: []const u8, +timestamp: i64, +terminfo_version: []const u8, + +pub fn parse(line: []const u8) ?Entry { + const trimmed = std.mem.trim(u8, line, " \t\r\n"); + if (trimmed.len == 0) return null; + + // Parse format: hostname|timestamp|terminfo_version + var iter = std.mem.tokenizeScalar(u8, trimmed, '|'); + const hostname = iter.next() orelse return null; + const timestamp_str = iter.next() orelse return null; + const terminfo_version = iter.next() orelse "xterm-ghostty"; + const timestamp = std.fmt.parseInt(i64, timestamp_str, 10) catch |err| { + std.log.warn( + "Invalid timestamp in cache entry: {s} err={}", + .{ timestamp_str, err }, + ); + return null; + }; + + return .{ + .hostname = hostname, + .timestamp = timestamp, + .terminfo_version = terminfo_version, + }; +} + +pub fn format(self: Entry, writer: anytype) !void { + try writer.print( + "{s}|{d}|{s}\n", + .{ self.hostname, self.timestamp, self.terminfo_version }, + ); +} + +pub fn isExpired(self: Entry, expire_days_: ?u32) bool { + const expire_days = expire_days_ orelse return false; + const now = std.time.timestamp(); + const age_days = @divTrunc(now -| self.timestamp, std.time.s_per_day); + return age_days > expire_days; +} + +test "cache entry expiration" { + const testing = std.testing; + const now = std.time.timestamp(); + + const fresh_entry: Entry = .{ + .hostname = "test.com", + .timestamp = now - std.time.s_per_day, // 1 day old + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(!fresh_entry.isExpired(90)); + + const old_entry: Entry = .{ + .hostname = "old.com", + .timestamp = now - (std.time.s_per_day * 100), // 100 days old + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(old_entry.isExpired(90)); + + // Test never-expire case + try testing.expect(!old_entry.isExpired(null)); +} + +test "cache entry expiration exact boundary" { + const testing = std.testing; + const now = std.time.timestamp(); + + // Exactly at expiration boundary + const boundary_entry: Entry = .{ + .hostname = "example.com", + .timestamp = now - (std.time.s_per_day * 30), + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(!boundary_entry.isExpired(30)); + try testing.expect(boundary_entry.isExpired(29)); +} + +test "cache entry expiration large timestamp" { + const testing = std.testing; + const now = std.time.timestamp(); + + const boundary_entry: Entry = .{ + .hostname = "example.com", + .timestamp = now + (std.time.s_per_day * 30), + .terminfo_version = "xterm-ghostty", + }; + try testing.expect(!boundary_entry.isExpired(30)); +} + +test "cache entry parsing valid formats" { + const testing = std.testing; + + const entry = Entry.parse("example.com|1640995200|xterm-ghostty").?; + try testing.expectEqualStrings("example.com", entry.hostname); + try testing.expectEqual(@as(i64, 1640995200), entry.timestamp); + try testing.expectEqualStrings("xterm-ghostty", entry.terminfo_version); + + // Test default terminfo version + const entry_no_version = Entry.parse("test.com|1640995200").?; + try testing.expectEqualStrings( + "xterm-ghostty", + entry_no_version.terminfo_version, + ); + + // Test complex hostnames + const complex_entry = Entry.parse("user@server.example.com|1640995200|xterm-ghostty").?; + try testing.expectEqualStrings( + "user@server.example.com", + complex_entry.hostname, + ); +} + +test "cache entry parsing invalid formats" { + const testing = std.testing; + + try testing.expect(Entry.parse("") == null); + + // Invalid format (no pipe) + try testing.expect(Entry.parse("v1") == null); + + // Missing timestamp + try testing.expect(Entry.parse("example.com") == null); + + // Invalid timestamp + try testing.expect(Entry.parse("example.com|invalid") == null); + + // Empty terminfo should default + try testing.expect(Entry.parse("example.com|1640995200|") != null); +} + +test "cache entry parsing malformed data resilience" { + const testing = std.testing; + + // Extra pipes should not break parsing + try testing.expect(Entry.parse("host|123|term|extra") != null); + + // Whitespace handling + try testing.expect(Entry.parse(" host|123|term ") != null); + try testing.expect(Entry.parse("\n") == null); + try testing.expect(Entry.parse(" \t \n") == null); + + // Extremely large timestamp + try testing.expect( + Entry.parse("host|999999999999999999999999999999999999999999999999|xterm-ghostty") == null, + ); +} diff --git a/src/cli/ssh_cache.zig b/src/cli/ssh_cache.zig index 2912addc1..c8e2e1123 100644 --- a/src/cli/ssh_cache.zig +++ b/src/cli/ssh_cache.zig @@ -4,21 +4,15 @@ const Allocator = std.mem.Allocator; const xdg = @import("../os/xdg.zig"); const args = @import("args.zig"); const Action = @import("action.zig").Action; - -pub const CacheError = error{ - InvalidCacheKey, - CacheLocked, -} || fs.File.OpenError || fs.File.WriteError || Allocator.Error; - -const MAX_CACHE_SIZE = 512 * 1024; // 512KB - sufficient for approximately 10k entries -const NEVER_EXPIRE = 0; +pub const Entry = @import("ssh-cache/Entry.zig"); +pub const DiskCache = @import("ssh-cache/DiskCache.zig"); pub const Options = struct { clear: bool = false, add: ?[]const u8 = null, remove: ?[]const u8 = null, host: ?[]const u8 = null, - @"expire-days": u32 = NEVER_EXPIRE, + @"expire-days": ?u32 = null, pub fn deinit(self: *Options) void { _ = self; @@ -30,373 +24,6 @@ pub const Options = struct { } }; -const CacheEntry = struct { - hostname: []const u8, - timestamp: i64, - terminfo_version: []const u8, - - fn parse(line: []const u8) ?CacheEntry { - const trimmed = std.mem.trim(u8, line, " \t\r\n"); - if (trimmed.len == 0) return null; - - // Parse format: hostname|timestamp|terminfo_version - var iter = std.mem.tokenizeScalar(u8, trimmed, '|'); - const hostname = iter.next() orelse return null; - const timestamp_str = iter.next() orelse return null; - const terminfo_version = iter.next() orelse "xterm-ghostty"; - - const timestamp = std.fmt.parseInt(i64, timestamp_str, 10) catch |err| { - std.log.warn("Invalid timestamp in cache entry: {s} err={}", .{ timestamp_str, err }); - return null; - }; - - return .{ - .hostname = hostname, - .timestamp = timestamp, - .terminfo_version = terminfo_version, - }; - } - - fn format(self: CacheEntry, writer: anytype) !void { - try writer.print("{s}|{d}|{s}\n", .{ self.hostname, self.timestamp, self.terminfo_version }); - } - - fn isExpired(self: CacheEntry, expire_days: u32) bool { - if (expire_days == NEVER_EXPIRE) return false; - const now = std.time.timestamp(); - const age_days = @divTrunc(now - self.timestamp, std.time.s_per_day); - return age_days > expire_days; - } -}; - -const AddResult = enum { - added, - updated, -}; - -fn getCachePath(allocator: Allocator) ![]const u8 { - const state_dir = try xdg.state(allocator, .{ .subdir = "ghostty" }); - defer allocator.free(state_dir); - return try std.fs.path.join(allocator, &.{ state_dir, "ssh_cache" }); -} - -// Supports both standalone hostnames and user@hostname format -fn isValidCacheKey(key: []const u8) bool { - // 253 + 1 + 64 for user@hostname - if (key.len == 0 or key.len > 320) return false; - - // Check for user@hostname format - if (std.mem.indexOf(u8, key, "@")) |at_pos| { - const user = key[0..at_pos]; - const hostname = key[at_pos + 1 ..]; - return isValidUser(user) and isValidHostname(hostname); - } - - return isValidHostname(key); -} - -// Basic hostname validation - accepts domains and IPs -// (including IPv6 in brackets) -fn isValidHostname(host: []const u8) bool { - if (host.len == 0 or host.len > 253) return false; - - // Handle IPv6 addresses in brackets - if (host.len >= 4 and host[0] == '[' and host[host.len - 1] == ']') { - const ipv6_part = host[1 .. host.len - 1]; - if (ipv6_part.len == 0) return false; - var has_colon = false; - for (ipv6_part) |c| { - switch (c) { - 'a'...'f', 'A'...'F', '0'...'9' => {}, - ':' => has_colon = true, - else => return false, - } - } - return has_colon; - } - - // Standard hostname/domain validation - for (host) |c| { - switch (c) { - 'a'...'z', 'A'...'Z', '0'...'9', '.', '-' => {}, - else => return false, - } - } - - // No leading/trailing dots or hyphens, no consecutive dots - if (host[0] == '.' or host[0] == '-' or - host[host.len - 1] == '.' or host[host.len - 1] == '-') - { - return false; - } - - return std.mem.indexOf(u8, host, "..") == null; -} - -fn isValidUser(user: []const u8) bool { - if (user.len == 0 or user.len > 64) return false; - for (user) |c| { - switch (c) { - 'a'...'z', 'A'...'Z', '0'...'9', '_', '-', '.' => {}, - else => return false, - } - } - return true; -} - -fn acquireFileLock(file: fs.File) CacheError!void { - _ = file.tryLock(.exclusive) catch { - return CacheError.CacheLocked; - }; -} - -fn readCacheFile( - alloc: Allocator, - path: []const u8, - entries: *std.StringHashMap(CacheEntry), -) !void { - const file = fs.openFileAbsolute(path, .{}) catch |err| switch (err) { - error.FileNotFound => return, - else => return err, - }; - defer file.close(); - - const content = try file.readToEndAlloc(alloc, MAX_CACHE_SIZE); - defer alloc.free(content); - - var lines = std.mem.tokenizeScalar(u8, content, '\n'); - - while (lines.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \t\r"); - - if (CacheEntry.parse(trimmed)) |entry| { - // Always allocate hostname first to avoid key pointer confusion - const hostname_copy = try alloc.dupe(u8, entry.hostname); - errdefer alloc.free(hostname_copy); - - const gop = try entries.getOrPut(hostname_copy); - if (!gop.found_existing) { - const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); - gop.value_ptr.* = .{ - .hostname = hostname_copy, - .timestamp = entry.timestamp, - .terminfo_version = terminfo_copy, - }; - } else { - // Don't need the copy since entry already exists - alloc.free(hostname_copy); - - // Handle duplicate entries - keep newer timestamp - if (entry.timestamp > gop.value_ptr.timestamp) { - gop.value_ptr.timestamp = entry.timestamp; - if (!std.mem.eql(u8, gop.value_ptr.terminfo_version, entry.terminfo_version)) { - alloc.free(gop.value_ptr.terminfo_version); - const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); - gop.value_ptr.terminfo_version = terminfo_copy; - } - } - } - } - } -} - -// Atomic write via temp file + rename, filters out expired entries -fn writeCacheFile( - alloc: Allocator, - path: []const u8, - entries: *const std.StringHashMap(CacheEntry), - expire_days: u32, -) !void { - // Ensure parent directory exists - const dir = std.fs.path.dirname(path).?; - fs.makeDirAbsolute(dir) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; - - // Write to temp file first - const tmp_path = try std.fmt.allocPrint(alloc, "{s}.tmp", .{path}); - defer alloc.free(tmp_path); - - const tmp_file = try fs.createFileAbsolute(tmp_path, .{ .mode = 0o600 }); - defer tmp_file.close(); - errdefer fs.deleteFileAbsolute(tmp_path) catch {}; - - const writer = tmp_file.writer(); - - // Only write non-expired entries - var iter = entries.iterator(); - while (iter.next()) |kv| { - if (!kv.value_ptr.isExpired(expire_days)) { - try kv.value_ptr.format(writer); - } - } - - // Atomic replace - try fs.renameAbsolute(tmp_path, path); -} - -fn checkHost(alloc: Allocator, host: []const u8) !bool { - if (!isValidCacheKey(host)) return CacheError.InvalidCacheKey; - - const path = try getCachePath(alloc); - - var entries = std.StringHashMap(CacheEntry).init(alloc); - - try readCacheFile(alloc, path, &entries); - return entries.contains(host); -} - -fn addHost(alloc: Allocator, host: []const u8) !AddResult { - if (!isValidCacheKey(host)) return CacheError.InvalidCacheKey; - - const path = try getCachePath(alloc); - - // Create cache directory if needed - const dir = std.fs.path.dirname(path).?; - fs.makeDirAbsolute(dir) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; - - // Open or create cache file with secure permissions - const file = fs.createFileAbsolute(path, .{ - .read = true, - .truncate = false, - .mode = 0o600, - }) catch |err| switch (err) { - error.PathAlreadyExists => blk: { - const existing_file = fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |open_err| { - return open_err; - }; - - // Verify and fix permissions on existing file - const stat = existing_file.stat() catch |stat_err| { - existing_file.close(); - return stat_err; - }; - - // Ensure file has correct permissions (readable/writable by owner only) - if (stat.mode & 0o777 != 0o600) { - existing_file.chmod(0o600) catch |chmod_err| { - existing_file.close(); - return chmod_err; - }; - } - - break :blk existing_file; - }, - else => return err, - }; - defer file.close(); - - try acquireFileLock(file); - defer file.unlock(); - - var entries = std.StringHashMap(CacheEntry).init(alloc); - - try readCacheFile(alloc, path, &entries); - - // Add or update entry - const gop = try entries.getOrPut(host); - const result = if (!gop.found_existing) blk: { - gop.key_ptr.* = try alloc.dupe(u8, host); - gop.value_ptr.* = .{ - .hostname = gop.key_ptr.*, - .timestamp = std.time.timestamp(), - .terminfo_version = "xterm-ghostty", - }; - break :blk AddResult.added; - } else blk: { - // Update timestamp for existing entry - gop.value_ptr.timestamp = std.time.timestamp(); - break :blk AddResult.updated; - }; - - try writeCacheFile(alloc, path, &entries, NEVER_EXPIRE); - return result; -} - -fn removeHost(alloc: Allocator, host: []const u8) !void { - if (!isValidCacheKey(host)) return CacheError.InvalidCacheKey; - - const path = try getCachePath(alloc); - - const file = fs.openFileAbsolute(path, .{ .mode = .read_write }) catch |err| switch (err) { - error.FileNotFound => return, - else => return err, - }; - defer file.close(); - - try acquireFileLock(file); - defer file.unlock(); - - var entries = std.StringHashMap(CacheEntry).init(alloc); - - try readCacheFile(alloc, path, &entries); - - _ = entries.fetchRemove(host); - - try writeCacheFile(alloc, path, &entries, NEVER_EXPIRE); -} - -fn listHosts(alloc: Allocator, writer: anytype) !void { - const path = try getCachePath(alloc); - - var entries = std.StringHashMap(CacheEntry).init(alloc); - - readCacheFile(alloc, path, &entries) catch |err| switch (err) { - error.FileNotFound => { - try writer.print("No hosts in cache.\n", .{}); - return; - }, - else => return err, - }; - - if (entries.count() == 0) { - try writer.print("No hosts in cache.\n", .{}); - return; - } - - // Sort entries by hostname for consistent output - var items = std.ArrayList(CacheEntry).init(alloc); - defer items.deinit(); - - var iter = entries.iterator(); - while (iter.next()) |kv| { - try items.append(kv.value_ptr.*); - } - - std.mem.sort(CacheEntry, items.items, {}, struct { - fn lessThan(_: void, a: CacheEntry, b: CacheEntry) bool { - return std.mem.lessThan(u8, a.hostname, b.hostname); - } - }.lessThan); - - try writer.print("Cached hosts ({d}):\n", .{items.items.len}); - const now = std.time.timestamp(); - - for (items.items) |entry| { - const age_days = @divTrunc(now - entry.timestamp, std.time.s_per_day); - if (age_days == 0) { - try writer.print(" {s} (today)\n", .{entry.hostname}); - } else if (age_days == 1) { - try writer.print(" {s} (yesterday)\n", .{entry.hostname}); - } else { - try writer.print(" {s} ({d} days ago)\n", .{ entry.hostname, age_days }); - } - } -} - -fn clearCache(alloc: Allocator) !void { - const path = try getCachePath(alloc); - - fs.deleteFileAbsolute(path) catch |err| switch (err) { - error.FileNotFound => {}, - else => return err, - }; -} - /// Manage the SSH terminfo cache for automatic remote host setup. /// /// When SSH integration is enabled with `shell-integration-features = ssh-terminfo`, @@ -407,6 +34,11 @@ fn clearCache(alloc: Allocator) !void { /// Entries older than the expiration period are automatically removed during cache /// operations. By default, entries never expire. /// +/// Only one of `--clear`, `--add`, `--remove`, or `--host` can be specified. +/// If multiple are specified, one of the actions will be executed but +/// it isn't guaranteed which one. This is entirely unsafe so you should split +/// multiple actions into separate commands. +/// /// Examples: /// ghostty +ssh-cache # List all cached hosts /// ghostty +ssh-cache --host=example.com # Check if host is cached @@ -432,34 +64,34 @@ pub fn run(alloc_gpa: Allocator) !u8 { const stdout = std.io.getStdOut().writer(); const stderr = std.io.getStdErr().writer(); + // Setup our disk cache to the standard location + const cache_path = try DiskCache.defaultPath(alloc, "ghostty"); + const cache: DiskCache = .{ .path = cache_path }; + if (opts.clear) { - try clearCache(alloc); + try cache.clear(); try stdout.print("Cache cleared.\n", .{}); return 0; } if (opts.add) |host| { - const result = addHost(alloc, host) catch |err| { - const Error = error{PermissionDenied} || @TypeOf(err); - switch (@as(Error, err)) { - CacheError.InvalidCacheKey => { - try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); - try stderr.print("Expected format: hostname or user@hostname\n", .{}); - return 1; - }, - CacheError.CacheLocked => { - try stderr.print("Error: Cache is busy, try again\n", .{}); - return 1; - }, - error.AccessDenied, error.PermissionDenied => { - try stderr.print("Error: Permission denied\n", .{}); - return 1; - }, - else => { - try stderr.print("Error: Unable to add '{s}' to cache\n", .{host}); - return 1; - }, - } + const result = cache.add(alloc, host) catch |err| switch (err) { + DiskCache.Error.HostnameIsInvalid => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + DiskCache.Error.CacheIsLocked => { + try stderr.print("Error: Cache is busy, try again\n", .{}); + return 1; + }, + else => { + try stderr.print( + "Error: Unable to add '{s}' to cache. Error: {}\n", + .{ host, err }, + ); + return 1; + }, }; switch (result) { @@ -470,50 +102,42 @@ pub fn run(alloc_gpa: Allocator) !u8 { } if (opts.remove) |host| { - removeHost(alloc, host) catch |err| { - const Error = error{PermissionDenied} || @TypeOf(err); - switch (@as(Error, err)) { - CacheError.InvalidCacheKey => { - try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); - try stderr.print("Expected format: hostname or user@hostname\n", .{}); - return 1; - }, - CacheError.CacheLocked => { - try stderr.print("Error: Cache is busy, try again\n", .{}); - return 1; - }, - error.AccessDenied, error.PermissionDenied => { - try stderr.print("Error: Permission denied\n", .{}); - return 1; - }, - else => { - try stderr.print("Error: Unable to remove '{s}' from cache\n", .{host}); - return 1; - }, - } + cache.remove(alloc, host) catch |err| switch (err) { + DiskCache.Error.HostnameIsInvalid => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + DiskCache.Error.CacheIsLocked => { + try stderr.print("Error: Cache is busy, try again\n", .{}); + return 1; + }, + else => { + try stderr.print( + "Error: Unable to remove '{s}' from cache. Error: {}\n", + .{ host, err }, + ); + return 1; + }, }; try stdout.print("Removed '{s}' from cache.\n", .{host}); return 0; } if (opts.host) |host| { - const cached = checkHost(alloc, host) catch |err| { - const Error = error{PermissionDenied} || @TypeOf(err); - switch (@as(Error, err)) { - CacheError.InvalidCacheKey => { - try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); - try stderr.print("Expected format: hostname or user@hostname\n", .{}); - return 1; - }, - error.AccessDenied, error.PermissionDenied => { - try stderr.print("Error: Permission denied\n", .{}); - return 1; - }, - else => { - try stderr.print("Error: Unable to check host '{s}' in cache\n", .{host}); - return 1; - }, - } + const cached = cache.contains(alloc, host) catch |err| switch (err) { + error.HostnameIsInvalid => { + try stderr.print("Error: Invalid hostname format '{s}'\n", .{host}); + try stderr.print("Expected format: hostname or user@hostname\n", .{}); + return 1; + }, + else => { + try stderr.print( + "Error: Unable to check host '{s}' in cache. Error: {}\n", + .{ host, err }, + ); + return 1; + }, }; if (cached) { @@ -532,224 +156,53 @@ pub fn run(alloc_gpa: Allocator) !u8 { } // Default action: list all hosts - try listHosts(alloc, stdout); + var entries = try cache.list(alloc); + defer DiskCache.deinitEntries(alloc, &entries); + try listEntries(alloc, &entries, stdout); return 0; } -// Tests -test "hostname validation - valid cases" { - const testing = std.testing; - try testing.expect(isValidHostname("example.com")); - try testing.expect(isValidHostname("sub.example.com")); - try testing.expect(isValidHostname("host-name.domain.org")); - try testing.expect(isValidHostname("192.168.1.1")); - try testing.expect(isValidHostname("a")); - try testing.expect(isValidHostname("1")); -} - -test "hostname validation - IPv6 addresses" { - const testing = std.testing; - try testing.expect(isValidHostname("[::1]")); - try testing.expect(isValidHostname("[2001:db8::1]")); - try testing.expect(!isValidHostname("[fe80::1%eth0]")); // Interface notation not supported - try testing.expect(!isValidHostname("[]")); // Empty IPv6 - try testing.expect(!isValidHostname("[invalid]")); // No colons -} - -test "hostname validation - invalid cases" { - const testing = std.testing; - try testing.expect(!isValidHostname("")); - try testing.expect(!isValidHostname("host\nname")); - try testing.expect(!isValidHostname(".example.com")); - try testing.expect(!isValidHostname("example.com.")); - try testing.expect(!isValidHostname("host..domain")); - try testing.expect(!isValidHostname("-hostname")); - try testing.expect(!isValidHostname("hostname-")); - try testing.expect(!isValidHostname("host name")); - try testing.expect(!isValidHostname("host_name")); - try testing.expect(!isValidHostname("host@domain")); - try testing.expect(!isValidHostname("host:port")); - - // Too long - const long_host = "a" ** 254; - try testing.expect(!isValidHostname(long_host)); -} - -test "user validation - valid cases" { - const testing = std.testing; - try testing.expect(isValidUser("user")); - try testing.expect(isValidUser("deploy")); - try testing.expect(isValidUser("test-user")); - try testing.expect(isValidUser("user_name")); - try testing.expect(isValidUser("user.name")); - try testing.expect(isValidUser("user123")); - try testing.expect(isValidUser("a")); -} - -test "user validation - complex realistic cases" { - const testing = std.testing; - try testing.expect(isValidUser("git")); - try testing.expect(isValidUser("ubuntu")); - try testing.expect(isValidUser("root")); - try testing.expect(isValidUser("service.account")); - try testing.expect(isValidUser("user-with-dashes")); -} - -test "user validation - invalid cases" { - const testing = std.testing; - try testing.expect(!isValidUser("")); - try testing.expect(!isValidUser("user name")); - try testing.expect(!isValidUser("user@domain")); - try testing.expect(!isValidUser("user:group")); - try testing.expect(!isValidUser("user\nname")); - - // Too long - const long_user = "a" ** 65; - try testing.expect(!isValidUser(long_user)); -} - -test "cache key validation - hostname format" { - const testing = std.testing; - try testing.expect(isValidCacheKey("example.com")); - try testing.expect(isValidCacheKey("sub.example.com")); - try testing.expect(isValidCacheKey("192.168.1.1")); - try testing.expect(isValidCacheKey("[::1]")); - try testing.expect(!isValidCacheKey("")); - try testing.expect(!isValidCacheKey(".invalid.com")); -} - -test "cache key validation - user@hostname format" { - const testing = std.testing; - try testing.expect(isValidCacheKey("user@example.com")); - try testing.expect(isValidCacheKey("deploy@prod.server.com")); - try testing.expect(isValidCacheKey("test-user@192.168.1.1")); - try testing.expect(isValidCacheKey("user_name@host.domain.org")); - try testing.expect(isValidCacheKey("git@github.com")); - try testing.expect(isValidCacheKey("ubuntu@[::1]")); - try testing.expect(!isValidCacheKey("@example.com")); - try testing.expect(!isValidCacheKey("user@")); - try testing.expect(!isValidCacheKey("user@@host")); - try testing.expect(!isValidCacheKey("user@.invalid.com")); -} - -test "cache entry expiration" { - const testing = std.testing; - const now = std.time.timestamp(); - - const fresh_entry = CacheEntry{ - .hostname = "test.com", - .timestamp = now - std.time.s_per_day, // 1 day old - .terminfo_version = "xterm-ghostty", - }; - try testing.expect(!fresh_entry.isExpired(90)); - - const old_entry = CacheEntry{ - .hostname = "old.com", - .timestamp = now - (std.time.s_per_day * 100), // 100 days old - .terminfo_version = "xterm-ghostty", - }; - try testing.expect(old_entry.isExpired(90)); - - // Test never-expire case - try testing.expect(!old_entry.isExpired(NEVER_EXPIRE)); -} - -test "cache entry expiration - boundary cases" { - const testing = std.testing; - const now = std.time.timestamp(); - - // Exactly at expiration boundary - const boundary_entry = CacheEntry{ - .hostname = "boundary.com", - .timestamp = now - (std.time.s_per_day * 30), // Exactly 30 days old - .terminfo_version = "xterm-ghostty", - }; - try testing.expect(!boundary_entry.isExpired(30)); // Should not be expired - try testing.expect(boundary_entry.isExpired(29)); // Should be expired -} - -test "cache entry parsing - valid formats" { - const testing = std.testing; - - const entry = CacheEntry.parse("example.com|1640995200|xterm-ghostty").?; - try testing.expectEqualStrings("example.com", entry.hostname); - try testing.expectEqual(@as(i64, 1640995200), entry.timestamp); - try testing.expectEqualStrings("xterm-ghostty", entry.terminfo_version); - - // Test default terminfo version - const entry_no_version = CacheEntry.parse("test.com|1640995200").?; - try testing.expectEqualStrings("xterm-ghostty", entry_no_version.terminfo_version); - - // Test complex hostnames - const complex_entry = CacheEntry.parse("user@server.example.com|1640995200|xterm-ghostty").?; - try testing.expectEqualStrings("user@server.example.com", complex_entry.hostname); -} - -test "cache entry parsing - invalid formats" { - const testing = std.testing; - - try testing.expect(CacheEntry.parse("") == null); - try testing.expect(CacheEntry.parse("v1") == null); // Invalid format (no pipe) - try testing.expect(CacheEntry.parse("example.com") == null); // Missing timestamp - try testing.expect(CacheEntry.parse("example.com|invalid") == null); // Invalid timestamp - try testing.expect(CacheEntry.parse("example.com|1640995200|") != null); // Empty terminfo should default -} - -test "cache entry parsing - malformed data resilience" { - const testing = std.testing; - - // Extra pipes should not break parsing - try testing.expect(CacheEntry.parse("host|123|term|extra") != null); - - // Whitespace handling - try testing.expect(CacheEntry.parse(" host|123|term ") != null); - try testing.expect(CacheEntry.parse("\n") == null); - try testing.expect(CacheEntry.parse(" \t \n") == null); -} - -test "duplicate cache entries - memory management" { - const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(testing.allocator); - defer arena.deinit(); - const alloc = arena.allocator(); - - var entries = std.StringHashMap(CacheEntry).init(alloc); - defer entries.deinit(); - - // Simulate reading a cache file with duplicate hostnames - const cache_content = "example.com|1640995200|xterm-ghostty\nexample.com|1640995300|xterm-ghostty-v2\n"; - - var lines = std.mem.tokenizeScalar(u8, cache_content, '\n'); - while (lines.next()) |line| { - const trimmed = std.mem.trim(u8, line, " \t\r"); - if (CacheEntry.parse(trimmed)) |entry| { - const gop = try entries.getOrPut(entry.hostname); - if (!gop.found_existing) { - const hostname_copy = try alloc.dupe(u8, entry.hostname); - const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); - gop.key_ptr.* = hostname_copy; - gop.value_ptr.* = CacheEntry{ - .hostname = hostname_copy, - .timestamp = entry.timestamp, - .terminfo_version = terminfo_copy, - }; - } else { - // Test the duplicate handling logic - if (entry.timestamp > gop.value_ptr.timestamp) { - gop.value_ptr.timestamp = entry.timestamp; - if (!std.mem.eql(u8, gop.value_ptr.terminfo_version, entry.terminfo_version)) { - alloc.free(gop.value_ptr.terminfo_version); - const terminfo_copy = try alloc.dupe(u8, entry.terminfo_version); - gop.value_ptr.terminfo_version = terminfo_copy; - } - } - } - } +fn listEntries( + alloc: Allocator, + entries: *const std.StringHashMap(Entry), + writer: anytype, +) !void { + if (entries.count() == 0) { + try writer.print("No hosts in cache.\n", .{}); + return; } - // Verify only one entry exists with the newer timestamp - try testing.expect(entries.count() == 1); - const entry = entries.get("example.com").?; - try testing.expectEqual(@as(i64, 1640995300), entry.timestamp); - try testing.expectEqualStrings("xterm-ghostty-v2", entry.terminfo_version); + // Sort entries by hostname for consistent output + var items = std.ArrayList(Entry).init(alloc); + defer items.deinit(); + + var iter = entries.iterator(); + while (iter.next()) |kv| { + try items.append(kv.value_ptr.*); + } + + std.mem.sort(Entry, items.items, {}, struct { + fn lessThan(_: void, a: Entry, b: Entry) bool { + return std.mem.lessThan(u8, a.hostname, b.hostname); + } + }.lessThan); + + try writer.print("Cached hosts ({d}):\n", .{items.items.len}); + const now = std.time.timestamp(); + + for (items.items) |entry| { + const age_days = @divTrunc(now - entry.timestamp, std.time.s_per_day); + if (age_days == 0) { + try writer.print(" {s} (today)\n", .{entry.hostname}); + } else if (age_days == 1) { + try writer.print(" {s} (yesterday)\n", .{entry.hostname}); + } else { + try writer.print(" {s} ({d} days ago)\n", .{ entry.hostname, age_days }); + } + } +} + +test { + _ = DiskCache; + _ = Entry; } diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 36ae116d5..469ff2859 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -177,10 +177,28 @@ pub fn setupFeatures( }; var buffer = try std.BoundedArray(u8, capacity).init(0); - inline for (fields) |field| { - if (@field(features, field.name)) { + // Sort the fields so that the output is deterministic. This is + // done at comptime so it has no runtime cost + const fields_sorted: [fields.len][]const u8 = comptime fields: { + var fields_sorted: [fields.len][]const u8 = undefined; + for (fields, 0..) |field, i| fields_sorted[i] = field.name; + std.mem.sortUnstable( + []const u8, + &fields_sorted, + {}, + (struct { + fn lessThan(_: void, lhs: []const u8, rhs: []const u8) bool { + return std.ascii.orderIgnoreCase(lhs, rhs) == .lt; + } + }).lessThan, + ); + break :fields fields_sorted; + }; + + inline for (fields_sorted) |name| { + if (@field(features, name)) { if (buffer.len > 0) try buffer.append(','); - try buffer.appendSlice(field.name); + try buffer.appendSlice(name); } } @@ -220,7 +238,7 @@ test "setup features" { defer env.deinit(); try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false, .@"ssh-env" = true, .@"ssh-terminfo" = false }); - try testing.expectEqualStrings("sudo,ssh-env", env.get("GHOSTTY_SHELL_FEATURES").?); + try testing.expectEqualStrings("ssh-env,sudo", env.get("GHOSTTY_SHELL_FEATURES").?); } } From b915084c38be8ccd7f8de62ce1da1852b2b24dad Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 9 Jul 2025 10:28:35 -0600 Subject: [PATCH 76/86] font/coretext: don't use vertical overlap constraints --- src/font/face/coretext.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index e2d60905d..4e6804eb0 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -343,7 +343,14 @@ pub const Face = struct { const cell_width: f64 = @floatFromInt(metrics.cell_width); // const cell_height: f64 = @floatFromInt(metrics.cell_height); - const glyph_size = opts.constraint.constrain( + // We eliminate any negative vertical padding since these overlap + // values aren't needed under CoreText with how precisely we apply + // constraints, and they can lead to extra height that looks bad + // for things like powerline glyphs. + var constraint = opts.constraint; + constraint.pad_top = @max(0.0, constraint.pad_top); + constraint.pad_bottom = @max(0.0, constraint.pad_bottom); + const glyph_size = constraint.constrain( .{ .width = rect.size.width, .height = rect.size.height, From e68c1d2cad0d0ef685f2b550873bfc471ff2b2f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 09:30:20 -0700 Subject: [PATCH 77/86] config: add available since for SSH shell integration --- src/config/Config.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 4e2f2d84b..5ebb5561b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -689,10 +689,10 @@ palette: Palette = .{}, /// other colors at runtime: /// /// * `cell-foreground` - Match the cell foreground color. -/// (Available since version 1.2.0) +/// (Available since: 1.2.0) /// /// * `cell-background` - Match the cell background color. -/// (Available since version 1.2.0) +/// (Available since: 1.2.0) @"cursor-color": ?TerminalColor = null, /// The opacity level (opposite of transparency) of the cursor. A value of 1 @@ -2217,6 +2217,7 @@ keybind: Keybinds = .{}, /// remote hosts and propagates COLORTERM, TERM_PROGRAM, and TERM_PROGRAM_VERSION. /// Whether or not these variables will be accepted by the remote host(s) will /// depend on whether or not the variables are allowed in their sshd_config. +/// (Available since: 1.2.0) /// /// * `ssh-terminfo` - Enable automatic terminfo installation on remote hosts. /// Attempts to install Ghostty's terminfo entry using `infocmp` and `tic` when @@ -2225,6 +2226,7 @@ keybind: Keybinds = .{}, /// remote host, it will be automatically "cached" to avoid repeat installations. /// If desired, the `+ssh-cache` CLI action can be used to manage the installation /// cache manually using various arguments. +/// (Available since: 1.2.0) /// /// SSH features work independently and can be combined for optimal experience: /// when both `ssh-env` and `ssh-terminfo` are enabled, Ghostty will install its From 579b15bef77140bdf6530fe823a4f8a9cc602427 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 9 Jul 2025 10:32:45 -0600 Subject: [PATCH 78/86] font/coretext: rework glyph quantization math The old math didn't allow fractional pixels on the left and bottom, and stretched glyphs vertically since the height was always rounded up. At very small font sizes this looked good, but at medium and even large sizes this just made things inconsistent and janky. These new calculations are practically pixel-identical to whatever CoreText is doing in 99% of cases, and the remaining cases seem to be some sort of auto-hinting since it's internal features of the glyph getting repositioned. Over all, I still prefer this to CoreText's quantize option, but if this causes further issues we should probably just revert the whole thing and go ahead and add an extra pixel of padding to the bottom and left... --- src/font/face/coretext.zig | 116 ++++++++++++++++++++++--------------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 4e6804eb0..bb9a472d2 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -361,48 +361,75 @@ pub const Face = struct { opts.constraint_width, ); - // We manually quantize the position and size of the glyph to whole - // pixel boundaries. Since macOS doesn't do font hinting this helps - // a lot for legibility at small sizes on low dpi displays. + // These calculations are an attempt to mostly imitate the effect of + // `shouldSubpixelQuantizeFonts`[^1], which helps maximize legibility + // at small pixel sizes (low DPI). We do this math ourselves instead + // of letting CoreText do it because it's not entirely clear how the + // math in CoreText works and we've run in to edge cases where glyphs + // have their bottom or left row cut off due to bad rounding. // - // Well, okay, so, it seems like macOS does have a rudimentary auto- - // hinter of sorts, except they call it "subpixel quantization"[^1]. + // This math seems to have a mostly comparable result to whatever it + // is that CoreText does, and is even (in my opinion) better in some + // cases. // - // Why not just use that? Because it's unpredictable and would force - // us to have an extra pixel of padding in the atlas for most glyphs - // that don't need it, since it's hard to know whether a given glyph - // will have its bottom or left edge snapped out an extra pixel. + // I'm not entirely certain but I suspect that when you enable the + // CoreText option it also does some sort of rudimentary hinting, + // but it doesn't seem to make that big of a difference in terms + // of legibility in the end. // - // Also, this empirically just looks a whole lot better than theirs. - // Admittedly this is a very specific use case, we're rendering for - // a monospace grid and don't really have to worry about sub-pixel - // positioning; I'm sure Apple's technique is better for cases with - // proportional text. - // - // An effort was made to more or less match Apple's quantization in - // terms of resulting whole-pixel glyph sizes. Oddly it looks like - // Apple is still horizontally quantizing to thirds of a pixel, as - // if they're doing subpixel rendering for a horizontally striped - // LCD, even though they haven't done subpixel rendering for years. - // We don't match them on that, it tends to just make it blurrier. - // - // [^1]: Well I'm 80% sure it's hinting since it seems to account for - // features inside of the glyph like crossbars, not just the bounding - // box like we do. The documentation is... sparse. Ref: - // https://developer.apple.com/documentation/coregraphics/cgcontext/setshouldsubpixelquantizefonts(_:)?language=objc + // [^1]: https://developer.apple.com/documentation/coregraphics/cgcontext/setshouldsubpixelquantizefonts(_:)?language=objc + + // We only want to apply quantization if we don't have any + // constraints and this isn't a bitmap glyph, since CoreText + // doesn't seem to apply its quantization to bitmap glyphs. // // TODO: Maybe gate this so it only applies at small font sizes, // or else offer a user config option that can disable it. - const x = @round(glyph_size.x); - const y = @round(glyph_size.y); - // We subtract a third here so that we behave (somewhat) like the weird - // one third pixel quantization that Apple does. This is basically just - // a fudge factor though. - const width = @max(1.0, @ceil(glyph_size.width + glyph_size.x - x - 1.0 / 3.0)); - const height = @max(1.0, @ceil(glyph_size.height + glyph_size.y - y)); + const should_quantize = !sbix and std.meta.eql(opts.constraint, .none); - const px_width: u32 = @intFromFloat(@ceil(width)); - const px_height: u32 = @intFromFloat(@ceil(height)); + // We offset our glyph by its bearings when we draw it, using `@floor` + // here rounds it *up* since we negate it right outside. Moving it by + // whole pixels ensures that we don't disturb the pixel alignment of + // the glyph, fractional pixels will still be drawn on all sides as + // necessary. + const draw_x = -@floor(rect.origin.x); + const draw_y = -@floor(rect.origin.y); + + // We use `x` and `y` for our full pixel bearings post-raster. + // We need to subtract the fractional pixel of difference from + // the edge of the draw area to the edge of the actual glyph. + const frac_x = rect.origin.x + draw_x; + const frac_y = rect.origin.y + draw_y; + const x = glyph_size.x - frac_x; + const y = glyph_size.y - frac_y; + + // We never modify the width. + // + // When using the CoreText option the widths do seem to be + // modified extremely subtly, but even at very small font + // sizes it's hardly a noticeable difference. + const width = glyph_size.width; + + // If the top of the glyph (taking in to account the y position) + // is within half a pixel of an exact pixel edge, we round up the + // height, otherwise leave it alone. + // + // This seems to match what CoreText does. + const frac_top = (glyph_size.height + frac_y) - @floor(glyph_size.height + frac_y); + const height = + if (should_quantize) + if (frac_top >= 0.5) + glyph_size.height + 1 - frac_top + else + glyph_size.height + else + glyph_size.height; + + // Add the fractional pixel to the width and height and take + // the ceiling to get a canvas size that will definitely fit + // our drawn glyph. + const px_width: u32 = @intFromFloat(@ceil(width + frac_x)); + const px_height: u32 = @intFromFloat(@ceil(height + frac_y)); // Settings that are specific to if we are rendering text or emoji. const color: struct { @@ -512,13 +539,8 @@ pub const Face = struct { height / rect.size.height, ); - // We want to render the glyphs at (0,0), but the glyphs themselves - // are offset by bearings, so we have to undo those bearings in order - // to get them to 0,0. - self.font.drawGlyphs(&glyphs, &.{.{ - .x = -rect.origin.x, - .y = -rect.origin.y, - }}, ctx); + // Draw our glyph. + self.font.drawGlyphs(&glyphs, &.{.{ .x = draw_x, .y = draw_y }}, ctx); // Write our rasterized glyph to the atlas. const region = try atlas.reserve(alloc, px_width, px_height); @@ -526,7 +548,7 @@ pub const Face = struct { // This should be the distance from the bottom of // the cell to the top of the glyph's bounding box. - const offset_y: i32 = @as(i32, @intFromFloat(@ceil(y + height))); + const offset_y: i32 = @as(i32, @intFromFloat(@round(y))) + @as(i32, @intCast(px_height)); // This should be the distance from the left of // the cell to the left of the glyph's bounding box. @@ -545,9 +567,7 @@ pub const Face = struct { // since in that case the position was already calculated with the // new cell width in mind. if (opts.constraint.align_horizontal == .none) { - var advances: [glyphs.len]macos.graphics.Size = undefined; - _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); - const advance = advances[0].width; + const advance = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, null); const new_advance = cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); // If the original advance is greater than the cell width then @@ -559,13 +579,13 @@ pub const Face = struct { // We also don't want to do anything if the advance is zero or // less, since this is used for stuff like combining characters. if (advance > new_advance or advance <= 0.0) { - break :offset_x @intFromFloat(@ceil(x)); + break :offset_x @intFromFloat(@round(x)); } break :offset_x @intFromFloat( @round(x + (new_advance - advance) / 2), ); } else { - break :offset_x @intFromFloat(@ceil(x)); + break :offset_x @intFromFloat(@round(x)); } }; From d8e7a6634ea25a54ebe7e5c2bfe312f5a50b0513 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 10:22:44 -0700 Subject: [PATCH 79/86] build: temporarily disable stderr capture on distcheck --- src/build/GhosttyDist.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index 3d7ba3b8d..25ec7182b 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -115,7 +115,8 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { // Capture stderr so it doesn't spew into the parent build. // On the flip side, if the test fails we won't know why so // that sucks but we should have already ran tests at this point. - _ = step.captureStdErr(); + // NOTE(mitchellh): temporarily disabled to diagnose heisenbug + //_ = step.captureStdErr(); break :step step; }; From 86dbfb98d72726d5cbf918d578e119cdff08e854 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 9 Jul 2025 10:42:13 -0700 Subject: [PATCH 80/86] Run GTK unit tests in CI, fix broken tests We only ran `apprt=none` tests in CI, which misses a huge surface area of unit tests. Thankfully only a couple were broken. This fixes that. --- .github/workflows/test.yml | 11 ++++++++++- src/apprt/gtk/gtk_version.zig | 22 +++++++++++++++++++++- src/os/kernel_info.zig | 2 +- src/os/main.zig | 10 ++++++++-- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8af7140c6..834d49a5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -561,7 +561,16 @@ jobs: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: Test GTK Build + - name: Test + run: | + nix develop -c \ + zig build \ + -Dapp-runtime=gtk \ + -Dgtk-x11=${{ matrix.x11 }} \ + -Dgtk-wayland=${{ matrix.wayland }} \ + test + + - name: Build run: | nix develop -c \ zig build \ diff --git a/src/apprt/gtk/gtk_version.zig b/src/apprt/gtk/gtk_version.zig index 5d75fb4fe..6f3d733a5 100644 --- a/src/apprt/gtk/gtk_version.zig +++ b/src/apprt/gtk/gtk_version.zig @@ -103,7 +103,7 @@ pub inline fn runtimeUntil( test "atLeast" { const testing = std.testing; - const funs = &.{ atLeast, runtimeAtLeast, runtimeUntil }; + const funs = &.{ atLeast, runtimeAtLeast }; inline for (funs) |fun| { try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); @@ -118,3 +118,23 @@ test "atLeast" { try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1)); } } + +test "runtimeUntil" { + const testing = std.testing; + + // This is an array in case we add a comptime variant. + const funs = &.{runtimeUntil}; + inline for (funs) |fun| { + try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + + try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1)); + try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION)); + try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + + try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); + try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION)); + try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1)); + + try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1)); + } +} diff --git a/src/os/kernel_info.zig b/src/os/kernel_info.zig index 9e3933dde..e57cc3047 100644 --- a/src/os/kernel_info.zig +++ b/src/os/kernel_info.zig @@ -17,7 +17,7 @@ test "read /proc/sys/kernel/osrelease" { if (comptime builtin.os.tag != .linux) return null; const allocator = std.testing.allocator; - const kernel_info = try getKernelInfo(allocator); + const kernel_info = getKernelInfo(allocator).?; defer allocator.free(kernel_info); // Since we can't hardcode the info in tests, just check diff --git a/src/os/main.zig b/src/os/main.zig index 7398fc779..41dc6aa29 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -2,6 +2,8 @@ //! system. These aren't restricted to syscalls or low-level operations, but //! also OS-specific features and conventions. +const builtin = @import("builtin"); + const dbus = @import("dbus.zig"); const desktop = @import("desktop.zig"); const env = @import("env.zig"); @@ -14,7 +16,7 @@ const openpkg = @import("open.zig"); const pipepkg = @import("pipe.zig"); const resourcesdir = @import("resourcesdir.zig"); const systemd = @import("systemd.zig"); -const kernelInfo = @import("kernel_info.zig"); +const kernel_info = @import("kernel_info.zig"); // Namespaces pub const args = @import("args.zig"); @@ -59,8 +61,12 @@ pub const pipe = pipepkg.pipe; pub const resourcesDir = resourcesdir.resourcesDir; pub const ResourcesDir = resourcesdir.ResourcesDir; pub const ShellEscapeWriter = shell.ShellEscapeWriter; -pub const getKernelInfo = kernelInfo.getKernelInfo; +pub const getKernelInfo = kernel_info.getKernelInfo; test { _ = i18n; + + if (comptime builtin.os.tag == .linux) { + _ = kernel_info; + } } From e18f16d94de50e4d82ff8a9c83b413374be10abb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 4 Jul 2025 14:57:09 -0500 Subject: [PATCH 81/86] linux: add functions for notifying systemd about process state Functions for notifying systemd that we are ready or have started reloading the configuration. --- src/os/systemd.zig | 133 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/src/os/systemd.zig b/src/os/systemd.zig index 9b67296d6..1a9a10ee7 100644 --- a/src/os/systemd.zig +++ b/src/os/systemd.zig @@ -63,3 +63,136 @@ pub fn launchedBySystemd() bool { else => false, }; } + +/// systemd notifications. Used by Ghostty to inform systemd of the state of the +/// process. Currently only used to notify systemd that we are ready and that +/// configuration reloading has started. +/// +/// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html +/// +/// These functions were re-implemented in Zig instead of using the `libsystemd` +/// library to avoid the complexity of another external dependency, as well as +/// to take advantage of Zig features like `comptime` to ensure minimal impact +/// on non-Linux systems (like FreeBSD) that will never support `systemd`. +/// +/// Linux systems that do not use `systemd` should not be impacted as they +/// should never start Ghostty with the `NOTIFY_SOCKET` environment variable set +/// and these functions essentially become a no-op. +/// +/// See `systemd`'s [Interface Portability and Stability Promise](https://systemd.io/PORTABILITY_AND_STABILITY/) +/// for assurances that the interfaces used here will be supported and stable for +/// the long term. +pub const notify = struct { + /// Send the given message to the UNIX socket specified in the NOTIFY_SOCKET + /// environment variable. If there NOTIFY_SOCKET environment variable does + /// not exist then no message is sent. + fn send(message: []const u8) void { + // systemd is Linux-only so this is a no-op anywhere else + if (comptime builtin.os.tag != .linux) return; + + // Get the socket address that should receive notifications. + const socket_path = std.posix.getenv("NOTIFY_SOCKET") orelse return; + + // If the socket address is an empty string return. + if (socket_path.len == 0) return; + + // The socket address must be a path or an abstract socket. + if (socket_path[0] != '/' and socket_path[0] != '@') { + log.warn("only AF_UNIX sockets with path or abstract namespace addresses are supported!", .{}); + return; + } + + var socket_address: std.os.linux.sockaddr.un = undefined; + + // Error out if the supplied socket path is too long. + if (socket_address.path.len < socket_path.len) { + log.warn("NOTIFY_SOCKET path is too long!", .{}); + return; + } + + socket_address.family = std.os.linux.AF.UNIX; + + @memcpy(socket_address.path[0..socket_path.len], socket_path); + socket_address.path[socket_path.len] = 0; + + const socket: std.os.linux.socket_t = socket: { + const rc = std.os.linux.socket( + std.os.linux.AF.UNIX, + std.os.linux.SOCK.DGRAM | std.os.linux.SOCK.CLOEXEC, + 0, + ); + switch (std.os.linux.E.init(rc)) { + .SUCCESS => break :socket @intCast(rc), + else => |e| { + log.warn("creating socket failed: {s}", .{@tagName(e)}); + return; + }, + } + }; + + defer _ = std.os.linux.close(socket); + + connect: { + const rc = std.os.linux.connect( + socket, + &socket_address, + @offsetOf(std.os.linux.sockaddr.un, "path") + socket_address.path.len, + ); + switch (std.os.linux.E.init(rc)) { + .SUCCESS => break :connect, + else => |e| { + log.warn("unable to connect to notify socket: {s}", .{@tagName(e)}); + return; + }, + } + } + + write: { + const rc = std.os.linux.write(socket, message.ptr, message.len); + switch (std.os.linux.E.init(rc)) { + .SUCCESS => { + const written = rc; + if (written < message.len) { + log.warn("short write to notify socket: {d} < {d}", .{ rc, message.len }); + return; + } + break :write; + }, + else => |e| { + log.warn("unable to write to notify socket: {s}", .{@tagName(e)}); + return; + }, + } + } + } + + /// Tell systemd that we are ready or that we are finished reloading. + /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#READY=1 + pub fn ready() void { + if (comptime builtin.os.tag != .linux) return; + + send("READY=1"); + } + + /// Tell systemd that we have started reloading our configuration. + /// See: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#RELOADING=1 + /// and: https://www.freedesktop.org/software/systemd/man/latest/sd_notify.html#MONOTONIC_USEC=%E2%80%A6 + pub fn reloading() void { + if (comptime builtin.os.tag != .linux) return; + + const ts = std.posix.clock_gettime(.MONOTONIC) catch |err| { + log.err("unable to get MONOTONIC clock: {}", .{err}); + return; + }; + + const now = ts.sec * std.time.us_per_s + @divFloor(ts.nsec, std.time.ns_per_us); + + var buffer: [64]u8 = undefined; + const message = std.fmt.bufPrint(&buffer, "RELOADING=1\nMONOTONIC_USEC={d}", .{now}) catch |err| { + log.err("unable to format reloading message: {}", .{err}); + return; + }; + + send(message); + } +}; From c9d0bbefc2a2e5152996113a78315fc723b155ec Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 4 Jul 2025 15:01:54 -0500 Subject: [PATCH 82/86] linux: switch systemd user service to type=notify-reload This allows `systemctl` to send SIGUSR2 to Ghostty to trigger a reload, which is more convenient than scripting `ps` and `kill` to find the Ghostty main PID. --- dist/linux/systemd.service.in | 5 ++++- src/apprt/gtk/App.zig | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/dist/linux/systemd.service.in b/dist/linux/systemd.service.in index 3ff848ddd..76ccdd3f4 100644 --- a/dist/linux/systemd.service.in +++ b/dist/linux/systemd.service.in @@ -1,9 +1,12 @@ [Unit] Description=@NAME@ After=graphical-session.target +After=dbus.socket +Requires=dbus.socket [Service] -Type=dbus +Type=notify-reload +ReloadSignal=SIGUSR2 BusName=@APPID@ ExecStart=@GHOSTTY@ --launched-from=systemd diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 907f3a36d..bdb2f0f24 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -29,6 +29,7 @@ const apprt = @import("../../apprt.zig"); const configpkg = @import("../../config.zig"); const input = @import("../../input.zig"); const internal_os = @import("../../os/main.zig"); +const systemd = @import("../../os/systemd.zig"); const terminal = @import("../../terminal/main.zig"); const Config = configpkg.Config; const CoreApp = @import("../../App.zig"); @@ -1035,6 +1036,12 @@ pub fn reloadConfig( target: apprt.action.Target, opts: apprt.action.ReloadConfig, ) !void { + // Tell systemd that reloading has started. + systemd.notify.reloading(); + + // When we exit this function tell systemd that reloading has finished. + defer systemd.notify.ready(); + if (opts.soft) { switch (target) { .app => try self.core_app.updateConfig(self, &self.config), @@ -1367,6 +1374,9 @@ pub fn run(self: *App) !void { log.warn("error handling configuration changes err={}", .{err}); }; + // Tell systemd that we are ready. + systemd.notify.ready(); + while (self.running) { _ = glib.MainContext.iteration(self.ctx, 1); From 248acbea5b5f8988b3e5cd44538b756025962bf6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 9 Jul 2025 12:29:45 -0500 Subject: [PATCH 83/86] gtk: remove NOTIFY_SOCKET from the inherited environment variables --- src/apprt/gtk/Surface.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 5c886e663..d16083d5a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2333,6 +2333,7 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { env.remove("DBUS_STARTER_BUS_TYPE"); env.remove("INVOCATION_ID"); env.remove("JOURNAL_STREAM"); + env.remove("NOTIFY_SOCKET"); // Unset environment varies set by snaps if we're running in a snap. // This allows Ghostty to further launch additional snaps. From a1cb52dcd35794c61dc7fb583f25284b7b6347d5 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 9 Jul 2025 14:29:46 -0400 Subject: [PATCH 84/86] fish: prefer 'command -q' to check for commands This is a fish built-in 'command' option that's the more idiomatic way to check for the availability of a command. https://fishshell.com/docs/current/cmds/command.html --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 0bba43b31..546f05fc8 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -124,9 +124,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" if test -n "$ssh_hostname" # Check if terminfo is already cached - if command -v ghostty >/dev/null 2>&1; and ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + if command -q ghostty; and ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 set ssh_term "xterm-ghostty" - else if command -v infocmp >/dev/null 2>&1 + else if command -q infocmp set -l ssh_terminfo set -l ssh_cpath_dir set -l ssh_cpath @@ -149,7 +149,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set -a ssh_opts -o "ControlPath=$ssh_cpath" # Cache successful installation - if test -n "$ssh_target"; and command -v ghostty >/dev/null 2>&1 + if test -n "$ssh_target"; and command -q ghostty ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true end else From e522d54d7b715dfd34fd9e8e36cd9d42d1527ef6 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 9 Jul 2025 15:59:59 -0400 Subject: [PATCH 85/86] shell-integration: simplify "ssh target" checks This value is always set to a non-empty string, and we only need this value after we've determined that 'ssh_hostname' is non-empty. In bash and zsh, we also don't need to check for the 'ghostty' command before we attempt to add the target to the cache. That command will safely fail silently if it's not available. --- src/shell-integration/bash/ghostty.bash | 8 +++----- src/shell-integration/elvish/lib/ghostty-integration.elv | 6 +++--- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 2 +- src/shell-integration/zsh/ghostty-integration | 8 +++----- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 63255bbc3..5b338b11e 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -120,9 +120,9 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break done < <(builtin command ssh -G "$@" 2>/dev/null) - builtin local ssh_target="${ssh_user}@${ssh_hostname}" - if [[ -n "$ssh_hostname" ]]; then + builtin local ssh_target="${ssh_user}@${ssh_hostname}" + # Check if terminfo is already cached if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" @@ -147,9 +147,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation - if [[ -n "$ssh_target" ]] && builtin command -v ghostty >/dev/null 2>&1; then - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - fi + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else builtin echo "Warning: Failed to install terminfo." >&2 fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 2eadbfd06..4e95b251f 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -137,9 +137,9 @@ } } - var ssh-target = $ssh-user"@"$ssh-hostname - if (not-eq $ssh-hostname "") { + var ssh-target = $ssh-user"@"$ssh-hostname + # Check if terminfo is already cached if (and (has-external ghostty) (bool ?(external ghostty +ssh-cache --host=$ssh-target >/dev/null 2>&1))) { set ssh-term = "xterm-ghostty" @@ -167,7 +167,7 @@ set ssh-opts = (conj $ssh-opts -o ControlPath=$ssh-cpath) # Cache successful installation - if (and (not-eq $ssh-target "") (has-external ghostty)) { + if (has-external ghostty) { external ghostty +ssh-cache --add=$ssh-target >/dev/null 2>&1 } } else { diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 546f05fc8..5381f834b 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -149,7 +149,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set -a ssh_opts -o "ControlPath=$ssh_cpath" # Cache successful installation - if test -n "$ssh_target"; and command -q ghostty + if command -q ghostty ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true end else diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 60101416e..f3fb46180 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -272,9 +272,9 @@ _ghostty_deferred_init() { [[ -n "$ssh_user" && -n "$ssh_hostname" ]] && break done < <(command ssh -G "$@" 2>/dev/null) - local ssh_target="${ssh_user}@${ssh_hostname}" - if [[ -n "$ssh_hostname" ]]; then + local ssh_target="${ssh_user}@${ssh_hostname}" + # Check if terminfo is already cached if (( $+commands[ghostty] )) && ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" @@ -299,9 +299,7 @@ _ghostty_deferred_init() { ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation - if [[ -n "$ssh_target" ]] && (( $+commands[ghostty] )); then - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true - fi + ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else print "Warning: Failed to install terminfo." >&2 fi From f5f2a4dd20642d7ca1d3f380349eb83762f1eb7e Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Wed, 9 Jul 2025 17:25:34 -0400 Subject: [PATCH 86/86] shell-integration: use $GHOSTTY_BIN_DIR/ghostty Locate our ghostty binary using $GHOSTTY_BIN_DIR rather than searching the PATH. --- src/shell-integration/bash/ghostty.bash | 4 ++-- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 10 +++++----- src/shell-integration/zsh/ghostty-integration | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 5b338b11e..aacf37c3a 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -124,7 +124,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then builtin local ssh_target="${ssh_user}@${ssh_hostname}" # Check if terminfo is already cached - if ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then + if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif builtin command -v infocmp >/dev/null 2>&1; then builtin local ssh_terminfo ssh_cpath_dir ssh_cpath @@ -147,7 +147,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else builtin echo "Warning: Failed to install terminfo." >&2 fi diff --git a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish index 5381f834b..834f0ef10 100644 --- a/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish +++ b/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish @@ -120,11 +120,11 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - set -l ssh_target "$ssh_user@$ssh_hostname" - if test -n "$ssh_hostname" + set -l ssh_target "$ssh_user@$ssh_hostname" + # Check if terminfo is already cached - if command -q ghostty; and ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1 + if test -x "$GHOSTTY_BIN_DIR/ghostty"; and "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1 set ssh_term "xterm-ghostty" else if command -q infocmp set -l ssh_terminfo @@ -149,8 +149,8 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set -a ssh_opts -o "ControlPath=$ssh_cpath" # Cache successful installation - if command -q ghostty - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true + if test -x "$GHOSTTY_BIN_DIR/ghostty" + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1; or true end else echo "Warning: Failed to install terminfo." >&2 diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index f3fb46180..8607664a2 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -276,7 +276,7 @@ _ghostty_deferred_init() { local ssh_target="${ssh_user}@${ssh_hostname}" # Check if terminfo is already cached - if (( $+commands[ghostty] )) && ghostty +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then + if "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --host="$ssh_target" >/dev/null 2>&1; then ssh_term="xterm-ghostty" elif (( $+commands[infocmp] )); then local ssh_terminfo ssh_cpath_dir ssh_cpath @@ -299,7 +299,7 @@ _ghostty_deferred_init() { ssh_opts+=(-o "ControlPath=$ssh_cpath") # Cache successful installation - ghostty +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true + "$GHOSTTY_BIN_DIR/ghostty" +ssh-cache --add="$ssh_target" >/dev/null 2>&1 || true else print "Warning: Failed to install terminfo." >&2 fi