From 314d52ac3a122b79d8d7db92c356b7ea8e0ed9e5 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sat, 22 Mar 2025 08:28:56 -0400 Subject: [PATCH 001/642] shell-integration: switch to $GHOSTTY_SHELL_FEATURES This change consolidates all three opt-out shell integration environment variables into a single opt-in $GHOSTTY_SHELL_FEATURES variable. Its value is a comma-delimited list of the enabled shell feature names (e.g. "cursor,title"). $GHOSTTY_SHELL_FEATURES is set at runtime and automatically added to the shell environment. Its value is based on the shell-integration-features configuration option. $GHOSTTY_SHELL_FEATURES is only set when at least one shell feature is enabled. It won't be set when 'shell-integration-features = false'. $GHOSTTY_SHELL_FEATURES lists only the enabled shell feature names. We could have alternatively gone in the opposite direction and listed the disabled features, letting the scripts assume each feature is on by default like we did before, but I think this explicit approach is a little safer and easier to reason about / debug. It also doesn't support the "no-" negation prefix used by the config system (e.g. "cursor,no-title"). This simplifies the implementation requirements of our (multiple) shell integration scripts, and because $GHOSTTY_SHELL_FEATURES is derived from shell-integration-features, the user-facing configuration interface retains that expressiveness. $GHOSTTY_SHELL_FEATURES is intended to primarily be an internal concern: an interface between the runtime and our shell integration scripts. It could be used by people with particular use cases who want to manually source those scripts, but that isn't the intended audience. ... and because the previous $GHOSTTY_SHELL_INTEGRATION_NO_* variables were also meant to be an internal concern, this change does not include backwards compatibility support for those names. One last advantage of a using a single $GHOSTTY_SHELL_FEATURES variable is that it can be easily forwarded to e.g. ssh sessions or other shell environments. --- src/shell-integration/bash/ghostty.bash | 8 ++--- .../elvish/lib/ghostty-integration.elv | 14 +++++---- .../ghostty-shell-integration.fish | 14 ++++----- src/shell-integration/zsh/ghostty-integration | 6 ++-- src/termio/shell_integration.zig | 29 ++++++++++--------- 5 files changed, 36 insertions(+), 35 deletions(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index 7fae435a3..0cfd41663 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -70,7 +70,7 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then fi # Sudo -if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" && -n "$TERMINFO" ]]; then +if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved. # # This approach supports wrapping a `sudo` alias, but the alias definition @@ -124,13 +124,13 @@ function __ghostty_precmd() { fi # Cursor - if test "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != "1"; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then PS1=$PS1'\[\e[5 q\]' PS0=$PS0'\[\e[0 q\]' fi # Title (working directory) - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then PS1=$PS1'\[\e]2;\w\a\]' fi fi @@ -161,7 +161,7 @@ function __ghostty_preexec() { PS2="$_GHOSTTY_SAVE_PS2" # Title (current command) - if [[ -n $cmd && "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then + if [[ -n $cmd && "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then builtin printf "\e]2;%s\a" "${cmd//[[:cntrl:]]}" fi diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 08fe42f3f..7d0bc2d83 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -36,6 +36,8 @@ } { + use str + # helper used by `mark-*` functions fn set-prompt-state {|new| set-env __ghostty_prompt_state $new } @@ -104,20 +106,20 @@ set edit:after-readline = (conj $edit:after-readline $mark-output-start~) set edit:after-command = (conj $edit:after-command $mark-output-end~) - var no-title = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_TITLE) - var no-cursor = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_CURSOR) - var no-sudo = (eq 1 $E:GHOSTTY_SHELL_INTEGRATION_NO_SUDO) + var title = (str:contains $E:GHOSTTY_SHELL_FEATURES "title") + var cursor = (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") + var sudo = (str:contains $E:GHOSTTY_SHELL_FEATURES "sudo") - if (not $no-title) { + if $title { set after-chdir = (conj $after-chdir {|_| report-pwd }) } - if (not $no-cursor) { + if $cursor { fn beam { printf "\e[5 q" } fn block { printf "\e[0 q" } set edit:before-readline = (conj $edit:before-readline $beam~) set edit:after-readline = (conj $edit:after-readline {|_| block }) } - if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { + if (and $sudo (not-eq "" $E:TERMINFO) (has-external sudo)) { edit:add-var sudo~ $sudo-with-terminfo~ } } 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 cd4f56105..770c8c781 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 @@ -49,10 +49,10 @@ status --is-interactive || ghostty_exit function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" functions -e __ghostty_setup - # Check if we are setting cursors - set --local no_cursor "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" + set --local cursor string match -q "*cursor*" "$GHOSTTY_SHELL_FEATURES" + set --local sudo string match -q "*sudo*" "$GHOSTTY_SHELL_FEATURES" - if test -z $no_cursor + if $cursor # Change the cursor to a beam on prompt. function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" echo -en "\e[5 q" @@ -62,13 +62,9 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" end end - # Check if we are setting sudo - set --local no_sudo "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" - # When using sudo shell integration feature, ensure $TERMINFO is set # and `sudo` is not already a function or alias - if test -z $no_sudo - and test -n "$TERMINFO"; and test "file" = (type -t sudo 2> /dev/null; or echo "x") + if $sudo 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" @@ -125,7 +121,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set --global fish_handle_reflow 1 # Initial calls for first prompt - if test -z $no_cursor + if $cursor __ghostty_set_cursor_beam end __ghostty_mark_prompt_start diff --git a/src/shell-integration/zsh/ghostty-integration b/src/shell-integration/zsh/ghostty-integration index 9eebe1a30..c1329683e 100644 --- a/src/shell-integration/zsh/ghostty-integration +++ b/src/shell-integration/zsh/ghostty-integration @@ -194,7 +194,7 @@ _ghostty_deferred_init() { _ghostty_report_pwd" _ghostty_report_pwd - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"title"* ]]; then # Enable terminal title changes. functions[_ghostty_precmd]+=" builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(%):-%(4~|…/%3~|%~)}\"\$'\\a'" @@ -202,7 +202,7 @@ _ghostty_deferred_init() { builtin print -rnu $_ghostty_fd \$'\\e]2;'\"\${(V)1}\"\$'\\a'" fi - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_CURSOR" != 1 ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"cursor"* ]]; then # Enable cursor shape changes depending on the current keymap. # This implementation leaks blinking block cursor into external commands # executed from zle. For example, users of fzf-based widgets may find @@ -221,7 +221,7 @@ _ghostty_deferred_init() { fi # Sudo - if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" ]] && [[ -n "$TERMINFO" ]]; then + if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* ]] && [[ -n "$TERMINFO" ]]; then # Wrap `sudo` command to ensure Ghostty terminfo is preserved sudo() { builtin local sudo_has_sudoedit_flags="no" diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 4bbf0a3b5..d87762dbc 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -150,9 +150,18 @@ pub fn setupFeatures( env: *EnvMap, features: config.ShellIntegrationFeatures, ) !void { - if (!features.cursor) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR", "1"); - if (!features.sudo) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_SUDO", "1"); - if (!features.title) try env.put("GHOSTTY_SHELL_INTEGRATION_NO_TITLE", "1"); + var enabled = try std.BoundedArray(u8, 256).init(0); + + inline for (@typeInfo(@TypeOf(features)).@"struct".fields) |f| { + if (@field(features, f.name)) { + if (enabled.len > 0) try enabled.append(','); + try enabled.appendSlice(f.name); + } + } + + if (enabled.len > 0) { + try env.put("GHOSTTY_SHELL_FEATURES", enabled.slice()); + } } test "setup features" { @@ -162,15 +171,13 @@ test "setup features" { defer arena.deinit(); const alloc = arena.allocator(); - // Test: all features enabled (no environment variables should be set) + // Test: all features enabled { var env = EnvMap.init(alloc); defer env.deinit(); try setupFeatures(&env, .{ .cursor = true, .sudo = true, .title = true }); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR") == null); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE") == null); + try testing.expectEqualStrings("cursor,sudo,title", env.get("GHOSTTY_SHELL_FEATURES").?); } // Test: all features disabled @@ -179,9 +186,7 @@ test "setup features" { defer env.deinit(); try setupFeatures(&env, .{ .cursor = false, .sudo = false, .title = false }); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO").?); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?); + try testing.expect(env.get("GHOSTTY_SHELL_FEATURES") == null); } // Test: mixed features @@ -190,9 +195,7 @@ test "setup features" { defer env.deinit(); try setupFeatures(&env, .{ .cursor = false, .sudo = true, .title = false }); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_CURSOR").?); - try testing.expect(env.get("GHOSTTY_SHELL_INTEGRATION_NO_SUDO") == null); - try testing.expectEqualStrings("1", env.get("GHOSTTY_SHELL_INTEGRATION_NO_TITLE").?); + try testing.expectEqualStrings("sudo", env.get("GHOSTTY_SHELL_FEATURES").?); } } From 77dc5c9dd2b11501950ecdaafe5f0cebd3732d5d Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sat, 22 Mar 2025 12:11:51 -0400 Subject: [PATCH 002/642] shell-integration: use fish's native list type Instead of looking for individual substrings in $GHOSTTY_SHELL_FEATURES, `string split` it into a list of feature names and use `contains` to detect their presence. --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 9 ++++----- 1 file changed, 4 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 770c8c781..35cea144a 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 @@ -49,10 +49,9 @@ status --is-interactive || ghostty_exit function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" functions -e __ghostty_setup - set --local cursor string match -q "*cursor*" "$GHOSTTY_SHELL_FEATURES" - set --local sudo string match -q "*sudo*" "$GHOSTTY_SHELL_FEATURES" + set --local features (string split , $GHOSTTY_SHELL_FEATURES) - if $cursor + if contains cursor $features # Change the cursor to a beam on prompt. function __ghostty_set_cursor_beam --on-event fish_prompt -d "Set cursor shape" echo -en "\e[5 q" @@ -64,7 +63,7 @@ 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 $sudo 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" @@ -121,7 +120,7 @@ function __ghostty_setup --on-event fish_prompt -d "Setup ghostty integration" set --global fish_handle_reflow 1 # Initial calls for first prompt - if $cursor + if contains cursor $features __ghostty_set_cursor_beam end __ghostty_mark_prompt_start From 36ff70eb7ff693684fd2f74ec55f7f161e9f477e Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sat, 22 Mar 2025 12:26:56 -0400 Subject: [PATCH 003/642] shell-integration: use elvish's native list type Instead of looking for individual substrings in $GHOSTTY_SHELL_FEATURES, `str:split` it into a list of feature names and use `has-value` to detect their presence. --- .../elvish/lib/ghostty-integration.elv | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 7d0bc2d83..b0de19d3f 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -106,20 +106,18 @@ set edit:after-readline = (conj $edit:after-readline $mark-output-start~) set edit:after-command = (conj $edit:after-command $mark-output-end~) - var title = (str:contains $E:GHOSTTY_SHELL_FEATURES "title") - var cursor = (str:contains $E:GHOSTTY_SHELL_FEATURES "cursor") - var sudo = (str:contains $E:GHOSTTY_SHELL_FEATURES "sudo") + var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] - if $title { + if (has-value $features title) { set after-chdir = (conj $after-chdir {|_| report-pwd }) } - if $cursor { + if (has-value $features cursor) { fn beam { printf "\e[5 q" } fn block { printf "\e[0 q" } set edit:before-readline = (conj $edit:before-readline $beam~) set edit:after-readline = (conj $edit:after-readline {|_| block }) } - if (and $sudo (not-eq "" $E:TERMINFO) (has-external sudo)) { + if (and (has-value $features sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) { edit:add-var sudo~ $sudo-with-terminfo~ } } From 0caba3e19f6d656e57a74c2d0ea899133305cd5c Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sat, 22 Mar 2025 15:54:48 -0400 Subject: [PATCH 004/642] shell-integration: comptime buffer capacity --- src/termio/shell_integration.zig | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index d87762dbc..c02351801 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -150,17 +150,23 @@ pub fn setupFeatures( env: *EnvMap, features: config.ShellIntegrationFeatures, ) !void { - var enabled = try std.BoundedArray(u8, 256).init(0); + const fields = @typeInfo(@TypeOf(features)).@"struct".fields; + const capacity: usize = capacity: { + comptime var n: usize = fields.len - 1; // commas + inline for (fields) |field| n += field.name.len; + break :capacity n; + }; + var buffer = try std.BoundedArray(u8, capacity).init(0); - inline for (@typeInfo(@TypeOf(features)).@"struct".fields) |f| { - if (@field(features, f.name)) { - if (enabled.len > 0) try enabled.append(','); - try enabled.appendSlice(f.name); + inline for (fields) |field| { + if (@field(features, field.name)) { + if (buffer.len > 0) try buffer.append(','); + try buffer.appendSlice(field.name); } } - if (enabled.len > 0) { - try env.put("GHOSTTY_SHELL_FEATURES", enabled.slice()); + if (buffer.len > 0) { + try env.put("GHOSTTY_SHELL_FEATURES", buffer.slice()); } } From cd6b850758b6c19aa7f081fee24ea6bd8d5296ea Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Sat, 22 Mar 2025 15:57:04 -0400 Subject: [PATCH 005/642] shell-integration: minor documentation updates --- src/termio/shell_integration.zig | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index c02351801..ae8d5b67c 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -30,7 +30,7 @@ pub const ShellIntegration = struct { command: []const u8, }; -/// Setup the command execution environment for automatic +/// Set up the command execution environment for automatic /// integrated shell integration and return a ShellIntegration /// struct describing the integration. If integration fails /// (shell type couldn't be detected, etc.), this will return null. @@ -144,8 +144,7 @@ test "force shell" { } } -/// Setup shell integration feature environment variables without -/// performing full shell integration setup. +/// Set up the shell integration features environment variable. pub fn setupFeatures( env: *EnvMap, features: config.ShellIntegrationFeatures, From 13b94d995c42a1c042510d26f011d2a5c6852f95 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Sun, 23 Mar 2025 14:41:07 +0100 Subject: [PATCH 006/642] added macedonian translation file --- po/mk_MK.UTF-8.po | 258 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 2 files changed, 259 insertions(+) create mode 100644 po/mk_MK.UTF-8.po diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po new file mode 100644 index 000000000..332fbea60 --- /dev/null +++ b/po/mk_MK.UTF-8.po @@ -0,0 +1,258 @@ +# Macedonian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Andrej Daskalov , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-23 14:17+0100\n" +"Last-Translator: Andrej Daskalov \n" +"Language-Team: Macedonian\n" +"Language: mk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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/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 +msgid "Configuration Errors" +msgstr "Грешки во конфигурацијата" + +#: src/apprt/gtk/ui/1.5/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 +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 +msgid "Reload Configuration" +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 +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: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 +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:90 +msgid "Open Configuration" +msgstr "Отвори конфигурација" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Инспектор на терминал" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "За Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Авторизирај пристап до привремена меморија" + +#: src/apprt/gtk/ui/1.5/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 +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 +msgid "Allow" +msgstr "Дозволи" + +#: src/apprt/gtk/ui/1.5/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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Предупредување: Потенцијално небезбедно вметнување" + +#: src/apprt/gtk/ui/1.5/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 "Копирано во привремена меморија" + +#: 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/Window.zig:200 +msgid "Main Menu" +msgstr "Главно мени" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Прегледај отворени јазичиња" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Конфигурацијата е одново вчитана." + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Развивачи на Ghostty" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 2b3d2c7f5..2278fd875 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -27,6 +27,7 @@ pub const locales = [_][:0]const u8{ "de_DE.UTF-8", "nb_NO.UTF-8", "zh_CN.UTF-8", + "mk_MK.UTF-8", }; /// Set for faster membership lookup of locales. From 7915ef56611c4e0e03fd0640c32eaec8ae697be6 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov Date: Tue, 25 Mar 2025 09:48:51 +0100 Subject: [PATCH 007/642] moved mk locale to bottom of list --- src/os/i18n.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 9fbc04a07..65fd43153 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -26,10 +26,10 @@ const log = std.log.scoped(.i18n); pub const locales = [_][:0]const u8{ "de_DE.UTF-8", "zh_CN.UTF-8", - "mk_MK.UTF-8", "nb_NO.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", + "mk_MK.UTF-8", }; /// Set for faster membership lookup of locales. From 6c3accede891219ffddfbf1a974bd6f522103d27 Mon Sep 17 00:00:00 2001 From: Andrej Daskalov <41647331+andrejdaskalov@users.noreply.github.com> Date: Tue, 25 Mar 2025 09:53:18 +0100 Subject: [PATCH 008/642] remove extra punctuation Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- po/mk_MK.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 332fbea60..5552cc6e4 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -251,7 +251,7 @@ msgstr "⚠️ Извршувате дебаг верзија на Ghostty! Пе #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" -msgstr "Конфигурацијата е одново вчитана." +msgstr "Конфигурацијата е одново вчитана" #: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" From 969839acf30d91f85e88590b4a62f49002ec7ed9 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 2 Apr 2025 14:30:33 +0200 Subject: [PATCH 009/642] gtk(x11): fix blur regions when using >200% scaling See #6957 We were not considering GTK's internal scale factor that converts between "surface coordinates" and actual device coordinates, and that worked fine until the scale factor reached 2x (200%). Since the code is now dependent on the scale factor (which could change at any given moment), we also listen to scale factor changes and then unconditionally call `winproto.syncAppearance`. Even though it's somewhat overkill, I don't expect people to change their scale factor dramatically all the time anyway... --- src/apprt/gtk/Window.zig | 27 +++++++++++++++++++++++++++ src/apprt/gtk/winproto/x11.zig | 17 ++++++++++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d8e64a980..5fcb0d42b 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -281,6 +281,15 @@ pub fn init(self: *Window, app: *App) !void { .detail = "is-active", }, ); + _ = gobject.Object.signals.notify.connect( + self.window, + *Window, + gtkWindowUpdateScaleFactor, + self, + .{ + .detail = "scale-factor", + }, + ); // If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we // need to stick the headerbar into the content box. @@ -784,6 +793,24 @@ fn gtkWindowNotifyIsActive( } } +fn gtkWindowUpdateScaleFactor( + _: *adw.ApplicationWindow, + _: *gobject.ParamSpec, + self: *Window, +) callconv(.c) void { + // On some platforms (namely X11) we need to refresh our appearance when + // the scale factor changes. In theory this could be more fine-grained as + // a full refresh could be expensive, but a) this *should* be rare, and + // b) quite noticeable visual bugs would occur if this is not present. + self.winproto.syncAppearance() catch |err| { + log.err( + "failed to sync appearance after scale factor has been updated={}", + .{err}, + ); + return; + }; +} + // Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab // sends an undefined value. fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 6d6950f74..c2b6bf416 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -219,13 +219,12 @@ pub const Window = struct { pub fn resizeEvent(self: *Window) !void { // The blur region must update with window resizes - const gtk_widget = self.gtk_window.as(gtk.Widget); - self.blur_region.width = gtk_widget.getWidth(); - self.blur_region.height = gtk_widget.getHeight(); try self.syncBlur(); } pub fn syncAppearance(self: *Window) !void { + // The user could have toggled between CSDs and SSDs, + // therefore we need to recalculate the blur region offset. self.blur_region = blur: { // NOTE(pluiedev): CSDs are a f--king mistake. // Please, GNOME, stop this nonsense of making a window ~30% bigger @@ -236,6 +235,11 @@ pub const Window = struct { self.gtk_window.as(gtk.Native).getSurfaceTransform(&x, &y); + // Transform surface coordinates to device coordinates. + const scale: f64 = @floatFromInt(self.gtk_window.as(gtk.Widget).getScaleFactor()); + x *= scale; + y *= scale; + break :blur .{ .x = @intFromFloat(x), .y = @intFromFloat(y), @@ -265,6 +269,13 @@ pub const Window = struct { // and I think it's not really noticeable enough to justify the effort. // (Wayland also has this visual artifact anyway...) + const gtk_widget = self.gtk_window.as(gtk.Widget); + + // Transform surface coordinates to device coordinates. + const scale = self.gtk_window.as(gtk.Widget).getScaleFactor(); + self.blur_region.width = gtk_widget.getWidth() * scale; + self.blur_region.height = gtk_widget.getHeight() * scale; + const blur = self.config.background_blur; log.debug("set blur={}, window xid={}, region={}", .{ blur, From af0004eb52eecdcc57e0901f51368d079dc97611 Mon Sep 17 00:00:00 2001 From: Simon Olofsson Date: Thu, 3 Apr 2025 10:58:08 +0200 Subject: [PATCH 010/642] docs: use Command instead of super for macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command is the name Apple uses for this key and that's printed on the keyboard 😉 --- 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 ecdcee7fc..9cd285d3b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -826,7 +826,7 @@ env: RepeatableStringMap = .{}, link: RepeatableLink = .{}, /// Enable URL matching. URLs are matched on hover with control (Linux) or -/// super (macOS) pressed and open using the default system application for +/// command (macOS) pressed and open using the default system application for /// the linked URL. /// /// The URL matcher is always lowest priority of any configured links (see From e19b5a150a3db28275fddcd65073b706547cef20 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 3 Apr 2025 14:56:09 -0400 Subject: [PATCH 011/642] libghostty: Action CValue should be untagged extern union Fixes #6962 I believe this is an upstream bug (https://github.com/ziglang/zig/issues/23454), where Zig is allowing extern unions to be tagged when created via type reification. This results in a CValue that has an extra trailing byte (the tag). This wasn't causing any noticeable issues for Ghostty for some reason but others using our pattern were seeing issues. And I did confirm that our CValue was indeed tagged and was the wrong byte size. I assume Swift was just ignoring it because it was extra data. I don't know, but we should fix this in general for libghostty. --- src/apprt/action.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 2ddbee524..30cb2fa5e 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -311,7 +311,7 @@ pub const Action = union(Key) { break :cvalue @Type(.{ .@"union" = .{ .layout = .@"extern", - .tag_type = Key, + .tag_type = null, .fields = &union_fields, .decls = &.{}, } }); @@ -323,6 +323,13 @@ pub const Action = union(Key) { value: CValue, }; + comptime { + // For ABI compatibility, we expect that this is our union size. + // At the time of writing, we don't promise ABI compatibility + // so we can change this but I want to be aware of it. + assert(@sizeOf(CValue) == 16); + } + /// Returns the value type for the given key. pub fn Value(comptime key: Key) type { inline for (@typeInfo(Action).@"union".fields) |field| { From f22893395550f82ed80c906b86614f128629eea4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Apr 2025 19:04:33 -0400 Subject: [PATCH 012/642] macos: left mouse click while not focused doesn't encode to pty Fixes #2595 This fixes an issue where a left mouse click on a terminal while not focused would subsequently be encoded to the pty as a mouse event. This is atypical for macOS applications in general and wasn't something we wanted to do. We do, however, want to ensure our terminal gains focus when clicked without focus. Specifically, a split. This matches iTerm2 behavior and is rather nice. We had this behavior before but our logic to make this work before caused the issue this commit is fixing. I also tested this with command+click which is a common macOS shortcut to emit a mouse event without raising the focus of the target window. In this case, we will properly focus the split but will not encode the mouse event to the pty. I think we actually do a _better job_ here tha iTerm2 (but, subjective) because we do encode the pty event properly if the split is focused whereas iTerm2 never does. --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index f04bf1af3..301ef5a9b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -201,7 +201,14 @@ extension Ghostty { self.eventMonitor = NSEvent.addLocalMonitorForEvents( matching: [ // We need keyUp because command+key events don't trigger keyUp. - .keyUp + .keyUp, + + // We need leftMouseDown to determine if we should focus ourselves + // when the app/window isn't in focus. We do this instead of + // "acceptsFirstMouse" because that forces us to also handle the + // event and encode the event to the pty which we want to avoid. + // (Issue 2595) + .leftMouseDown, ] ) { [weak self] event in self?.localEventHandler(event) } @@ -450,11 +457,40 @@ extension Ghostty { case .keyUp: localEventKeyUp(event) + case .leftMouseDown: + localEventLeftMouseDown(event) + default: event } } + private func localEventLeftMouseDown(_ event: NSEvent) -> NSEvent? { + // We only want to process events that are on this window. + guard let window, + event.window != nil, + window == event.window else { return event } + + // The clicked location in this window should be this view. + let location = convert(event.locationInWindow, from: nil) + guard hitTest(location) == self else { return event } + + // We only want to grab focus if either our app or window was + // not focused. + guard !NSApp.isActive || !window.isKeyWindow else { return event } + + // If we're already focused we do nothing + guard !focused else { return event } + + // Make ourselves the first responder + window.makeFirstResponder(self) + + // We have to keep processing the event so that AppKit can properly + // focus the window and dispatch events. If you return nil here then + // nobody gets a windowDidBecomeKey event and so on. + return event + } + private func localEventKeyUp(_ event: NSEvent) -> NSEvent? { // We only care about events with "command" because all others will // trigger the normal responder chain. @@ -620,14 +656,6 @@ extension Ghostty { ghostty_surface_draw(surface); } - override func acceptsFirstMouse(for event: NSEvent?) -> Bool { - // "Override this method in a subclass to allow instances to respond to - // click-through. This allows the user to click on a view in an inactive - // window, activating the view with one click, instead of clicking first - // to make the window active and then clicking the view." - return true - } - override func mouseDown(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) From fe0536aaaf570f6f71f9ba4d476afb89ee90ccbe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 4 Apr 2025 22:08:06 -0400 Subject: [PATCH 013/642] macos: replay control+key events that go to doCommand Fixes #7000 Related to #6909, the same mechanism, but it turns out some control+keys are also handled in this same way (namely control+esc leads to "cancel" by default, which is not what we want). --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 301ef5a9b..c6a3d7629 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -915,7 +915,7 @@ extension Ghostty { // If we are in a keyDown then we don't need to redispatch a command-modded // key event (see docs for this field) so reset this to nil because // `interpretKeyEvents` may dispach it. - self.lastCommandEvent = nil + self.lastPerformKeyEvent = nil self.interpretKeyEvents([translationEvent]) @@ -955,7 +955,8 @@ extension Ghostty { _ = keyAction(GHOSTTY_ACTION_RELEASE, event: event) } - /// Records the timestamp of the last event to performKeyEquivalent that had a command key active. + /// Records the timestamp of the last event to performKeyEquivalent that we need to save. + /// We currently save all commands with command or control set. /// /// For command+key inputs, the AppKit input stack calls performKeyEquivalent to give us a chance /// to handle them first. If we return "false" then it goes through the standard AppKit responder chain. @@ -980,7 +981,7 @@ extension Ghostty { /// The best thing I could find was to store the event timestamp which has decent granularity /// and compare that. To further complicate things, some events are synthetic and have a zero /// timestamp so we have to protect against that. Fun! - var lastCommandEvent: TimeInterval? + var lastPerformKeyEvent: TimeInterval? /// Special case handling for some control keys override func performKeyEquivalent(with event: NSEvent) -> Bool { @@ -1053,23 +1054,24 @@ extension Ghostty { // Ignore all other non-command events. This lets the event continue // through the AppKit event systems. - if (!event.modifierFlags.contains(.command)) { + if (!event.modifierFlags.contains(.command) && + !event.modifierFlags.contains(.control)) { // Reset since we got a non-command event. - lastCommandEvent = nil + lastPerformKeyEvent = nil return false } // If we have a prior command binding and the timestamp matches exactly // then we pass it through to keyDown for encoding. - if let lastCommandEvent { - self.lastCommandEvent = nil - if lastCommandEvent == event.timestamp { + if let lastPerformKeyEvent { + self.lastPerformKeyEvent = nil + if lastPerformKeyEvent == event.timestamp { equivalent = event.characters ?? "" break } } - lastCommandEvent = event.timestamp + lastPerformKeyEvent = event.timestamp return false } @@ -1572,9 +1574,9 @@ extension Ghostty.SurfaceView: NSTextInputClient { override func doCommand(by selector: Selector) { // If we are being processed by performKeyEquivalent with a command binding, // we send it back through the event system so it can be encoded. - if let lastCommandEvent, + if let lastPerformKeyEvent, let current = NSApp.currentEvent, - lastCommandEvent == current.timestamp + lastPerformKeyEvent == current.timestamp { NSApp.sendEvent(current) return From f6ec39a5d8a99c9a62957823f2a3c4e81e323062 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 6 Apr 2025 00:13:30 +0000 Subject: [PATCH 014/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index fb0b8cb1f..086e19dd8 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", - .hash = "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz", + .hash = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index c93f16231..d43bf3d56 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6": { + "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", - "hash": "sha256-nOkH31MQQd2PPdjVpRxBxNQWfR9Exg6nRF/KHgSz3cM=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz", + "hash": "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 407c5da7b..1dc56da50 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6"; + name = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz"; - hash = "sha256-nOkH31MQQd2PPdjVpRxBxNQWfR9Exg6nRF/KHgSz3cM="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz"; + hash = "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index f2cc560cc..b9bdc50d2 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz From 9643e9c7a666e818bb672d642757f8c20d279fc2 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sun, 6 Apr 2025 19:29:58 -0500 Subject: [PATCH 015/642] introduce issue triage template --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 139 +++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 .github/DISCUSSION_TEMPLATE/issue-triage.yml diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml new file mode 100644 index 000000000..42c89c004 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -0,0 +1,139 @@ +labels: ["needs confirmation"] +body: + - type: markdown + attributes: + value: | + > [!IMPORTANT] + > Please read through [the Discussion rules](https://github.com/ghostty-org/ghostty/discussions/6937) and review [the FAQs](https://ghostty.org/docs/help#common-issues-and-solutions) prior to opening a new Discussion. + - type: markdown + attributes: + value: "# Issue Details" + - type: textarea + attributes: + label: Issue Description + description: | + Provide a detailed description of the issue. Include relevant information, such as: + - The feature or configuration option you encounter the issue with. + - The expected behavior. + - The actual behavior (and how it deviates from the expected behavior, if it is not immediately obvious). + - Relevant Ghostty logs or other stacktraces. + - Relevant screenshots, screen recordings, or other supporting media (as needed). + - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR) in your description. + + >[!TIP] + > **Not sure what information to include?** + > Here are some recommendations: + > - **Input issues:** include your keyboard layout, a screenshot of the terminal inspector's logged keystrokes (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. + > - **Font issues:** include the problematic character(s), the output of `ghostty +show-face` for these character(s), and if they work in other applications. + > - **VT issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction. + > - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version. + + placeholder: | + Example: When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.` + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: | + Provide a detailed set of step-by-step instructions for reproducing this issue. + placeholder: | + 1. Open Ghostty. + 2. Connect to a remote server via SSH. + 3. Try to execute `clear`. + 4. Observe `xterm-ghostty` error message above. + validations: + required: true + - type: textarea + attributes: + label: Ghostty Version + description: Paste the output of `ghostty +version` here. + placeholder: | + Ghostty 1.1.3 + + Version + - version: 1.1.3 + - channel: stable + Build Config + - Zig version: 0.13.0 + - build mode : builtin.OptimizeMode.ReleaseFast + - app runtime: apprt.Runtime.none + - font engine: font.main.Backend.coretext + - renderer : renderer.Metal + - libxev : main.Backend.kqueue + render: text + validations: + required: true + - type: input + attributes: + label: OS Version Information + description: | + Please tell us what operating system (name and version) you are using. + placeholder: Ubuntu 24.04.1 (Noble Numbat) + validations: + required: true + - type: dropdown + attributes: + label: (Linux only) Display Server + description: | + If you run Linux, please tell us if you use X11 or Wayland. If you aren't sure, you can determine this by running `[ -z "$WAYLAND_DISPLAY" ] && echo X11 || echo Wayland`. + options: + - X11 + - Wayland + - Other + validations: + required: false + - type: input + attributes: + label: (Linux only) Desktop Environment/Window Manager + description: | + If you run Linux, please tell us what Desktop Environment/Window Manager you are using (include the name and version). + placeholder: GNOME 47.4 + validations: + required: false + - type: textarea + attributes: + label: Ghostty Configuration + description: | + Please paste the output of `ghostty +show-config` here. + placeholder: | + font-family = CommitMono Nerd Font + font-family-bold = CommitMono Nerd Font + font-family-italic = CommitMono Nerd Font + font-family-bold-italic = CommitMono Nerd Font + font-feature = +cv07 + font-size = 16 + font-thicken = true + theme = catppuccin-mocha + render: ini + validations: + required: true + - type: textarea + attributes: + label: Additional Relevant Configuration + description: | + If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide any relevant configuration information here. If you use custom CSS or shaders for Ghostty, please also include them here, if applicable to your issue. + placeholder: | + `tmux.conf` + --- + set -g default-terminal "tmux-256color" + set-option -sa terminal-overrides ",xterm*:Tc" + set -g base-index 1 + setw -g pane-base-index 1 + render: text + validations: + required: false + - type: markdown + attributes: + value: | + # User Acknowledgements + > [!TIP] + > Use the following links to review the existing Ghostty [Discussions](https://github.com/ghostty-org/ghostty/discussions?discussions_q=) and [Issues](https://github.com/ghostty-org/ghostty/issues?q=sort%3Areactions-desc). + - type: checkboxes + attributes: + label: "I acknowledge that:" + options: + - label: I have reviewed the FAQ and confirm that my issue is NOT among them. + required: true + - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an exiting issue or discussion. + required: true From 9144f4db58befb90b3609b9698f0a41213042e4d Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Tue, 8 Apr 2025 00:44:53 +0800 Subject: [PATCH 016/642] Fix macOS shortcut binding for `close_window` action --- .../Terminal/TerminalController.swift | 12 +++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 25 ++++++++++++++++++- macos/Sources/Ghostty/Package.swift | 3 +++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ddc459c5b..f54eb6539 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -91,6 +91,12 @@ class TerminalController: BaseTerminalController { name: Ghostty.Notification.didEqualizeSplits, object: nil ) + center.addObserver( + self, + selector: #selector(onCloseWindow), + name: .ghosttyCloseWindow, + object: nil + ) } required init?(coder: NSCoder) { @@ -842,6 +848,12 @@ class TerminalController: BaseTerminalController { closeTab(self) } + @objc private func onCloseWindow(notification: SwiftUI.Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree?.contains(view: target) ?? false else { return } + closeWindow(self) + } + @objc private func onResetWindowSize(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard surfaceTree?.contains(view: target) ?? false else { return } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 88f8d1dc9..ddb954e04 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -107,7 +107,7 @@ extension Ghostty { deinit { // This will force the didSet callbacks to run which free. self.app = nil - + #if os(macOS) NotificationCenter.default.removeObserver(self) #endif @@ -451,6 +451,9 @@ extension Ghostty { case GHOSTTY_ACTION_CLOSE_TAB: closeTab(app, target: target) + case GHOSTTY_ACTION_CLOSE_WINDOW: + closeWindow(app, target: target) + case GHOSTTY_ACTION_TOGGLE_FULLSCREEN: toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen) @@ -686,6 +689,26 @@ extension Ghostty { } } + private static func closeWindow(_ app: ghostty_app_t, target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("close window does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + + NotificationCenter.default.post( + name: .ghosttyCloseWindow, + object: surfaceView + ) + + default: + assertionFailure() + } + } + private static func toggleFullscreen( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index ca37002b0..cda4b557e 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -248,6 +248,9 @@ extension Notification.Name { /// Close tab static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab") + /// Close window + static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow") + /// Resize the window to a default size. static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize") } From df174a74f83f71118e21b740c85ddad84f6b599b Mon Sep 17 00:00:00 2001 From: Fabian Boehm Date: Mon, 7 Apr 2025 21:20:21 +0200 Subject: [PATCH 017/642] shell-integration: Fix condition for sudo A missing ";" meant the check for $TERMINFO was never executed. --- .../fish/vendor_conf.d/ghostty-shell-integration.fish | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 35cea144a..e7c264e1f 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,7 +63,7 @@ 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" From b64f49a0d7de72dffde02369c81cd94052a2e6db Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Apr 2025 13:31:51 -0600 Subject: [PATCH 018/642] fix(kittygfx): accept commands with no control data This sort of command is treated as valid by Kitty so we should too. In fact, it occurs with the example `send-png` script provided in the docs for the protocol. --- src/terminal/kitty/graphics_command.zig | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index 840949d74..61ba33a4d 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -98,6 +98,12 @@ pub const Parser = struct { self.state = .control_value; }, + // This can be encountered if we have a sequence with no + // control data, only payload data (i.e. "\x1b_G;"). + // + // Kitty treats this as valid so we do as well. + ';' => self.state = .data, + else => try self.accumulateValue(c, .control_key_ignore), }, @@ -1053,6 +1059,21 @@ test "delete command" { try testing.expectEqual(@as(u32, 4), dv.y); } +test "no control data" { + const testing = std.testing; + const alloc = testing.allocator; + var p = Parser.init(alloc); + defer p.deinit(); + + const input = ";QUFBQQ"; + for (input) |c| try p.feed(c); + const command = try p.complete(); + defer command.deinit(alloc); + + try testing.expect(command.control == .transmit); + try testing.expectEqualStrings("AAAA", command.data); +} + test "ignore unknown keys (long)" { const testing = std.testing; const alloc = testing.allocator; From 9808c137969c4f3eaa308c6ae390d76b0fcbebd5 Mon Sep 17 00:00:00 2001 From: Hanna Date: Mon, 7 Apr 2025 16:02:53 -0400 Subject: [PATCH 019/642] refactor: use builtin hostname function --- src/shell-integration/elvish/lib/ghostty-integration.elv | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index b0de19d3f..6c35a21c5 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -75,7 +75,8 @@ } fn report-pwd { - printf "\e]7;file://%s%s\a" (hostname) (pwd) + use platform + printf "\e]7;file://%s%s\a" (platform:hostname) ($pwd) } fn sudo-with-terminfo {|@args| @@ -109,7 +110,7 @@ var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] if (has-value $features title) { - set after-chdir = (conj $after-chdir {|_| report-pwd }) + set after-chdir = (conj $after-chdir {|_| report- }) } if (has-value $features cursor) { fn beam { printf "\e[5 q" } From 77f5fe256064f2f9f10123f507d42d4e847cc843 Mon Sep 17 00:00:00 2001 From: Hanna Date: Mon, 7 Apr 2025 16:09:43 -0400 Subject: [PATCH 020/642] fix: parenthesis are unneeded around builtins --- src/shell-integration/elvish/lib/ghostty-integration.elv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 6c35a21c5..319cffa8d 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -76,7 +76,7 @@ fn report-pwd { use platform - printf "\e]7;file://%s%s\a" (platform:hostname) ($pwd) + printf "\e]7;file://%s%s\a" platform:hostname $pwd } fn sudo-with-terminfo {|@args| From a8f760c6d2a6027d5161cc9ed3cce40526f8446f Mon Sep 17 00:00:00 2001 From: Hanna Date: Mon, 7 Apr 2025 16:10:50 -0400 Subject: [PATCH 021/642] fix: undo accidental replace --- src/shell-integration/elvish/lib/ghostty-integration.elv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index 319cffa8d..bf6477aed 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -110,7 +110,7 @@ var features = [(str:split ',' $E:GHOSTTY_SHELL_FEATURES)] if (has-value $features title) { - set after-chdir = (conj $after-chdir {|_| report- }) + set after-chdir = (conj $after-chdir {|_| report-pwd }) } if (has-value $features cursor) { fn beam { printf "\e[5 q" } From c7635d5f418defdb3f01d99b870dc52208763482 Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:19:38 -0500 Subject: [PATCH 022/642] remove the please Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 42c89c004..cb69df07e 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -112,7 +112,7 @@ body: attributes: label: Additional Relevant Configuration description: | - If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide any relevant configuration information here. If you use custom CSS or shaders for Ghostty, please also include them here, if applicable to your issue. + If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide any relevant configuration information here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | `tmux.conf` --- From ddb85ca1b1a1183163c63088b1804ad7ad961a64 Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:25:54 -0500 Subject: [PATCH 023/642] better discussion & issue callout in the important note Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index cb69df07e..2f5142d09 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -4,7 +4,7 @@ body: attributes: value: | > [!IMPORTANT] - > Please read through [the Discussion rules](https://github.com/ghostty-org/ghostty/discussions/6937) and review [the FAQs](https://ghostty.org/docs/help#common-issues-and-solutions) prior to opening a new Discussion. + > Please read through [the Discussion rules](https://github.com/ghostty-org/ghostty/discussions/6937), review [the FAQs](https://ghostty.org/docs/help#common-issues-and-solutions), and check for both existing [Discussions](https://github.com/ghostty-org/ghostty/discussions?discussions_q=) and [Issues](https://github.com/ghostty-org/ghostty/issues?q=sort%3Areactions-desc) prior to opening a new Discussion. - type: markdown attributes: value: "# Issue Details" From d3b7fe347364184beec6a48ffb39e206ff458ac2 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 7 Apr 2025 15:28:31 -0500 Subject: [PATCH 024/642] make following into these for better readability --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 2f5142d09..8c7d5d507 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -128,7 +128,7 @@ body: value: | # User Acknowledgements > [!TIP] - > Use the following links to review the existing Ghostty [Discussions](https://github.com/ghostty-org/ghostty/discussions?discussions_q=) and [Issues](https://github.com/ghostty-org/ghostty/issues?q=sort%3Areactions-desc). + > Use the these links to review the existing Ghostty [Discussions](https://github.com/ghostty-org/ghostty/discussions?discussions_q=) and [Issues](https://github.com/ghostty-org/ghostty/issues?q=sort%3Areactions-desc). - type: checkboxes attributes: label: "I acknowledge that:" From b3c61d90f3b9a54c1f4e7c5738d54ae95f793c6b Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 7 Apr 2025 20:37:57 -0500 Subject: [PATCH 025/642] add note about commits --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 8c7d5d507..cd19b496b 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -18,7 +18,7 @@ body: - The actual behavior (and how it deviates from the expected behavior, if it is not immediately obvious). - Relevant Ghostty logs or other stacktraces. - Relevant screenshots, screen recordings, or other supporting media (as needed). - - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR) in your description. + - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description. >[!TIP] > **Not sure what information to include?** From 92959bc09c53bafdb8484e0b4491a30512c6f3d3 Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 7 Apr 2025 20:39:22 -0500 Subject: [PATCH 026/642] fix typo Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index cd19b496b..e9f8c587d 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -128,7 +128,7 @@ body: value: | # User Acknowledgements > [!TIP] - > Use the these links to review the existing Ghostty [Discussions](https://github.com/ghostty-org/ghostty/discussions?discussions_q=) and [Issues](https://github.com/ghostty-org/ghostty/issues?q=sort%3Areactions-desc). + > Use these links to review the existing Ghostty [Discussions](https://github.com/ghostty-org/ghostty/discussions?discussions_q=) and [Issues](https://github.com/ghostty-org/ghostty/issues?q=sort%3Areactions-desc). - type: checkboxes attributes: label: "I acknowledge that:" From a6123c044735cefeacfa6da94b1ebe4670a6a2f1 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 7 Apr 2025 20:41:04 -0500 Subject: [PATCH 027/642] fix trailing whitespace --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index e9f8c587d..faac466db 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -18,7 +18,7 @@ body: - The actual behavior (and how it deviates from the expected behavior, if it is not immediately obvious). - Relevant Ghostty logs or other stacktraces. - Relevant screenshots, screen recordings, or other supporting media (as needed). - - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description. + - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description. >[!TIP] > **Not sure what information to include?** From 9440dbba1a48fc606a9b08f86e4dc84e6eec12ef Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 7 Apr 2025 21:25:26 -0500 Subject: [PATCH 028/642] add notes abotu minimum config --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index faac466db..f33f83fb9 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -95,7 +95,7 @@ body: attributes: label: Ghostty Configuration description: | - Please paste the output of `ghostty +show-config` here. + Please provide the minimum conifguration needed to reproduce this issue. If you cannot determine the minimum configuration needed to reproduce the issue, paste the output of `ghostty +show-config` here. placeholder: | font-family = CommitMono Nerd Font font-family-bold = CommitMono Nerd Font @@ -112,7 +112,7 @@ body: attributes: label: Additional Relevant Configuration description: | - If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide any relevant configuration information here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. + If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide any relevant configuration information here. Please provide the minimal configuration needed. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | `tmux.conf` --- From 279a6b6f58ff2e7b42136f99165edb85335ec9c3 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 7 Apr 2025 21:27:01 -0500 Subject: [PATCH 029/642] fix typos & make it read more naturally --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index f33f83fb9..3ed106ce8 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -95,7 +95,7 @@ body: attributes: label: Ghostty Configuration description: | - Please provide the minimum conifguration needed to reproduce this issue. If you cannot determine the minimum configuration needed to reproduce the issue, paste the output of `ghostty +show-config` here. + Please provide the minimum configuration needed to reproduce this issue. If you cannot determine the minimum configuration needed to reproduce the issue, paste the output of `ghostty +show-config` here. placeholder: | font-family = CommitMono Nerd Font font-family-bold = CommitMono Nerd Font @@ -112,7 +112,7 @@ body: attributes: label: Additional Relevant Configuration description: | - If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide any relevant configuration information here. Please provide the minimal configuration needed. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. + If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide a minimum version of the relevant configuration information here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | `tmux.conf` --- From 8a446b77761dbad529ceb9fc64971d808eacb21c Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:41:09 -0500 Subject: [PATCH 030/642] remove the verbosity Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 3ed106ce8..09137f3d1 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -95,7 +95,7 @@ body: attributes: label: Ghostty Configuration description: | - Please provide the minimum configuration needed to reproduce this issue. If you cannot determine the minimum configuration needed to reproduce the issue, paste the output of `ghostty +show-config` here. + Please provide the minimum configuration needed to reproduce this issue. If you cannot determine this, paste the output of `ghostty +show-config` here. placeholder: | font-family = CommitMono Nerd Font font-family-bold = CommitMono Nerd Font From 6d36eeef3c6477bbb92b7a251b81873cf0ca935e Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 7 Apr 2025 21:41:29 -0500 Subject: [PATCH 031/642] add the verbosity Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 09137f3d1..d3cf58537 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -112,7 +112,7 @@ body: attributes: label: Additional Relevant Configuration description: | - If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide a minimum version of the relevant configuration information here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. + If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | `tmux.conf` --- From b213c157f079787e197bc3ad236db3616f4680fb Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 8 Apr 2025 10:38:57 -0400 Subject: [PATCH 032/642] elvish: use kitty-shell-cwd:// to report pwd OSC 7's standard body is a percent-encoded file:// URL. There isn't an easy way for us to percent-encode the path ($pwd) component here without implementing a custom function. Instead, switch to the kitty-shell-cwd:// scheme, which Kitty introduced to ease this implementation challenge in shell scripts. It accepts the path string verbatim, without an encoding. In Ghostty, we accept both the file:// and kitty-shell-cwd:// schemes, and we attempt to URI-decode them both, so in practice this is more about the "correctness" of this protocol than a functional change. It's also possible we might decide to treat these schemes differently in the runtime, like Kitty does. --- src/shell-integration/elvish/lib/ghostty-integration.elv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index bf6477aed..be98758c2 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -76,7 +76,7 @@ fn report-pwd { use platform - printf "\e]7;file://%s%s\a" platform:hostname $pwd + printf "\e]7;kitty-shell-cwd://%s%s\a" platform:hostname $pwd } fn sudo-with-terminfo {|@args| From 5b4976f6ef998948058560676a47bdc519643e0f Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Tue, 8 Apr 2025 10:53:45 -0400 Subject: [PATCH 033/642] elvish: fix platform:hostname function call syntax --- src/shell-integration/elvish/lib/ghostty-integration.elv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/elvish/lib/ghostty-integration.elv b/src/shell-integration/elvish/lib/ghostty-integration.elv index be98758c2..a6d052a72 100644 --- a/src/shell-integration/elvish/lib/ghostty-integration.elv +++ b/src/shell-integration/elvish/lib/ghostty-integration.elv @@ -76,7 +76,7 @@ fn report-pwd { use platform - printf "\e]7;kitty-shell-cwd://%s%s\a" platform:hostname $pwd + printf "\e]7;kitty-shell-cwd://%s%s\a" (platform:hostname) $pwd } fn sudo-with-terminfo {|@args| From cb1b447e8cc099aa60bf309df910b3fcce3f4ea6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 8 Apr 2025 18:33:12 -0500 Subject: [PATCH 034/642] gtk: fix forcing the window theme to light or dark Fixes #7038 --- src/apprt/gtk/App.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b4bebe8ee..ddee49459 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -314,8 +314,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .prefer_dark; }, .system => .prefer_light, - .dark => .prefer_dark, - .light => .force_dark, + .dark => .force_dark, + .light => .force_light, }, ); From 722d41a359d71f251efab9135d1bef5837512352 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 5 Apr 2025 11:45:40 -0400 Subject: [PATCH 035/642] config: allow commands to specify whether they shell expand or not This introduces a syntax for `command` and `initial-command` that allows the user to specify whether it should be run via `/bin/sh -c` or not. The syntax is a prefix `direct:` or `shell:` prior to the command, with no prefix implying a default behavior as documented. Previously, we unconditionally ran commands via `/bin/sh -c`, primarily to avoid having to do any shell expansion ourselves. We also leaned on it as a crutch for PATH-expansion but this is an easy problem compared to shell expansion. For the principle of least surprise, this worked well for configurations specified via the config file, and is still the default. However, these configurations are also set via the `-e` special flag to the CLI, and it is very much not the principle of least surprise to have the command run via `/bin/sh -c` in that scenario since a shell has already expanded all the arguments and given them to us in a nice separated format. But we had no way to toggle this behavior. This commit introduces the ability to do this, and changes the defaults so that `-e` doesn't shell expand. Further, we also do PATH lookups ourselves for the non-shell expanded case because thats easy (using execvpe style extensions but implemented as part of the Zig stdlib). We don't do path expansion (e.g. `~/`) because thats a shell expansion. So to be clear, there are no two polar opposite behavioes here with clear semantics: 1. Direct commands are passed to `execvpe` directly, space separated. This will not handle quoted strings, environment variables, path expansion (e.g. `~/`), command expansion (e.g. `$()`), etc. 2. Shell commands are passed to `/bin/sh -c` and will be shell expanded as per the shell's rules. This will handle everything that `sh` supports. In doing this work, I also stumbled upon a variety of smaller improvements that could be made: - A number of allocations have been removed from the startup path that only existed to add a null terminator to various strings. We now have null terminators from the beginning since we are almost always on a system that's going to need it anyways. - For bash shell integration, we no longer wrap the new bash command in a shell since we've formed a full parsed command line. - The process of creating the command to execute by termio is now unit tested, so we can test the various complex cases particularly on macOS of wrapping commands in the login command. - `xdg-terminal-exec` on Linux uses the `direct:` method by default since it is also assumed to be executed via a shell environment. --- src/Command.zig | 20 +- src/Surface.zig | 28 +- src/apprt/embedded.zig | 7 +- src/config.zig | 1 + src/config/Config.zig | 92 ++++-- src/config/command.zig | 322 +++++++++++++++++++ src/os/passwd.zig | 25 +- src/termio/Exec.zig | 517 ++++++++++++++++++++++--------- src/termio/shell_integration.zig | 172 ++++++---- 9 files changed, 901 insertions(+), 283 deletions(-) create mode 100644 src/config/command.zig diff --git a/src/Command.zig b/src/Command.zig index a810b16ce..e17c1b370 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -33,14 +33,17 @@ const EnvMap = std.process.EnvMap; const PreExecFn = fn (*Command) void; -/// Path to the command to run. This must be an absolute path. This -/// library does not do PATH lookup. -path: []const u8, +/// Path to the command to run. This doesn't have to be an absolute path, +/// because use exec functions that search the PATH, if necessary. +/// +/// This field is null-terminated to avoid a copy for the sake of +/// adding a null terminator since POSIX systems are so common. +path: [:0]const u8, /// Command-line arguments. It is the responsibility of the caller to set /// args[0] to the command. If args is empty then args[0] will automatically /// be set to equal path. -args: []const []const u8, +args: []const [:0]const u8, /// Environment variables for the child process. If this is null, inherits /// the environment variables from this process. These are the exact @@ -129,9 +132,8 @@ pub fn start(self: *Command, alloc: Allocator) !void { fn startPosix(self: *Command, arena: Allocator) !void { // Null-terminate all our arguments - const pathZ = try arena.dupeZ(u8, self.path); - const argsZ = try arena.allocSentinel(?[*:0]u8, self.args.len, null); - for (self.args, 0..) |arg, i| argsZ[i] = (try arena.dupeZ(u8, arg)).ptr; + const argsZ = try arena.allocSentinel(?[*:0]const u8, self.args.len, null); + for (self.args, 0..) |arg, i| argsZ[i] = arg.ptr; // Determine our env vars const envp = if (self.env) |env_map| @@ -184,7 +186,9 @@ fn startPosix(self: *Command, arena: Allocator) !void { if (self.pre_exec) |f| f(self); // Finally, replace our process. - _ = posix.execveZ(pathZ, argsZ, envp) catch null; + // Note: we must use the "p"-variant of exec here because we + // do not guarantee our command is looked up already in the path. + _ = posix.execvpeZ(self.path, argsZ, envp) catch null; // If we are executing this code, the exec failed. In that scenario, // we return a very specific error that can be detected to determine diff --git a/src/Surface.zig b/src/Surface.zig index 46fa476f7..89031a1b5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -518,7 +518,7 @@ pub fn init( }; // The command we're going to execute - const command: ?[]const u8 = if (app.first) + const command: ?configpkg.Command = if (app.first) config.@"initial-command" orelse config.command else config.command; @@ -650,21 +650,19 @@ pub fn init( // title to the command being executed. This allows window managers // to set custom styling based on the command being executed. const v = command orelse break :xdg; - if (v.len > 0) { - const title = alloc.dupeZ(u8, v) catch |err| { - log.warn( - "error copying command for title, title will not be set err={}", - .{err}, - ); - break :xdg; - }; - defer alloc.free(title); - _ = try rt_app.performAction( - .{ .surface = self }, - .set_title, - .{ .title = title }, + const title = v.string(alloc) catch |err| { + log.warn( + "error copying command for title, title will not be set err={}", + .{err}, ); - } + break :xdg; + }; + defer alloc.free(title); + _ = try rt_app.performAction( + .{ .surface = self }, + .set_title, + .{ .title = title }, + ); } // We are no longer the first surface diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 9ae00ab8e..50b54435d 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -636,6 +636,11 @@ pub const Surface = struct { /// The command to run in the new surface. If this is set then /// the "wait-after-command" option is also automatically set to true, /// since this is used for scripting. + /// + /// This command always run in a shell (e.g. via `/bin/sh -c`), + /// despite Ghostty allowing directly executed commands via config. + /// This is a legacy thing and we should probably change it in the + /// future once we have a concrete use case. command: [*:0]const u8 = "", }; @@ -696,7 +701,7 @@ pub const Surface = struct { // If we have a command from the options then we set it. const cmd = std.mem.sliceTo(opts.command, 0); if (cmd.len > 0) { - config.command = cmd; + config.command = .{ .shell = cmd }; config.@"wait-after-command" = true; } diff --git a/src/config.zig b/src/config.zig index a06e19872..fb7359b3e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -14,6 +14,7 @@ pub const formatEntry = formatter.formatEntry; // Field types pub const ClipboardAccess = Config.ClipboardAccess; +pub const Command = Config.Command; pub const ConfirmCloseSurface = Config.ConfirmCloseSurface; pub const CopyOnSelect = Config.CopyOnSelect; pub const CustomShaderAnimation = Config.CustomShaderAnimation; diff --git a/src/config/Config.zig b/src/config/Config.zig index 9cd285d3b..a0d9275e9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -22,7 +22,6 @@ const inputpkg = @import("../input.zig"); const terminal = @import("../terminal/main.zig"); const internal_os = @import("../os/main.zig"); const cli = @import("../cli.zig"); -const Command = @import("../Command.zig"); const conditional = @import("conditional.zig"); const Conditional = conditional.Conditional; @@ -34,6 +33,7 @@ const KeyValue = @import("key.zig").Value; const ErrorList = @import("ErrorList.zig"); const MetricModifier = fontpkg.Metrics.Modifier; const help_strings = @import("help_strings"); +pub const Command = @import("command.zig").Command; const RepeatableStringMap = @import("RepeatableStringMap.zig"); pub const Path = @import("path.zig").Path; pub const RepeatablePath = @import("path.zig").RepeatablePath; @@ -691,8 +691,17 @@ palette: Palette = .{}, /// * `passwd` entry (user information) /// /// This can contain additional arguments to run the command with. If additional -/// arguments are provided, the command will be executed using `/bin/sh -c`. -/// Ghostty does not do any shell command parsing. +/// 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. +/// +/// 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. /// /// 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 @@ -702,7 +711,7 @@ palette: Palette = .{}, /// arguments. For example, `ghostty -e fish --with --custom --args`. /// This flag sets the `initial-command` configuration, see that for more /// information. -command: ?[]const u8 = null, +command: ?Command = null, /// This is the same as "command", but only applies to the first terminal /// surface created when Ghostty starts. Subsequent terminal surfaces will use @@ -718,6 +727,10 @@ command: ?[]const u8 = null, /// fish --with --custom --args`. The `-e` flag automatically forces some /// other behaviors as well: /// +/// * Disables shell expansion since the input is expected to already +/// be shell-expanded by the upstream (e.g. the shell used to type in +/// the `ghostty -e` command). +/// /// * `gtk-single-instance=false` - This ensures that a new instance is /// launched and the CLI args are respected. /// @@ -735,7 +748,7 @@ command: ?[]const u8 = null, /// name your binary appropriately or source the shell integration script /// manually. /// -@"initial-command": ?[]const u8 = null, +@"initial-command": ?Command = null, /// Extra environment variables to pass to commands launched in a terminal /// surface. The format is `env=KEY=VALUE`. @@ -2564,21 +2577,17 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { // Next, take all remaining args and use that to build up // a command to execute. - var command = std.ArrayList(u8).init(arena_alloc); - errdefer command.deinit(); + var builder = std.ArrayList([:0]const u8).init(arena_alloc); + errdefer builder.deinit(); for (args) |arg_raw| { const arg = std.mem.sliceTo(arg_raw, 0); - try self._replay_steps.append( - arena_alloc, - .{ .arg = try arena_alloc.dupe(u8, arg) }, - ); - - try command.appendSlice(arg); - try command.append(' '); + const copy = try arena_alloc.dupeZ(u8, arg); + try self._replay_steps.append(arena_alloc, .{ .arg = copy }); + try builder.append(copy); } self.@"_xdg-terminal-exec" = true; - self.@"initial-command" = command.items[0 .. command.items.len - 1]; + self.@"initial-command" = .{ .direct = try builder.toOwnedSlice() }; return; } } @@ -3023,7 +3032,7 @@ pub fn finalize(self: *Config) !void { // We don't do this in flatpak because SHELL in Flatpak is always // set to /bin/sh. if (self.command) |cmd| - log.info("shell src=config value={s}", .{cmd}) + log.info("shell src=config value={}", .{cmd}) else shell_env: { // Flatpak always gets its shell from outside the sandbox if (internal_os.isFlatpak()) break :shell_env; @@ -3035,7 +3044,9 @@ pub fn finalize(self: *Config) !void { if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| { log.info("default shell source=env value={s}", .{value}); - self.command = value; + + const copy = try alloc.dupeZ(u8, value); + self.command = .{ .shell = copy }; // If we don't need the working directory, then we can exit now. if (!wd_home) break :command; @@ -3046,7 +3057,7 @@ pub fn finalize(self: *Config) !void { .windows => { if (self.command == null) { log.warn("no default shell found, will default to using cmd", .{}); - self.command = "cmd.exe"; + self.command = .{ .shell = "cmd.exe" }; } if (wd_home) { @@ -3063,7 +3074,7 @@ pub fn finalize(self: *Config) !void { if (self.command == null) { if (pw.shell) |sh| { log.info("default shell src=passwd value={s}", .{sh}); - self.command = sh; + self.command = .{ .shell = sh }; } } @@ -3145,13 +3156,13 @@ pub fn parseManuallyHook( // Build up the command. We don't clean this up because we take // ownership in our allocator. - var command = std.ArrayList(u8).init(alloc); + var command: std.ArrayList([:0]const u8) = .init(alloc); errdefer command.deinit(); while (iter.next()) |param| { - try self._replay_steps.append(alloc, .{ .arg = try alloc.dupe(u8, param) }); - try command.appendSlice(param); - try command.append(' '); + const copy = try alloc.dupeZ(u8, param); + try self._replay_steps.append(alloc, .{ .arg = copy }); + try command.append(copy); } if (command.items.len == 0) { @@ -3167,9 +3178,8 @@ pub fn parseManuallyHook( return false; } - self.@"initial-command" = command.items[0 .. command.items.len - 1]; - // See "command" docs for the implied configurations and why. + self.@"initial-command" = .{ .direct = command.items }; self.@"gtk-single-instance" = .false; self.@"quit-after-last-window-closed" = true; self.@"quit-after-last-window-closed-delay" = null; @@ -3184,7 +3194,7 @@ pub fn parseManuallyHook( // Keep track of our input args for replay try self._replay_steps.append( alloc, - .{ .arg = try alloc.dupe(u8, arg) }, + .{ .arg = try alloc.dupeZ(u8, arg) }, ); // If we didn't find a special case, continue parsing normally @@ -3377,6 +3387,16 @@ fn equalField(comptime T: type, old: T, new: T) bool { [:0]const u8, => return std.mem.eql(u8, old, new), + []const [:0]const u8, + => { + if (old.len != new.len) return false; + for (old, new) |a, b| { + if (!std.mem.eql(u8, a, b)) return false; + } + + return true; + }, + else => {}, } @@ -3412,6 +3432,8 @@ fn equalField(comptime T: type, old: T, new: T) bool { }, .@"union" => |info| { + if (@hasDecl(T, "equal")) return old.equal(new); + const tag_type = info.tag_type.?; const old_tag = std.meta.activeTag(old); const new_tag = std.meta.activeTag(new); @@ -3441,7 +3463,7 @@ fn equalField(comptime T: type, old: T, new: T) bool { const Replay = struct { const Step = union(enum) { /// An argument to parse as if it came from the CLI or file. - arg: []const u8, + arg: [:0]const u8, /// A base path to expand relative paths against. expand: []const u8, @@ -3481,7 +3503,7 @@ const Replay = struct { return switch (self) { .@"-e" => self, .diagnostic => |v| .{ .diagnostic = try v.clone(alloc) }, - .arg => |v| .{ .arg = try alloc.dupe(u8, v) }, + .arg => |v| .{ .arg = try alloc.dupeZ(u8, v) }, .expand => |v| .{ .expand = try alloc.dupe(u8, v) }, .conditional_arg => |v| conditional: { var conds = try alloc.alloc(Conditional, v.conditions.len); @@ -6620,7 +6642,11 @@ test "parse e: command only" { var it: TestIterator = .{ .data = &.{"foo"} }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("foo", cfg.@"initial-command".?); + + const cmd = cfg.@"initial-command".?; + try testing.expect(cmd == .direct); + try testing.expectEqual(cmd.direct.len, 1); + try testing.expectEqualStrings(cmd.direct[0], "foo"); } test "parse e: command and args" { @@ -6631,7 +6657,13 @@ test "parse e: command and args" { var it: TestIterator = .{ .data = &.{ "echo", "foo", "bar baz" } }; try testing.expect(!try cfg.parseManuallyHook(alloc, "-e", &it)); - try testing.expectEqualStrings("echo foo bar baz", cfg.@"initial-command".?); + + const cmd = cfg.@"initial-command".?; + try testing.expect(cmd == .direct); + try testing.expectEqual(cmd.direct.len, 3); + try testing.expectEqualStrings(cmd.direct[0], "echo"); + try testing.expectEqualStrings(cmd.direct[1], "foo"); + try testing.expectEqualStrings(cmd.direct[2], "bar baz"); } test "clone default" { diff --git a/src/config/command.zig b/src/config/command.zig new file mode 100644 index 000000000..9efeb199e --- /dev/null +++ b/src/config/command.zig @@ -0,0 +1,322 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const formatterpkg = @import("formatter.zig"); + +/// A command to execute (argv0 and args). +/// +/// A command is specified as a simple string such as "nvim a b c". +/// By default, we expect the downstream to do some sort of shell expansion +/// on this string. +/// +/// If a command is already expanded and the user does NOT want to do +/// shell expansion (because this usually requires a round trip into +/// /bin/sh or equivalent), specify a `direct:`-prefix. e.g. +/// `direct:nvim a b c`. +/// +/// The whitespace before or around the prefix is ignored. For example, +/// ` direct:nvim a b c` and `direct: nvim a b c` are equivalent. +/// +/// If the command is not absolute, it'll be looked up via the PATH. +/// For the shell-expansion case, we let the shell do this. For the +/// direct case, we do this directly. +pub const Command = union(enum) { + const Self = @This(); + + /// Execute a command directly, e.g. via `exec`. The format here + /// is already structured to be ready to passed directly to `exec` + /// with index zero being the command to execute. + /// + /// Index zero is not guaranteed to be an absolute path, and may require + /// PATH lookup. It is up to the downstream to do this, usually via + /// delegation to something like `execvp`. + direct: []const [:0]const u8, + + /// Execute a command via shell expansion. This provides the command + /// as a single string that is expected to be expanded in some way + /// (up to the downstream). Usually `/bin/sh -c`. + shell: [:0]const u8, + + pub fn parseCLI( + self: *Self, + alloc: Allocator, + input_: ?[]const u8, + ) !void { + // Input is required. Whitespace on the edges isn't needed. + // Commands must be non-empty. + const input = input_ orelse return error.ValueRequired; + const trimmed = std.mem.trim(u8, input, " "); + if (trimmed.len == 0) return error.ValueRequired; + + // If we have a `:` then we MIGHT have a prefix to specify what + // tag we should use. + const tag: std.meta.Tag(Self), const str: []const u8 = tag: { + if (std.mem.indexOfScalar(u8, trimmed, ':')) |idx| { + const prefix = trimmed[0..idx]; + if (std.mem.eql(u8, prefix, "direct")) { + break :tag .{ .direct, trimmed[idx + 1 ..] }; + } else if (std.mem.eql(u8, prefix, "shell")) { + break :tag .{ .shell, trimmed[idx + 1 ..] }; + } + } + + break :tag .{ .shell, trimmed }; + }; + + switch (tag) { + .shell => { + // We have a shell command, so we can just dupe it. + const copy = try alloc.dupeZ(u8, std.mem.trim(u8, str, " ")); + self.* = .{ .shell = copy }; + }, + + .direct => { + // We're not shell expanding, so the arguments are naively + // split on spaces. + var builder: std.ArrayListUnmanaged([:0]const u8) = .empty; + var args = std.mem.splitScalar( + u8, + std.mem.trim(u8, str, " "), + ' ', + ); + while (args.next()) |arg| { + const copy = try alloc.dupeZ(u8, arg); + try builder.append(alloc, copy); + } + + self.* = .{ .direct = try builder.toOwnedSlice(alloc) }; + }, + } + } + + /// Creates a command as a single string, joining arguments as + /// necessary with spaces. Its not guaranteed that this is a valid + /// command; it is only meant to be human readable. + pub fn string( + self: *const Self, + alloc: Allocator, + ) Allocator.Error![:0]const u8 { + return switch (self.*) { + .shell => |v| try alloc.dupeZ(u8, v), + .direct => |v| try std.mem.joinZ(alloc, " ", v), + }; + } + + /// Get an iterator over the arguments array. This may allocate + /// depending on the active tag of the command. + /// + /// For direct commands, this is very cheap and just iterates over + /// the array. There is no allocation. + /// + /// For shell commands, this will use Zig's ArgIteratorGeneral as + /// a best effort shell string parser. This is not guaranteed to be + /// 100% accurate, but it works for common cases. This requires allocation. + pub fn argIterator( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!ArgIterator { + return switch (self.*) { + .direct => |v| .{ .direct = .{ .args = v } }, + .shell => |v| .{ .shell = try .init(alloc, v) }, + }; + } + + /// Iterates over each argument in the command. + pub const ArgIterator = union(enum) { + shell: std.process.ArgIteratorGeneral(.{}), + direct: struct { + i: usize = 0, + args: []const [:0]const u8, + }, + + /// Return the next argument. This may or may not be a copy + /// depending on the active tag. If you want to ensure that every + /// argument is a copy, use the `clone` method first. + pub fn next(self: *ArgIterator) ?[:0]const u8 { + return switch (self.*) { + .shell => |*v| v.next(), + .direct => |*v| { + if (v.i >= v.args.len) return null; + defer v.i += 1; + return v.args[v.i]; + }, + }; + } + + pub fn deinit(self: *ArgIterator) void { + switch (self.*) { + .shell => |*v| v.deinit(), + .direct => {}, + } + } + }; + + pub fn clone( + self: *const Self, + alloc: Allocator, + ) Allocator.Error!Self { + return switch (self.*) { + .shell => |v| .{ .shell = try alloc.dupeZ(u8, v) }, + .direct => |v| direct: { + const copy = try alloc.alloc([:0]const u8, v.len); + for (v, 0..) |arg, i| copy[i] = try alloc.dupeZ(u8, arg); + break :direct .{ .direct = copy }; + }, + }; + } + + pub fn formatEntry(self: Self, formatter: anytype) !void { + switch (self) { + .shell => |v| try formatter.formatEntry([]const u8, v), + + .direct => |v| { + var buf: [4096]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const writer = fbs.writer(); + writer.writeAll("direct:") catch return error.OutOfMemory; + for (v) |arg| { + writer.writeAll(arg) catch return error.OutOfMemory; + writer.writeByte(' ') catch return error.OutOfMemory; + } + + const written = fbs.getWritten(); + try formatter.formatEntry( + []const u8, + written[0..@intCast(written.len - 1)], + ); + }, + } + } + + test "Command: parseCLI errors" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, null)); + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, "")); + try testing.expectError(error.ValueRequired, v.parseCLI(alloc, " ")); + } + + test "Command: parseCLI shell expanded" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try v.parseCLI(alloc, "echo hello"); + try testing.expect(v == .shell); + try testing.expectEqualStrings(v.shell, "echo hello"); + + // Spaces are stripped + try v.parseCLI(alloc, " echo hello "); + try testing.expect(v == .shell); + try testing.expectEqualStrings(v.shell, "echo hello"); + } + + test "Command: parseCLI direct" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var v: Self = undefined; + try v.parseCLI(alloc, "direct:echo hello"); + try testing.expect(v == .direct); + try testing.expectEqual(v.direct.len, 2); + try testing.expectEqualStrings(v.direct[0], "echo"); + try testing.expectEqualStrings(v.direct[1], "hello"); + + // Spaces around the prefix + try v.parseCLI(alloc, " direct: echo hello"); + try testing.expect(v == .direct); + try testing.expectEqual(v.direct.len, 2); + try testing.expectEqualStrings(v.direct[0], "echo"); + try testing.expectEqualStrings(v.direct[1], "hello"); + } + + test "Command: argIterator shell" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .shell = "echo hello world" }; + var it = try v.argIterator(alloc); + defer it.deinit(); + + try testing.expectEqualStrings(it.next().?, "echo"); + try testing.expectEqualStrings(it.next().?, "hello"); + try testing.expectEqualStrings(it.next().?, "world"); + try testing.expect(it.next() == null); + } + + test "Command: argIterator direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .direct = &.{ "echo", "hello world" } }; + var it = try v.argIterator(alloc); + defer it.deinit(); + + try testing.expectEqualStrings(it.next().?, "echo"); + try testing.expectEqualStrings(it.next().?, "hello world"); + try testing.expect(it.next() == null); + } + + test "Command: string shell" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .shell = "echo hello world" }; + const str = try v.string(alloc); + defer alloc.free(str); + try testing.expectEqualStrings(str, "echo hello world"); + } + + test "Command: string direct" { + const testing = std.testing; + const alloc = testing.allocator; + + var v: Self = .{ .direct = &.{ "echo", "hello world" } }; + const str = try v.string(alloc); + defer alloc.free(str); + try testing.expectEqualStrings(str, "echo hello world"); + } + + test "Command: formatConfig shell" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + var v: Self = undefined; + try v.parseCLI(alloc, "echo hello"); + try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = echo hello\n", buf.items); + } + + test "Command: formatConfig direct" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var buf = std.ArrayList(u8).init(alloc); + defer buf.deinit(); + + var v: Self = undefined; + try v.parseCLI(alloc, "direct: echo hello"); + try v.formatEntry(formatterpkg.entryFormatter("a", buf.writer())); + try std.testing.expectEqualSlices(u8, "a = direct:echo hello\n", buf.items); + } +}; + +test { + _ = Command; +} diff --git a/src/os/passwd.zig b/src/os/passwd.zig index c12214ee4..e9bbff066 100644 --- a/src/os/passwd.zig +++ b/src/os/passwd.zig @@ -25,9 +25,9 @@ const c = if (builtin.os.tag != .windows) @cImport({ // Entry that is retrieved from the passwd API. This only contains the fields // we care about. pub const Entry = struct { - shell: ?[]const u8 = null, - home: ?[]const u8 = null, - name: ?[]const u8 = null, + shell: ?[:0]const u8 = null, + home: ?[:0]const u8 = null, + name: ?[:0]const u8 = null, }; /// Get the passwd entry for the currently executing user. @@ -117,30 +117,27 @@ pub fn get(alloc: Allocator) !Entry { // Shell and home are the last two entries var it = std.mem.splitBackwardsScalar(u8, std.mem.trimRight(u8, output, " \r\n"), ':'); - result.shell = it.next() orelse null; - result.home = it.next() orelse null; + result.shell = if (it.next()) |v| try alloc.dupeZ(u8, v) else null; + result.home = if (it.next()) |v| try alloc.dupeZ(u8, v) else null; return result; } if (pw.pw_shell) |ptr| { const source = std.mem.sliceTo(ptr, 0); - const sh = try alloc.alloc(u8, source.len); - @memcpy(sh, source); - result.shell = sh; + const value = try alloc.dupeZ(u8, source); + result.shell = value; } if (pw.pw_dir) |ptr| { const source = std.mem.sliceTo(ptr, 0); - const dir = try alloc.alloc(u8, source.len); - @memcpy(dir, source); - result.home = dir; + const value = try alloc.dupeZ(u8, source); + result.home = value; } if (pw.pw_name) |ptr| { const source = std.mem.sliceTo(ptr, 0); - const name = try alloc.alloc(u8, source.len); - @memcpy(name, source); - result.name = name; + const value = try alloc.dupeZ(u8, source); + result.name = value; } return result; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 61b501258..abe49a47b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -24,6 +24,7 @@ const SegmentedPool = @import("../datastruct/main.zig").SegmentedPool; const ptypkg = @import("../pty.zig"); const Pty = ptypkg.Pty; const EnvMap = std.process.EnvMap; +const PasswdEntry = internal_os.passwd.Entry; const windows = internal_os.windows; const log = std.log.scoped(.io_exec); @@ -725,7 +726,7 @@ pub const ThreadData = struct { }; pub const Config = struct { - command: ?[]const u8 = null, + command: ?configpkg.Command = null, env: EnvMap, env_override: configpkg.RepeatableStringMap = .{}, shell_integration: configpkg.Config.ShellIntegration = .detect, @@ -746,7 +747,7 @@ const Subprocess = struct { arena: std.heap.ArenaAllocator, cwd: ?[]const u8, env: ?EnvMap, - args: [][]const u8, + args: []const [:0]const u8, grid_size: renderer.GridSize, screen_size: renderer.ScreenSize, pty: ?Pty = null, @@ -892,18 +893,29 @@ const Subprocess = struct { env.remove("VTE_VERSION"); // Setup our shell integration, if we can. - const integrated_shell: ?shell_integration.Shell, const shell_command: []const u8 = shell: { - const default_shell_command = cfg.command orelse switch (builtin.os.tag) { - .windows => "cmd.exe", - else => "sh", - }; + const shell_command: configpkg.Command = shell: { + const default_shell_command: configpkg.Command = + cfg.command orelse .{ .shell = switch (builtin.os.tag) { + .windows => "cmd.exe", + else => "sh", + } }; const force: ?shell_integration.Shell = switch (cfg.shell_integration) { .none => { - // Even if shell integration is none, we still want to set up the feature env vars - try shell_integration.setupFeatures(&env, cfg.shell_integration_features); - break :shell .{ null, default_shell_command }; + // Even if shell integration is none, we still want to + // set up the feature env vars + try shell_integration.setupFeatures( + &env, + cfg.shell_integration_features, + ); + + // This is a source of confusion for users despite being + // opt-in since it results in some Ghostty features not + // working. We always want to log it. + log.info("shell integration disabled by configuration", .{}); + break :shell default_shell_command; }, + .detect => null, .bash => .bash, .elvish => .elvish, @@ -911,9 +923,9 @@ const Subprocess = struct { .zsh => .zsh, }; - const dir = cfg.resources_dir orelse break :shell .{ - null, - default_shell_command, + const dir = cfg.resources_dir orelse { + log.warn("no resources dir set, shell integration disabled", .{}); + break :shell default_shell_command; }; const integration = try shell_integration.setup( @@ -923,19 +935,18 @@ const Subprocess = struct { &env, force, cfg.shell_integration_features, - ) orelse break :shell .{ null, default_shell_command }; + ) orelse { + log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); + break :shell default_shell_command; + }; - break :shell .{ integration.shell, integration.command }; - }; - - if (integrated_shell) |shell| { log.info( "shell integration automatically injected shell={}", - .{shell}, + .{integration.shell}, ); - } else if (cfg.shell_integration != .none) { - log.warn("shell could not be detected, no automatic shell integration will be injected", .{}); - } + + break :shell integration.command; + }; // Add the environment variables that override any others. { @@ -947,134 +958,29 @@ const Subprocess = struct { } // Build our args list - const args = args: { - const cap = 9; // the most we'll ever use - var args = try std.ArrayList([]const u8).initCapacity(alloc, cap); - defer args.deinit(); + const args: []const [:0]const u8 = execCommand( + alloc, + shell_command, + internal_os.passwd, + ) catch |err| switch (err) { + // If we fail to allocate space for the command we want to + // execute, we'd still like to try to run something so + // Ghostty can launch (and maybe the user can debug this further). + // Realistically, if you're getting OOM, I think other stuff is + // about to crash, but we can try. + error.OutOfMemory => oom: { + log.warn("failed to allocate space for command args, falling back to basic shell", .{}); - // If we're on macOS, we have to use `login(1)` to get all of - // the proper environment variables set, a login shell, and proper - // hushlogin behavior. - if (comptime builtin.target.os.tag.isDarwin()) darwin: { - const passwd = internal_os.passwd.get(alloc) catch |err| { - log.warn("failed to read passwd, not using a login shell err={}", .{err}); - break :darwin; + // The comptime here is important to ensure the full slice + // is put into the binary data and not the stack. + break :oom comptime switch (builtin.os.tag) { + .windows => &.{"cmd.exe"}, + else => &.{"/bin/sh"}, }; + }, - const username = passwd.name orelse { - log.warn("failed to get username, not using a login shell", .{}); - break :darwin; - }; - - const hush = if (passwd.home) |home| hush: { - var dir = std.fs.openDirAbsolute(home, .{}) catch |err| { - log.warn( - "failed to open home dir, not checking for hushlogin err={}", - .{err}, - ); - break :hush false; - }; - defer dir.close(); - - break :hush if (dir.access(".hushlogin", .{})) true else |_| false; - } else false; - - const cmd = try std.fmt.allocPrint( - alloc, - "exec -l {s}", - .{shell_command}, - ); - - // The reason for executing login this way is unclear. This - // comment will attempt to explain but prepare for a truly - // unhinged reality. - // - // The first major issue is that on macOS, a lot of users - // put shell configurations in ~/.bash_profile instead of - // ~/.bashrc (or equivalent for another shell). This file is only - // loaded for a login shell so macOS users expect all their terminals - // to be login shells. No other platform behaves this way and its - // totally braindead but somehow the entire dev community on - // macOS has cargo culted their way to this reality so we have to - // do it... - // - // To get a login shell, you COULD just prepend argv0 with a `-` - // but that doesn't fully work because `getlogin()` C API will - // return the wrong value, SHELL won't be set, and various - // other login behaviors that macOS users expect. - // - // The proper way is to use `login(1)`. But login(1) forces - // the working directory to change to the home directory, - // which we may not want. If we specify "-l" then we can avoid - // this behavior but now the shell isn't a login shell. - // - // There is another issue: `login(1)` on macOS 14.3 and earlier - // checked for ".hushlogin" in the working directory. This means - // that if we specify "-l" then we won't get hushlogin honored - // if its in the home directory (which is standard). To get - // around this, we check for hushlogin ourselves and if present - // specify the "-q" flag to login(1). - // - // So to get all the behaviors we want, we specify "-l" but - // execute "bash" (which is built-in to macOS). We then use - // the bash builtin "exec" to replace the process with a login - // shell ("-l" on exec) with the command we really want. - // - // We use "bash" instead of other shells that ship with macOS - // because as of macOS Sonoma, we found with a microbenchmark - // that bash can `exec` into the desired command ~2x faster - // than zsh. - // - // To figure out a lot of this logic I read the login.c - // source code in the OSS distribution Apple provides for - // macOS. - // - // Awesome. - try args.append("/usr/bin/login"); - if (hush) try args.append("-q"); - try args.append("-flp"); - - // We execute bash with "--noprofile --norc" so that it doesn't - // load startup files so that (1) our shell integration doesn't - // break and (2) user configuration doesn't mess this process - // up. - try args.append(username); - try args.append("/bin/bash"); - try args.append("--noprofile"); - try args.append("--norc"); - try args.append("-c"); - try args.append(cmd); - break :args try args.toOwnedSlice(); - } - - if (comptime builtin.os.tag == .windows) { - // We run our shell wrapped in `cmd.exe` so that we don't have - // to parse the command line ourselves if it has arguments. - - // Note we don't free any of the memory below since it is - // allocated in the arena. - const windir = try std.process.getEnvVarOwned(alloc, "WINDIR"); - const cmd = try std.fs.path.join(alloc, &[_][]const u8{ - windir, - "System32", - "cmd.exe", - }); - - try args.append(cmd); - try args.append("/C"); - } else { - // We run our shell wrapped in `/bin/sh` so that we don't have - // to parse the command line ourselves if it has arguments. - // Additionally, some environments (NixOS, I found) use /bin/sh - // to setup some environment variables that are important to - // have set. - try args.append("/bin/sh"); - if (internal_os.isFlatpak()) try args.append("-l"); - try args.append("-c"); - } - - try args.append(shell_command); - break :args try args.toOwnedSlice(); + // This logs on its own, this is a bad error. + error.SystemError => return err, }; // We have to copy the cwd because there is no guarantee that @@ -1562,3 +1468,320 @@ pub const ReadThread = struct { } } }; + +/// Builds the argv array for the process we should exec for the +/// configured command. This isn't as straightforward as it seems since +/// we deal with shell-wrapping, macOS login shells, etc. +/// +/// The passwdpkg comptime argument is expected to have a single function +/// `get(Allocator)` that returns a passwd entry. This is used by macOS +/// to determine the username and home directory for the login shell. +/// It is unused on other platforms. +/// +/// Memory ownership: +/// +/// The allocator should be an arena, since the returned value may or +/// may not be allocated and args may or may not be allocated (or copied). +/// Pointers in the return value may point to pointers in the command +/// struct. +fn execCommand( + alloc: Allocator, + command: configpkg.Command, + comptime passwdpkg: type, +) (Allocator.Error || error{SystemError})![]const [:0]const u8 { + // If we're on macOS, we have to use `login(1)` to get all of + // the proper environment variables set, a login shell, and proper + // hushlogin behavior. + if (comptime builtin.target.os.tag.isDarwin()) darwin: { + const passwd = passwdpkg.get(alloc) catch |err| { + log.warn("failed to read passwd, not using a login shell err={}", .{err}); + break :darwin; + }; + + const username = passwd.name orelse { + log.warn("failed to get username, not using a login shell", .{}); + break :darwin; + }; + + const hush = if (passwd.home) |home| hush: { + var dir = std.fs.openDirAbsolute(home, .{}) catch |err| { + log.warn( + "failed to open home dir, not checking for hushlogin err={}", + .{err}, + ); + break :hush false; + }; + defer dir.close(); + + break :hush if (dir.access(".hushlogin", .{})) true else |_| false; + } else false; + + // If we made it this far we're going to start building + // the actual command. + var args: std.ArrayList([:0]const u8) = try .initCapacity( + alloc, + + // This capacity is chosen based on what we'd need to + // execute a shell command (very common). We can/will + // grow if necessary for a longer command (uncommon). + 9, + ); + defer args.deinit(); + + // The reason for executing login this way is unclear. This + // comment will attempt to explain but prepare for a truly + // unhinged reality. + // + // The first major issue is that on macOS, a lot of users + // put shell configurations in ~/.bash_profile instead of + // ~/.bashrc (or equivalent for another shell). This file is only + // loaded for a login shell so macOS users expect all their terminals + // to be login shells. No other platform behaves this way and its + // totally braindead but somehow the entire dev community on + // macOS has cargo culted their way to this reality so we have to + // do it... + // + // To get a login shell, you COULD just prepend argv0 with a `-` + // but that doesn't fully work because `getlogin()` C API will + // return the wrong value, SHELL won't be set, and various + // other login behaviors that macOS users expect. + // + // The proper way is to use `login(1)`. But login(1) forces + // the working directory to change to the home directory, + // which we may not want. If we specify "-l" then we can avoid + // this behavior but now the shell isn't a login shell. + // + // There is another issue: `login(1)` on macOS 14.3 and earlier + // checked for ".hushlogin" in the working directory. This means + // that if we specify "-l" then we won't get hushlogin honored + // if its in the home directory (which is standard). To get + // around this, we check for hushlogin ourselves and if present + // specify the "-q" flag to login(1). + // + // So to get all the behaviors we want, we specify "-l" but + // execute "bash" (which is built-in to macOS). We then use + // the bash builtin "exec" to replace the process with a login + // shell ("-l" on exec) with the command we really want. + // + // We use "bash" instead of other shells that ship with macOS + // because as of macOS Sonoma, we found with a microbenchmark + // that bash can `exec` into the desired command ~2x faster + // than zsh. + // + // To figure out a lot of this logic I read the login.c + // source code in the OSS distribution Apple provides for + // macOS. + // + // Awesome. + try args.append("/usr/bin/login"); + if (hush) try args.append("-q"); + try args.append("-flp"); + try args.append(username); + + switch (command) { + // Direct args can be passed directly to login, since + // login uses execvp we don't need to worry about PATH + // searching. + .direct => |v| try args.appendSlice(v), + + .shell => |v| { + // Use "exec" to replace the bash process with + // our intended command so we don't have a parent + // process hanging around. + const cmd = try std.fmt.allocPrintZ( + alloc, + "exec -l {s}", + .{v}, + ); + + // We execute bash with "--noprofile --norc" so that it doesn't + // load startup files so that (1) our shell integration doesn't + // break and (2) user configuration doesn't mess this process + // up. + try args.append("/bin/bash"); + try args.append("--noprofile"); + try args.append("--norc"); + try args.append("-c"); + try args.append(cmd); + }, + } + + return try args.toOwnedSlice(); + } + + return switch (command) { + .direct => |v| v, + + .shell => |v| shell: { + var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4); + defer args.deinit(); + + if (comptime builtin.os.tag == .windows) { + // We run our shell wrapped in `cmd.exe` so that we don't have + // to parse the command line ourselves if it has arguments. + + // Note we don't free any of the memory below since it is + // allocated in the arena. + const windir = std.process.getEnvVarOwned( + alloc, + "WINDIR", + ) catch |err| { + log.warn("failed to get WINDIR, cannot run shell command err={}", .{err}); + return error.SystemError; + }; + const cmd = try std.fs.path.joinZ(alloc, &[_][]const u8{ + windir, + "System32", + "cmd.exe", + }); + + try args.append(cmd); + try args.append("/C"); + } else { + // We run our shell wrapped in `/bin/sh` so that we don't have + // to parse the command line ourselves if it has arguments. + // Additionally, some environments (NixOS, I found) use /bin/sh + // to setup some environment variables that are important to + // have set. + try args.append("/bin/sh"); + if (internal_os.isFlatpak()) try args.append("-l"); + try args.append("-c"); + } + + try args.append(v); + break :shell try args.toOwnedSlice(); + }, + }; +} + +test "execCommand darwin: shell command" { + if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand(alloc, .{ .shell = "foo bar baz" }, struct { + fn get(_: Allocator) !PasswdEntry { + return .{ + .name = "testuser", + }; + } + }); + + try testing.expectEqual(8, result.len); + try testing.expectEqualStrings(result[0], "/usr/bin/login"); + try testing.expectEqualStrings(result[1], "-flp"); + try testing.expectEqualStrings(result[2], "testuser"); + try testing.expectEqualStrings(result[3], "/bin/bash"); + try testing.expectEqualStrings(result[4], "--noprofile"); + try testing.expectEqualStrings(result[5], "--norc"); + try testing.expectEqualStrings(result[6], "-c"); + try testing.expectEqualStrings(result[7], "exec -l foo bar baz"); +} + +test "execCommand darwin: direct command" { + if (comptime !builtin.os.tag.isDarwin()) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand(alloc, .{ .direct = &.{ + "foo", + "bar baz", + } }, struct { + fn get(_: Allocator) !PasswdEntry { + return .{ + .name = "testuser", + }; + } + }); + + try testing.expectEqual(5, result.len); + try testing.expectEqualStrings(result[0], "/usr/bin/login"); + try testing.expectEqualStrings(result[1], "-flp"); + try testing.expectEqualStrings(result[2], "testuser"); + try testing.expectEqualStrings(result[3], "foo"); + try testing.expectEqualStrings(result[4], "bar baz"); +} + +test "execCommand: shell command, empty passwd" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand( + alloc, + .{ .shell = "foo bar baz" }, + struct { + fn get(_: Allocator) !PasswdEntry { + // Empty passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return .{}; + } + }, + ); + + try testing.expectEqual(3, result.len); + try testing.expectEqualStrings(result[0], "/bin/sh"); + try testing.expectEqualStrings(result[1], "-c"); + try testing.expectEqualStrings(result[2], "foo bar baz"); +} + +test "execCommand: shell command, error passwd" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand( + alloc, + .{ .shell = "foo bar baz" }, + struct { + fn get(_: Allocator) !PasswdEntry { + // Failed passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return error.Fail; + } + }, + ); + + try testing.expectEqual(3, result.len); + try testing.expectEqualStrings(result[0], "/bin/sh"); + try testing.expectEqualStrings(result[1], "-c"); + try testing.expectEqualStrings(result[2], "foo bar baz"); +} + +test "execCommand: direct command, error passwd" { + if (comptime builtin.os.tag == .windows) return error.SkipZigTest; + + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + const result = try execCommand(alloc, .{ + .direct = &.{ + "foo", + "bar baz", + }, + }, struct { + fn get(_: Allocator) !PasswdEntry { + // Failed passwd entry means we can't construct a macOS + // login command and falls back to POSIX behavior. + return error.Fail; + } + }); + + try testing.expectEqual(2, result.len); + try testing.expectEqualStrings(result[0], "foo"); + try testing.expectEqualStrings(result[1], "bar baz"); +} diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index ae8d5b67c..2cf809694 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -27,7 +27,7 @@ pub const ShellIntegration = struct { /// bash in particular it may be different. /// /// The memory is allocated in the arena given to setup. - command: []const u8, + command: config.Command, }; /// Set up the command execution environment for automatic @@ -41,7 +41,7 @@ pub const ShellIntegration = struct { pub fn setup( alloc_arena: Allocator, resource_dir: []const u8, - command: []const u8, + command: config.Command, env: *EnvMap, force_shell: ?Shell, features: config.ShellIntegrationFeatures, @@ -51,14 +51,24 @@ pub fn setup( .elvish => "elvish", .fish => "fish", .zsh => "zsh", - } else exe: { - // The command can include arguments. Look for the first space - // and use the basename of the first part as the command's exe. - const idx = std.mem.indexOfScalar(u8, command, ' ') orelse command.len; - break :exe std.fs.path.basename(command[0..idx]); + } else switch (command) { + .direct => |v| std.fs.path.basename(v[0]), + .shell => |v| exe: { + // Shell strings can include spaces so we want to only + // look up to the space if it exists. No shell that we integrate + // has spaces. + const idx = std.mem.indexOfScalar(u8, v, ' ') orelse v.len; + break :exe std.fs.path.basename(v[0..idx]); + }, }; - const result = try setupShell(alloc_arena, resource_dir, command, env, exe); + const result = try setupShell( + alloc_arena, + resource_dir, + command, + env, + exe, + ); // Setup our feature env vars try setupFeatures(env, features); @@ -69,7 +79,7 @@ pub fn setup( fn setupShell( alloc_arena: Allocator, resource_dir: []const u8, - command: []const u8, + command: config.Command, env: *EnvMap, exe: []const u8, ) !?ShellIntegration { @@ -83,7 +93,10 @@ fn setupShell( // we're using Apple's Bash because /bin is non-writable // on modern macOS due to System Integrity Protection. if (comptime builtin.target.os.tag.isDarwin()) { - if (std.mem.eql(u8, "/bin/bash", command)) { + if (std.mem.eql(u8, "/bin/bash", switch (command) { + .direct => |v| v[0], + .shell => |v| v, + })) { return null; } } @@ -104,7 +117,7 @@ fn setupShell( try setupXdgDataDirs(alloc_arena, resource_dir, env); return .{ .shell = .elvish, - .command = try alloc_arena.dupe(u8, command), + .command = try command.clone(alloc_arena), }; } @@ -112,7 +125,7 @@ fn setupShell( try setupXdgDataDirs(alloc_arena, resource_dir, env); return .{ .shell = .fish, - .command = try alloc_arena.dupe(u8, command), + .command = try command.clone(alloc_arena), }; } @@ -120,7 +133,7 @@ fn setupShell( try setupZsh(resource_dir, env); return .{ .shell = .zsh, - .command = try alloc_arena.dupe(u8, command), + .command = try command.clone(alloc_arena), }; } @@ -139,7 +152,14 @@ test "force shell" { inline for (@typeInfo(Shell).@"enum".fields) |field| { const shell = @field(Shell, field.name); - const result = try setup(alloc, ".", "sh", &env, shell, .{}); + const result = try setup( + alloc, + ".", + .{ .shell = "sh" }, + &env, + shell, + .{}, + ); try testing.expectEqual(shell, result.?.shell); } } @@ -215,25 +235,21 @@ test "setup features" { /// enables the integration or null if integration failed. fn setupBash( alloc: Allocator, - command: []const u8, + command: config.Command, resource_dir: []const u8, env: *EnvMap, -) !?[]const u8 { - // Accumulates the arguments that will form the final shell command line. - // We can build this list on the stack because we're just temporarily - // referencing other slices, but we can fall back to heap in extreme cases. - var args_alloc = std.heap.stackFallback(1024, alloc); - var args = try std.ArrayList([]const u8).initCapacity(args_alloc.get(), 2); +) !?config.Command { + var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 2); defer args.deinit(); // Iterator that yields each argument in the original command line. // This will allocate once proportionate to the command line length. - var iter = try std.process.ArgIteratorGeneral(.{}).init(alloc, command); + var iter = try command.argIterator(alloc); defer iter.deinit(); // Start accumulating arguments with the executable and `--posix` mode flag. if (iter.next()) |exe| { - try args.append(exe); + try args.append(try alloc.dupeZ(u8, exe)); } else return null; try args.append("--posix"); @@ -267,17 +283,17 @@ fn setupBash( if (std.mem.indexOfScalar(u8, arg, 'c') != null) { return null; } - try args.append(arg); + try args.append(try alloc.dupeZ(u8, arg)); } else if (std.mem.eql(u8, arg, "-") or std.mem.eql(u8, arg, "--")) { // All remaining arguments should be passed directly to the shell // command. We shouldn't perform any further option processing. - try args.append(arg); + try args.append(try alloc.dupeZ(u8, arg)); while (iter.next()) |remaining_arg| { - try args.append(remaining_arg); + try args.append(try alloc.dupeZ(u8, remaining_arg)); } break; } else { - try args.append(arg); + try args.append(try alloc.dupeZ(u8, arg)); } } try env.put("GHOSTTY_BASH_INJECT", inject.slice()); @@ -310,30 +326,36 @@ fn setupBash( ); try env.put("ENV", integ_dir); - // Join the accumulated arguments to form the final command string. - return try std.mem.join(alloc, " ", args.items); + // Since we built up a command line, we don't need to wrap it in + // ANOTHER shell anymore and can do a direct command. + return .{ .direct = try args.toOwnedSlice() }; } test "bash" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash", ".", &env); - defer if (command) |c| alloc.free(c); + const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - try testing.expectEqualStrings("bash --posix", command.?); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?); } test "bash: unsupported options" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); - const cmdlines = [_][]const u8{ + const cmdlines = [_][:0]const u8{ "bash --posix", "bash --rcfile script.sh --posix", "bash --init-file script.sh --posix", @@ -345,7 +367,7 @@ test "bash: unsupported options" { var env = EnvMap.init(alloc); defer env.deinit(); - try testing.expect(try setupBash(alloc, cmdline, ".", &env) == null); + try testing.expect(try setupBash(alloc, .{ .shell = cmdline }, ".", &env) == null); try testing.expect(env.get("GHOSTTY_BASH_INJECT") == null); try testing.expect(env.get("GHOSTTY_BASH_RCFILE") == null); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); @@ -354,17 +376,20 @@ test "bash: unsupported options" { test "bash: inject flags" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); // bash --norc { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash --norc", ".", &env); - defer if (command) |c| alloc.free(c); + const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env); - try testing.expectEqualStrings("bash --posix", command.?); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?); } @@ -373,52 +398,55 @@ test "bash: inject flags" { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash --noprofile", ".", &env); - defer if (command) |c| alloc.free(c); + const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env); - try testing.expectEqualStrings("bash --posix", command.?); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?); } } test "bash: rcfile" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var env = EnvMap.init(alloc); defer env.deinit(); // bash --rcfile { - const command = try setupBash(alloc, "bash --rcfile profile.sh", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix", command.?); + const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } // bash --init-file { - const command = try setupBash(alloc, "bash --init-file profile.sh", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix", command.?); + const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env); + try testing.expectEqual(2, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } } test "bash: HISTFILE" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); // HISTFILE unset { var env = EnvMap.init(alloc); defer env.deinit(); - const command = try setupBash(alloc, "bash", ".", &env); - defer if (command) |c| alloc.free(c); - + _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); try testing.expect(std.mem.endsWith(u8, env.get("HISTFILE").?, ".bash_history")); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE").?); } @@ -430,9 +458,7 @@ test "bash: HISTFILE" { try env.put("HISTFILE", "my_history"); - const command = try setupBash(alloc, "bash", ".", &env); - defer if (command) |c| alloc.free(c); - + _ = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); try testing.expectEqualStrings("my_history", env.get("HISTFILE").?); try testing.expect(env.get("GHOSTTY_BASH_UNEXPORT_HISTFILE") == null); } @@ -440,25 +466,35 @@ test "bash: HISTFILE" { test "bash: additional arguments" { const testing = std.testing; - const alloc = testing.allocator; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); var env = EnvMap.init(alloc); defer env.deinit(); // "-" argument separator { - const command = try setupBash(alloc, "bash - --arg file1 file2", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix - --arg file1 file2", command.?); + const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env); + try testing.expectEqual(6, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); + try testing.expectEqualStrings("-", command.?.direct[2]); + try testing.expectEqualStrings("--arg", command.?.direct[3]); + try testing.expectEqualStrings("file1", command.?.direct[4]); + try testing.expectEqualStrings("file2", command.?.direct[5]); } // "--" argument separator { - const command = try setupBash(alloc, "bash -- --arg file1 file2", ".", &env); - defer if (command) |c| alloc.free(c); - - try testing.expectEqualStrings("bash --posix -- --arg file1 file2", command.?); + const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env); + try testing.expectEqual(6, command.?.direct.len); + try testing.expectEqualStrings("bash", command.?.direct[0]); + try testing.expectEqualStrings("--posix", command.?.direct[1]); + try testing.expectEqualStrings("--", command.?.direct[2]); + try testing.expectEqualStrings("--arg", command.?.direct[3]); + try testing.expectEqualStrings("file1", command.?.direct[4]); + try testing.expectEqualStrings("file2", command.?.direct[5]); } } From 49a97a589c51c367258bba4810ee49bdbc587de8 Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Thu, 10 Apr 2025 17:45:35 -0500 Subject: [PATCH 036/642] fix typo - exiting to existing Co-authored-by: Leah Amelia Chen --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index d3cf58537..35ed2dc33 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -135,5 +135,5 @@ body: options: - label: I have reviewed the FAQ and confirm that my issue is NOT among them. required: true - - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an exiting issue or discussion. + - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion. required: true From f0ade53fd23bb029a52093aa2dfb16739fb30f45 Mon Sep 17 00:00:00 2001 From: trag1c Date: Fri, 11 Apr 2025 00:48:24 +0200 Subject: [PATCH 037/642] ci: add a script and workflow for requesting i18n review --- .github/scripts/request_review.py | 115 ++++++++++++++++++++++++++++++ .github/workflows/review.yml | 37 ++++++++++ flake.nix | 1 + nix/devShell.nix | 4 ++ 4 files changed, 157 insertions(+) create mode 100644 .github/scripts/request_review.py create mode 100644 .github/workflows/review.yml diff --git a/.github/scripts/request_review.py b/.github/scripts/request_review.py new file mode 100644 index 000000000..1a53e82e4 --- /dev/null +++ b/.github/scripts/request_review.py @@ -0,0 +1,115 @@ +# /// script +# requires-python = ">=3.9" +# dependencies = [ +# "githubkit", +# ] +# /// + +import asyncio +import os +import re +from itertools import chain + +from githubkit import GitHub + +ORG_NAME = "ghostty-org" +REPO_NAME = "ghostty" +ALLOWED_PARENT_TEAM = "localization" +LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}") + +gh = GitHub(os.environ["GITHUB_TOKEN"]) + + +async def fetch_and_parse_codeowners() -> dict[str, str]: + content = ( + await gh.rest.repos.async_get_content( + ORG_NAME, + REPO_NAME, + "CODEOWNERS", + headers={"Accept": "application/vnd.github.raw+json"}, + ) + ).text + + codeowners: dict[str, str] = {} + for line in content.splitlines(): + if not line or line.lstrip().startswith("#"): + continue + # This assumes that all entries only list one owner + # and that this owner is a team (ghostty-org/foobar) + path, owner = line.split() + codeowners[path.lstrip("/")] = owner.removeprefix(f"@{ORG_NAME}/") + return codeowners + + +async def get_team_members(team_name: str) -> list[str]: + team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data + if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM: + members = ( + await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) + ).parsed_data + return [m.login for m in members] + return [] + + +async def get_changed_files(pr_number: int) -> list[str]: + diff_entries = ( + await gh.rest.pulls.async_list_files( + ORG_NAME, + REPO_NAME, + pr_number, + per_page=3000, + headers={"Accept": "application/vnd.github+json"}, + ) + ).parsed_data + return [d.filename for d in diff_entries] + + +async def request_review(pr_number: int, pr_author: str, *users: str) -> None: + await asyncio.gather( + *( + gh.rest.pulls.async_request_reviewers( + ORG_NAME, + REPO_NAME, + pr_number, + headers={"Accept": "application/vnd.github+json"}, + data={"reviewers": [user]}, + ) + for user in users + if user != pr_author + ) + ) + + +def is_localization_team(team_name: str) -> bool: + return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None + + +async def main() -> None: + pr_number = int(os.environ["PR_NUMBER"]) + changed_files = await get_changed_files(pr_number) + pr_author = ( + await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) + ).parsed_data.user.login + localization_codewners = { + path: owner + for path, owner in (await fetch_and_parse_codeowners()).items() + if is_localization_team(owner) + } + + found_owners = set[str]() + for file in changed_files: + for path, owner in localization_codewners.items(): + if file.startswith(path): + break + else: + continue + found_owners.add(owner) + + member_lists = await asyncio.gather( + *(get_team_members(owner) for owner in found_owners) + ) + await request_review(pr_number, pr_author, *chain.from_iterable(member_lists)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml new file mode 100644 index 000000000..9abe0b5e2 --- /dev/null +++ b/.github/workflows/review.yml @@ -0,0 +1,37 @@ +name: Request Review + +on: + pull_request: + types: + - opened + - synchronize + +env: + PY_COLORS: 1 + +jobs: + review: + runs-on: namespace-profile-ghostty-xsm + steps: + - uses: actions/checkout@v4 + + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@v1.2.0 + with: + path: | + /nix + /zig + + - uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v15 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Request Localization Review + env: + GITHUB_TOKEN: ${{ secrets.GH_REVIEW_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: nix develop -c uv run .github/scripts/request_review.py diff --git a/flake.nix b/flake.nix index c8e53d7e9..d4c6aa6ca 100644 --- a/flake.nix +++ b/flake.nix @@ -51,6 +51,7 @@ devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix { zig = zig.packages.${system}."0.14.0"; wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {}; + uv = pkgs-unstable.uv; # remove once blueprint-compiler 0.16.0 is in the stable nixpkgs blueprint-compiler = pkgs-unstable.blueprint-compiler; zon2nix = zon2nix; diff --git a/nix/devShell.nix b/nix/devShell.nix index 6949744d0..5b69f882b 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -57,6 +57,7 @@ pandoc, hyperfine, typos, + uv, wayland, wayland-scanner, wayland-protocols, @@ -109,6 +110,9 @@ in # Localization gettext + # CI + uv + # We need these GTK-related deps on all platform so we can build # dist tarballs. blueprint-compiler From 02bb81ad44155581cb492748b7fb323fd419d6ca Mon Sep 17 00:00:00 2001 From: halosatrio <63773815+halosatrio@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:33:28 +0700 Subject: [PATCH 038/642] i18n: add indonesian translation --- po/id_ID.UTF-8.po | 266 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 po/id_ID.UTF-8.po diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po new file mode 100644 index 000000000..d5acf3b21 --- /dev/null +++ b/po/id_ID.UTF-8.po @@ -0,0 +1,266 @@ +# Indonesian translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Satrio Bayu Aji , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-20 15:19+0700\n" +"Last-Translator: Satrio Bayu Aji \n" +"Language-Team: Indonesian \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 +msgid "Change Terminal Title" +msgstr "Ubah Judul Terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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 +msgid "Cancel" +msgstr "Batal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Kesalahan Konfigurasi" + +#: src/apprt/gtk/ui/1.5/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 "" +"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di bawah ini, " +"dan muat ulang konfigurasi anda atau abaikan kesalahan ini." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Muat ulang Konfigurasi" + +#: 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 "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 +msgid "Paste" +msgstr "Tempel" + +#: 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 "Hapus" + +#: 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 "Atur ulang" + +#: 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 "Bagi panel" + +#: 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 "Ubah Judul…" + +#: 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 "Bagi panel ke Atas" + +#: 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 "Bagi panel ke Bawah" + +#: 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 "Bagi panel ke Kiri" + +#: 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 "Bagi panel ke Kanan" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +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:246 +msgid "New Tab" +msgstr "Tab Baru" + +#: 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 "Tutup Tab" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Jendela" + +#: 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 "Jendela Baru" + +#: 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 "Tutup Jendela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +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 +msgid "Open Configuration" +msgstr "Buka Konfigurasi" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspektur Terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Tentang Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Mengesahkan Akses Papan klip" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip " +"saat ini ditampilkan di bawah ini." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/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 +msgid "Allow" +msgstr "Izinkan" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Peringatan: Tempelan yang Berpotensi Tidak aman" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya +"beberapa perintah mungkin dijalankan." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspektur Terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Disalin ke papan klip" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Tutup" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Keluar dari Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Tutup Jendela?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Tutup Tab?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Tutup Layar?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Semua sesi terminal akan diakhiri." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Semua sesi terminal di jendela ini akan diakhiri." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Semua sesi terminal di tab ini akan diakhiri." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Proses yang sedang berjalan dalam layar ini akan dihentikan." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Menu Utama" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Lihat Tabs Terbuka" + +#: src/apprt/gtk/Window.zig:295 +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:725 +msgid "Reloaded the configuration" +msgstr "Memuat ulang konfigurasi" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Pengembang Ghostty" From d903cc9827d29cd2edc156cdff338ec2fb3a93e7 Mon Sep 17 00:00:00 2001 From: halosatrio <63773815+halosatrio@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:39:56 +0700 Subject: [PATCH 039/642] i18n: fix translation --- po/id_ID.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index d5acf3b21..717671a64 100644 --- a/po/id_ID.UTF-8.po +++ b/po/id_ID.UTF-8.po @@ -197,7 +197,7 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya +"Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya " "beberapa perintah mungkin dijalankan." #: src/apprt/gtk/inspector.zig:144 @@ -226,7 +226,7 @@ msgstr "Tutup Tab?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Tutup Layar?" +msgstr "Tutup Bagi panel?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." From 1222e80eb14ea4f14cee18174499a02fecdb0e57 Mon Sep 17 00:00:00 2001 From: halosatrio <63773815+halosatrio@users.noreply.github.com> Date: Thu, 20 Mar 2025 15:48:03 +0700 Subject: [PATCH 040/642] i18n: add id_ID code locale --- src/os/i18n.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/os/i18n.zig b/src/os/i18n.zig index baae73e46..80c63352f 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -29,6 +29,7 @@ pub const locales = [_][:0]const u8{ "nb_NO.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", + "id_ID.UTF-8", }; /// Set for faster membership lookup of locales. From 561a0e3897a53b07f326e364c244d43c5c3c8205 Mon Sep 17 00:00:00 2001 From: halosatrio <63773815+halosatrio@users.noreply.github.com> Date: Thu, 20 Mar 2025 18:41:04 +0700 Subject: [PATCH 041/642] i18n: fix capitalization and some translation --- po/id_ID.UTF-8.po | 48 +++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index 717671a64..c8b89a89e 100644 --- a/po/id_ID.UTF-8.po +++ b/po/id_ID.UTF-8.po @@ -18,7 +18,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Ubah Judul Terminal" +msgstr "Ubah judul terminal" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." @@ -35,7 +35,7 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Kesalahan Konfigurasi" +msgstr "Kesalahan konfigurasi" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 msgid "" @@ -53,7 +53,7 @@ msgstr "Abaikan" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Reload Configuration" -msgstr "Muat ulang Konfigurasi" +msgstr "Muat ulang konfigurasi" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -79,32 +79,32 @@ msgstr "Atur ulang" #: 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 "Bagi panel" +msgstr "Belah" #: 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 "Ubah Judul…" +msgstr "Ubah judul…" #: 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 "Bagi panel ke Atas" +msgstr "Belah atas" #: 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 "Bagi panel ke Bawah" +msgstr "Belah bawah" #: 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 "Bagi panel ke Kiri" +msgstr "Belah kiri" #: 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 "Bagi panel ke Kanan" +msgstr "Belah kanan" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" @@ -114,12 +114,12 @@ msgstr "Tab" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: src/apprt/gtk/Window.zig:246 msgid "New Tab" -msgstr "Tab Baru" +msgstr "Tab baru" #: 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 "Tutup Tab" +msgstr "Tutup tab" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 msgid "Window" @@ -128,12 +128,12 @@ msgstr "Jendela" #: 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 "Jendela Baru" +msgstr "Jendela baru" #: 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 "Tutup Jendela" +msgstr "Tutup jendela" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" @@ -142,11 +142,11 @@ 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 msgid "Open Configuration" -msgstr "Buka Konfigurasi" +msgstr "Buka konfigurasi" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Terminal Inspector" -msgstr "Inspektur Terminal" +msgstr "Inspektur terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 @@ -160,7 +160,7 @@ 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 msgid "Authorize Clipboard Access" -msgstr "Mengesahkan Akses Papan klip" +msgstr "Mengesahkan akses papan klip" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" @@ -190,7 +190,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" -msgstr "Peringatan: Tempelan yang Berpotensi Tidak aman" +msgstr "Peringatan: Tempelan yang berpotensi tidak aman" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 msgid "" @@ -202,7 +202,7 @@ msgstr "" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspektur Terminal" +msgstr "Ghostty: Inspektur terminal" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" @@ -218,15 +218,15 @@ msgstr "Keluar dari Ghostty?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" -msgstr "Tutup Jendela?" +msgstr "Tutup jendela?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Tutup Tab?" +msgstr "Tutup tab?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Tutup Bagi panel?" +msgstr "Tutup belahan?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -242,15 +242,15 @@ msgstr "Semua sesi terminal di tab ini akan diakhiri." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "Proses yang sedang berjalan dalam layar ini akan dihentikan." +msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." #: src/apprt/gtk/Window.zig:200 msgid "Main Menu" -msgstr "Menu Utama" +msgstr "Menu utama" #: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" -msgstr "Lihat Tabs Terbuka" +msgstr "Lihat tab terbuka" #: src/apprt/gtk/Window.zig:295 msgid "" From 7adc2954c3b2a29892aa460cc13bd296ce1d5081 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Apr 2025 09:26:59 -0700 Subject: [PATCH 042/642] update CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index b76c7b3da..59998001c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -159,6 +159,7 @@ /po/README_TRANSLATORS.md @ghostty-org/localization /po/com.mitchellh.ghostty.pot @ghostty-org/localization /po/de_DE.UTF-8.po @ghostty-org/de_DE +/po/id_ID.UTF-8.po @ghostty-org/id_ID /po/nb_NO.UTF-8.po @ghostty-org/nb_NO /po/pl_PL.UTF-8.po @ghostty-org/pl_PL /po/uk_UA.UTF-8.po @ghostty-org/uk_UA From 20ef2150de979d27e1f5e0117d40adb570dfc21c Mon Sep 17 00:00:00 2001 From: Lon Sagisawa Date: Sat, 22 Mar 2025 15:21:10 +0900 Subject: [PATCH 043/642] i18n: Add Japanese translations --- CODEOWNERS | 1 + po/ja_JP.UTF-8.po | 268 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 3 files changed, 270 insertions(+) create mode 100644 po/ja_JP.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 59998001c..a7571ae43 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -160,6 +160,7 @@ /po/com.mitchellh.ghostty.pot @ghostty-org/localization /po/de_DE.UTF-8.po @ghostty-org/de_DE /po/id_ID.UTF-8.po @ghostty-org/id_ID +/po/ja_JP.UTF-8.po @ghostty-org/ja_JP /po/nb_NO.UTF-8.po @ghostty-org/nb_NO /po/pl_PL.UTF-8.po @ghostty-org/pl_PL /po/uk_UA.UTF-8.po @ghostty-org/uk_UA diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po new file mode 100644 index 000000000..8cdb0b38c --- /dev/null +++ b/po/ja_JP.UTF-8.po @@ -0,0 +1,268 @@ +# Japanese translations for com.mitchellh.ghostty package +# com.mitchellh.ghostty パッケージに対する英訳. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Lon Sagisawa , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-21 00:08+0900\n" +"Last-Translator: Lon Sagisawa \n" +"Language-Team: Japanese\n" +"Language: ja\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\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/CloseDialog.zig:44 +msgid "Cancel" +msgstr "キャンセル" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "設定エラー" + +#: src/apprt/gtk/ui/1.5/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 +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 +msgid "Reload Configuration" +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 +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: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 +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:90 +msgid "Open Configuration" +msgstr "設定ファイルを開く" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "端末インスペクター" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Ghosttyについて" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "クリップボードへのアクセスを承認" + +#: src/apprt/gtk/ui/1.5/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 +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 +msgid "Allow" +msgstr "許可" + +#: src/apprt/gtk/ui/1.5/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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "警告: 危険な可能性のあるペースト" + +#: src/apprt/gtk/ui/1.5/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 "クリップボードにコピーしました" + +#: 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/Window.zig:200 +msgid "Main Menu" +msgstr "メインメニュー" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "開いているすべてのタブを表示" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Ghosttyのデバッグビルドを実行しています! パフォーマンスが低下しています。" + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "設定を再読み込みしました" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty 開発者" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 80c63352f..9a56eade3 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -29,6 +29,7 @@ pub const locales = [_][:0]const u8{ "nb_NO.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", + "ja_JP.UTF-8", "id_ID.UTF-8", }; From 10a90b5b67f1d3d9d28ce14d006f27b7ec603692 Mon Sep 17 00:00:00 2001 From: Lon Sagisawa Date: Sat, 22 Mar 2025 17:57:02 +0900 Subject: [PATCH 044/642] fix: inconsistency around space --- po/ja_JP.UTF-8.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index 8cdb0b38c..7a4ee6929 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -153,7 +153,7 @@ msgstr "端末インスペクター" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 msgid "About Ghostty" -msgstr "Ghosttyについて" +msgstr "Ghostty について" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 msgid "Quit" @@ -216,7 +216,7 @@ msgstr "閉じる" #: src/apprt/gtk/CloseDialog.zig:87 msgid "Quit Ghostty?" -msgstr "Ghosttyを終了しますか?" +msgstr "Ghostty を終了しますか?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" @@ -257,7 +257,7 @@ msgstr "開いているすべてのタブを表示" #: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Ghosttyのデバッグビルドを実行しています! パフォーマンスが低下しています。" +msgstr "⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From e2a8a3243c00f248c7b2296ff436cbcea51f7298 Mon Sep 17 00:00:00 2001 From: Kirwiisp <59315476+Kirwiisp@users.noreply.github.com> Date: Sat, 22 Mar 2025 09:23:01 +0100 Subject: [PATCH 045/642] i18n: Add French translation --- CODEOWNERS | 1 + po/fr_FR.UTF-8.po | 267 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 3 +- 3 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 po/fr_FR.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index a7571ae43..fac1a44f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -165,6 +165,7 @@ /po/pl_PL.UTF-8.po @ghostty-org/pl_PL /po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/zh_CN.UTF-8.po @ghostty-org/zh_CN +/po/fr_FR.UTF-8.po @ghostty-org/fr_FR # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po new file mode 100644 index 000000000..3b805133e --- /dev/null +++ b/po/fr_FR.UTF-8.po @@ -0,0 +1,267 @@ +# French translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Kirwiisp , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-22 09:31+0100\n" +"Last-Translator: Kirwiisp \n" +"Language-Team: French \n" +"Language: fr\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 "Changer nom du terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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 +msgid "Cancel" +msgstr "Annuler" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Erreur de configuration" + +#: src/apprt/gtk/ui/1.5/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 "Une ou plusieurs erreurs de configuration ont été trouvée(s). Veuillez lire les erreurs ci-dessous, et recharger votre configuration ou bien ignorer ces erreurs." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Recharger la configuration" + +#: 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 "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 +msgid "Paste" +msgstr "Coller" + +#: 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 "Tout effacer" + +#: 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 "Réinitialiser" + +#: 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 "Créer panneau" + +#: 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 "Changer le titre" + +#: 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 "Panneau en haut" + +#: 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 "Panneau en bas" + +#: 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 "Panneau à gauche" + +#: 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 "Panneau à droite" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +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:246 +msgid "New Tab" +msgstr "Nouvel onglet" + +#: 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 "Fermer onglet" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Fenêtre" + +#: 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 "Nouvelle fenêtre" + +#: 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 "Fermer la fenêtre" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +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 +msgid "Open Configuration" +msgstr "Ouvrir la configuration" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspecteur de terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "À propos de Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Autoriser l'accès au presse-papier" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Une application essai de lire depuis le presse-papier." +"Le contenu actuel du presse-papier est affiché ci-dessous" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/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 +msgid "Allow" +msgstr "Autoriser" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Une application essai d'écrire dans le presse-papier." +"Le contenu actuel du presse-papier est affiché ci-dessous" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Warning: Collage potentiellement non sécurisé" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Coller ce texte dans le terminal pourrait être dangereux, " +"il semblerait que certaines commandes pourraient être exécutées" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspecteur" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copié dans le presse-papier" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Fermer" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Quitter Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Fermer la fenêtre?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Fermer l'onglet?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Fermer le panneau?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Toutes les sessions vont être arrêtées" + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Toutes les sessions de cette fenêtre vont être arrêtées" + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Toutes les sessions de cet onglet vont être arrêtées" + +#: src/apprt/gtk/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Menu principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Voir les onglets ouverts" + +#: src/apprt/gtk/Window.zig:295 +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:725 +msgid "Reloaded the configuration" +msgstr "Recharger la configuration" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Les developpeurs de Ghostty" + diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 9a56eade3..f0d815c92 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -24,8 +24,9 @@ const log = std.log.scoped(.i18n); /// 3. Most preferred locale for a language without a country code. /// pub const locales = [_][:0]const u8{ - "de_DE.UTF-8", "zh_CN.UTF-8", + "de_DE.UTF-8", + "fr_FR.UTF-8", "nb_NO.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", From 1aa16cdf6b0c9d84dbd79142e80ed2aefd170c74 Mon Sep 17 00:00:00 2001 From: Nicolas G <59315476+Kirwiisp@users.noreply.github.com> Date: Sat, 22 Mar 2025 10:30:33 +0100 Subject: [PATCH 046/642] Fix ponctuation and Warning translation Fix: missing ponctuations on various lines. Change "Warning" to "Attention", might change in the futur if it does not perfectly match common practice. Fix: Multiline on config errors dialog --- po/fr_FR.UTF-8.po | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 3b805133e..2bf391ceb 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -23,7 +23,7 @@ msgstr "Changer nom du terminal" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." -msgstr "Laisser vide pour restaurer le titre par défaut" +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 @@ -42,7 +42,9 @@ msgstr "Erreur de configuration" msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." -msgstr "Une ou plusieurs erreurs de configuration ont été trouvée(s). Veuillez lire les erreurs ci-dessous, et recharger votre configuration ou bien ignorer ces erreurs." +msgstr "" +"Une ou plusieurs erreurs de configuration ont été trouvée(s). Veuillez lire les erreurs ci-dessous," +"et recharger votre configuration ou bien ignorer ces erreurs." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -83,7 +85,7 @@ msgstr "Créer panneau" #: 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 "Changer le titre" +msgstr "Changer le titre…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 @@ -167,7 +169,7 @@ msgid "" "clipboard contents are shown below." msgstr "" "Une application essai de lire depuis le presse-papier." -"Le contenu actuel du presse-papier est affiché ci-dessous" +"Le contenu actuel du presse-papier est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -185,11 +187,11 @@ msgid "" "clipboard contents are shown below." msgstr "" "Une application essai d'écrire dans le presse-papier." -"Le contenu actuel du presse-papier est affiché ci-dessous" +"Le contenu actuel du presse-papier est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" -msgstr "Warning: Collage potentiellement non sécurisé" +msgstr "Attention: Collage potentiellement dangereux" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 msgid "" @@ -197,7 +199,7 @@ msgid "" "commands may be executed." msgstr "" "Coller ce texte dans le terminal pourrait être dangereux, " -"il semblerait que certaines commandes pourraient être exécutées" +"il semblerait que certaines commandes pourraient être exécutées." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" @@ -229,19 +231,19 @@ msgstr "Fermer le panneau?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." -msgstr "Toutes les sessions vont être arrêtées" +msgstr "Toutes les sessions vont être arrêtées." #: src/apprt/gtk/CloseDialog.zig:97 msgid "All terminal sessions in this window will be terminated." -msgstr "Toutes les sessions de cette fenêtre vont être arrêtées" +msgstr "Toutes les sessions de cette fenêtre vont être arrêtées." #: src/apprt/gtk/CloseDialog.zig:98 msgid "All terminal sessions in this tab will be terminated." -msgstr "Toutes les sessions de cet onglet vont être arrêtées" +msgstr "Toutes les sessions de cet onglet vont être arrêtées." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "Le processus en cours dans ce panneau va être arrêté" +msgstr "Le processus en cours dans ce panneau va être arrêté." #: src/apprt/gtk/Window.zig:200 msgid "Main Menu" @@ -264,4 +266,3 @@ msgstr "Recharger la configuration" #: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Les developpeurs de Ghostty" - From 930079ca01d49cb22545ed723592b9430db4abc5 Mon Sep 17 00:00:00 2001 From: Kirwiisp <59315476+Kirwiisp@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:05:19 +0100 Subject: [PATCH 047/642] fix: spelling mistakes --- po/fr_FR.UTF-8.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 2bf391ceb..2e9609d94 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -19,7 +19,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Changer nom du terminal" +msgstr "Changer le nom du terminal" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." @@ -36,14 +36,14 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Erreur de configuration" +msgstr "Erreurs de configuration" #: src/apprt/gtk/ui/1.5/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 "" -"Une ou plusieurs erreurs de configuration ont été trouvée(s). Veuillez lire les erreurs ci-dessous," +"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire les erreurs ci-dessous," "et recharger votre configuration ou bien ignorer ces erreurs." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 @@ -161,15 +161,15 @@ 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 msgid "Authorize Clipboard Access" -msgstr "Autoriser l'accès au presse-papier" +msgstr "Autoriser l'accès au presse-papiers" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essai de lire depuis le presse-papier." -"Le contenu actuel du presse-papier est affiché ci-dessous." +"Une application essai de lire depuis le presse-papiers." +"Le contenu actuel du presse-papiers est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -186,7 +186,7 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essai d'écrire dans le presse-papier." +"Une application essaie d'écrire dans le presse-papiers." "Le contenu actuel du presse-papier est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 @@ -207,7 +207,7 @@ msgstr "Ghostty: Inspecteur" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" -msgstr "Copié dans le presse-papier" +msgstr "Copié dans le presse-papiers" #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" @@ -265,4 +265,4 @@ msgstr "Recharger la configuration" #: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" -msgstr "Les developpeurs de Ghostty" +msgstr "Les développeurs de Ghostty" From 5ca5afb13db4db8685664504ce6d4106a8f36970 Mon Sep 17 00:00:00 2001 From: Kirwiisp <59315476+Kirwiisp@users.noreply.github.com> Date: Wed, 26 Mar 2025 13:01:20 +0100 Subject: [PATCH 048/642] fix: spelling and typo --- po/fr_FR.UTF-8.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 2e9609d94..fc5bfd054 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -168,7 +168,7 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essai de lire depuis le presse-papiers." +"Une application essaie de lire depuis le presse-papiers." "Le contenu actuel du presse-papiers est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 @@ -187,7 +187,7 @@ msgid "" "clipboard contents are shown below." msgstr "" "Une application essaie d'écrire dans le presse-papiers." -"Le contenu actuel du presse-papier est affiché ci-dessous." +"Le contenu actuel du presse-papiers est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" @@ -215,19 +215,19 @@ msgstr "Fermer" #: src/apprt/gtk/CloseDialog.zig:87 msgid "Quit Ghostty?" -msgstr "Quitter Ghostty?" +msgstr "Quitter Ghostty ?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" -msgstr "Fermer la fenêtre?" +msgstr "Fermer la fenêtre ?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Fermer l'onglet?" +msgstr "Fermer l'onglet ?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Fermer le panneau?" +msgstr "Fermer le panneau ?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -257,7 +257,7 @@ msgstr "Voir les onglets ouverts" 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." +"⚠️ Vous utilisez une version de débogage de Ghostty ! Les performances seront dégradées." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From 5be4b0de6b7e43b5b9a0f842f3821114d2d11716 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Apr 2025 09:42:45 -0700 Subject: [PATCH 049/642] CODEOWNERS --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index fac1a44f9..1dbe5af17 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -159,13 +159,13 @@ /po/README_TRANSLATORS.md @ghostty-org/localization /po/com.mitchellh.ghostty.pot @ghostty-org/localization /po/de_DE.UTF-8.po @ghostty-org/de_DE +/po/fr_FR.UTF-8.po @ghostty-org/fr_FR /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP /po/nb_NO.UTF-8.po @ghostty-org/nb_NO /po/pl_PL.UTF-8.po @ghostty-org/pl_PL /po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/zh_CN.UTF-8.po @ghostty-org/zh_CN -/po/fr_FR.UTF-8.po @ghostty-org/fr_FR # Packaging - Snap /snap/ @ghostty-org/snap From df5dd1858affe71fdd401fe11c47f2218508bf18 Mon Sep 17 00:00:00 2001 From: blackzeshi Date: Mon, 24 Mar 2025 00:34:58 +0500 Subject: [PATCH 050/642] add russian translation --- po/ru_RU.UTF-8.po | 261 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 2 files changed, 262 insertions(+) create mode 100644 po/ru_RU.UTF-8.po diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po new file mode 100644 index 000000000..f867f4f77 --- /dev/null +++ b/po/ru_RU.UTF-8.po @@ -0,0 +1,261 @@ +# Russian translations for com.mitchellh.ghostty package +# Английские переводы для пакета com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# blackzeshi , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-24 00:01+0500\n" +"Last-Translator: blackzeshi \n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\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/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 +msgid "Configuration Errors" +msgstr "Ошибки конфигурации" + +#: src/apprt/gtk/ui/1.5/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 +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 +msgid "Reload Configuration" +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 +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: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 +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:90 +msgid "Open Configuration" +msgstr "Открыть конфигурационный файл" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Инспектор терминала" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "О Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Разрешить доступ к буферу обмена" + +#: src/apprt/gtk/ui/1.5/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 +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 +msgid "Allow" +msgstr "Разрешить" + +#: src/apprt/gtk/ui/1.5/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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Внимание! Вставляемые данные могут нанести вред вашей системе" + +#: src/apprt/gtk/ui/1.5/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 "Скопировано в буфер обмена" + +#: 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/Window.zig:200 +msgid "Main Menu" +msgstr "Главное меню" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Просмотреть открытые вкладки" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "Вы запустили Ghostty в режиме отладки! Это может влиять на производительность." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Конфигурация была обновлена" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Разработчики Ghostty" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index f0d815c92..bb76e2011 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -32,6 +32,7 @@ pub const locales = [_][:0]const u8{ "pl_PL.UTF-8", "ja_JP.UTF-8", "id_ID.UTF-8", + "ru_RU.UTF-8", }; /// Set for faster membership lookup of locales. From 91f9fdb1be8b5a2a40367cec24f9e28ee58b6656 Mon Sep 17 00:00:00 2001 From: blackzeshi Date: Mon, 24 Mar 2025 20:54:30 +0500 Subject: [PATCH 051/642] some fixes to adding russian translation (i.e. emoji) --- po/ru_RU.UTF-8.po | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index f867f4f77..86e6648f6 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -44,7 +44,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 msgid "Ignore" @@ -167,7 +169,9 @@ msgstr "Разрешить доступ к буферу обмена" 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 @@ -183,7 +187,9 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -193,7 +199,9 @@ msgstr "Внимание! Вставляемые данные могут нан msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "Вставка этого текста в терминал может быть опасной. Это выглядит как команды, которые могут быть исполнены." +msgstr "" +"Вставка этого текста в терминал может быть опасной. Это выглядит " +"как команды, которые могут быть исполнены." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" @@ -250,7 +258,8 @@ msgstr "Просмотреть открытые вкладки" #: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "Вы запустили Ghostty в режиме отладки! Это может влиять на производительность." +msgstr "" +"⚠️ Вы запустили Ghostty в режиме отладки! Это может влиять на производительность." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From 0d23f7af31905ea3eb78373cb2e37296e0cd618a Mon Sep 17 00:00:00 2001 From: blackzeshi <105582686+zeshi09@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:34:12 +0500 Subject: [PATCH 052/642] Update po/ru_RU.UTF-8.po 248 line changed Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 86e6648f6..f10db2d3b 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -245,7 +245,7 @@ msgstr "Все сессии терминала в этой вкладке буд #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "Запущенный процесс в этой сплит-области будет остановлен." +msgstr "Процесс, работающий в этой сплит-области, будет остановлен." #: src/apprt/gtk/Window.zig:200 msgid "Main Menu" From f4054daf0d7cda0928be9b0df8686e95460352d7 Mon Sep 17 00:00:00 2001 From: blackzeshi <105582686+zeshi09@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:35:19 +0500 Subject: [PATCH 053/642] Update po/ru_RU.UTF-8.po 262 line about debug build fixed Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index f10db2d3b..1cce96232 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -259,7 +259,7 @@ msgstr "Просмотреть открытые вкладки" msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -"⚠️ Вы запустили Ghostty в режиме отладки! Это может влиять на производительность." +"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на производительность." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From d0403021a4332e251672c54bef9f6c09dd12adac Mon Sep 17 00:00:00 2001 From: blackzeshi <105582686+zeshi09@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:35:58 +0500 Subject: [PATCH 054/642] Update po/ru_RU.UTF-8.po 2 line fixed Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 1cce96232..0811d2338 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -1,5 +1,5 @@ # Russian translations for com.mitchellh.ghostty package -# Английские переводы для пакета com.mitchellh.ghostty. +# Русские переводы для пакета com.mitchellh.ghostty. # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. # blackzeshi , 2025. From 0d226d139baf29c792866ed47265c826cf072620 Mon Sep 17 00:00:00 2001 From: blackzeshi <105582686+zeshi09@users.noreply.github.com> Date: Sun, 30 Mar 2025 15:18:02 +0500 Subject: [PATCH 055/642] Update po/ru_RU.UTF-8.po Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 0811d2338..50e67fa48 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -92,7 +92,7 @@ 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 "Сплит наверх" +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 From 077ae6a09807322f911314900ff8e4619c208d82 Mon Sep 17 00:00:00 2001 From: blackzeshi <105582686+zeshi09@users.noreply.github.com> Date: Sun, 30 Mar 2025 15:18:24 +0500 Subject: [PATCH 056/642] Update po/ru_RU.UTF-8.po Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 50e67fa48..a3c21a246 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -45,8 +45,8 @@ 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 msgid "Ignore" From 52ac670913b3339256317d349564ed93762f95b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Apr 2025 09:47:09 -0700 Subject: [PATCH 057/642] CODEOWNERS --- CODEOWNERS | 1 + src/os/i18n.zig | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/CODEOWNERS b/CODEOWNERS index 1dbe5af17..e7066ed9f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -164,6 +164,7 @@ /po/ja_JP.UTF-8.po @ghostty-org/ja_JP /po/nb_NO.UTF-8.po @ghostty-org/nb_NO /po/pl_PL.UTF-8.po @ghostty-org/pl_PL +/po/ru_RU.UTF-8.po @ghostty-org/ru_RU /po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/zh_CN.UTF-8.po @ghostty-org/zh_CN diff --git a/src/os/i18n.zig b/src/os/i18n.zig index d546bbe8f..eab726592 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -23,6 +23,12 @@ const log = std.log.scoped(.i18n); /// /// 3. Most preferred locale for a language without a country code. /// +/// Note for "most common" locales, this is subjective and based on +/// the perceived userbase of Ghostty, which may not be representative +/// of general populations or global language distribution. Also note +/// that ordering may be weird when we first merge a new locale since +/// we don't have a good way to determine this. We can always reorder +/// with some data. pub const locales = [_][:0]const u8{ "zh_CN.UTF-8", "de_DE.UTF-8", From e52aad5deac355e6d4a71dc8e2600d270c971f9f Mon Sep 17 00:00:00 2001 From: Nico Geesink Date: Mon, 24 Mar 2025 15:44:39 +0100 Subject: [PATCH 058/642] Add nl_NL (Dutch) translations --- po/nl_NL.UTF-8.po | 268 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 2 files changed, 269 insertions(+) create mode 100644 po/nl_NL.UTF-8.po diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po new file mode 100644 index 000000000..8039a0530 --- /dev/null +++ b/po/nl_NL.UTF-8.po @@ -0,0 +1,268 @@ +# Dutch translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-24 15:00+0100\n" +"Last-Translator: \n" +"Language-Team: Dutch \n" +"Language: nl\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 "Verander De Titel van De Terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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 +msgid "Cancel" +msgstr "Annuleren" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Configuratiefouten" + +#: src/apprt/gtk/ui/1.5/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 "" +"Een of meer configuratiefouten zijn gevonden. Bekijk de fouten hieronder, " +"en herlaad uw configuratie of negeer deze fouten." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Herlaad configuratie" + +#: 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 "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 +msgid "Paste" +msgstr "Plakken" + +#: 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 "Leegmaken" + +#: 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 "Herstellen" + +#: 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 "Splitsing" + +#: 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 "Verander Titel…" + +#: 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 "Splits Naar Boven" + +#: 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 "Splits Naar Beneden" + +#: 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 "Splits Naar Links" + +#: 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 "Splits Naar Rechts" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Tablad" + +#: 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 +msgid "New Tab" +msgstr "Nieuw Tablad" + +#: 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 "Sluit Tablad" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Venster" + +#: 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 "Nieuw Venster" + +#: 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 "Sluit Venster" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +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 +msgid "Open Configuration" +msgstr "Open Configuratie" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Terminal Inspecteur" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Over Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Verleen Toegang Tot Klembord" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Een applicatie probeert de inhoud van het klembord te lezen. De huidige " +"inhoud van het klembord wordt hieronder weergegeven." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/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 +msgid "Allow" +msgstr "Toestaan" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Waarschuwing: Mogelijk Onveilige Plakactie" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Het plakken van deze tekst tekst in de terminal is mogelijk gevaarlijk, omdat " +"het lijkt op een commando dat uitgevoerd kan worden." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Terminal Inspecteur" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Gekopieerd naar klembord" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Afsluiten" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Wilt U Ghostty Afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Wilt U Dit Venster Afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Wil U Dit Tablad Afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Wilt U Deze Splitsing Afsluiten?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Alle terminalsessies zullen worden beëindigd." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Alle terminalsessies binnen dit venster zullen worden beëindigd." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Alle terminalsessies binnen dit tablad zullen worden beëindigd." + +#: src/apprt/gtk/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Hoofdmenu" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Open Tabladen Bekijken" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ U draait een debug versie van Ghostty! Prestaties zullen minder zijn dan normaal." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "De configuratie is herladen" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty Ontwikkelaars" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index eab726592..fd51f1b2e 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -33,6 +33,7 @@ pub const locales = [_][:0]const u8{ "zh_CN.UTF-8", "de_DE.UTF-8", "fr_FR.UTF-8", + "nl_NL.UTF-8", "nb_NO.UTF-8", "ru_RU.UTF-8", "uk_UA.UTF-8", From 7db64b8e347e27b3586b83d4fe723139b9a8e448 Mon Sep 17 00:00:00 2001 From: Nico Geesink Date: Mon, 24 Mar 2025 19:53:51 +0100 Subject: [PATCH 059/642] Add name and don't use title case --- po/nl_NL.UTF-8.po | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 8039a0530..809423223 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -1,7 +1,7 @@ # Dutch translations for com.mitchellh.ghostty package. # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. -# , 2025. +# Nico Geesink , 2025. # msgid "" msgstr "" @@ -9,7 +9,7 @@ msgstr "" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" -"Last-Translator: \n" +"Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" @@ -19,7 +19,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Verander De Titel van De Terminal" +msgstr "Verander de titel van de terminal" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." @@ -85,27 +85,27 @@ msgstr "Splitsing" #: 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 "Verander Titel…" +msgstr "Verander titel…" #: 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 "Splits Naar Boven" +msgstr "Splits naar boven" #: 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 "Splits Naar Beneden" +msgstr "Splits naar beneden" #: 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 "Splits Naar Links" +msgstr "Splits naar links" #: 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 "Splits Naar Rechts" +msgstr "Splits naar rechts" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" @@ -115,12 +115,12 @@ msgstr "Tablad" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: src/apprt/gtk/Window.zig:246 msgid "New Tab" -msgstr "Nieuw Tablad" +msgstr "Nieuw tablad" #: 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 "Sluit Tablad" +msgstr "Sluit tablad" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 msgid "Window" @@ -129,12 +129,12 @@ msgstr "Venster" #: 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 "Nieuw Venster" +msgstr "Nieuw venster" #: 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 "Sluit Venster" +msgstr "Sluit venster" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" @@ -143,11 +143,11 @@ 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 msgid "Open Configuration" -msgstr "Open Configuratie" +msgstr "Open configuratie" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Terminal Inspector" -msgstr "Terminal Inspecteur" +msgstr "Terminal inspecteur" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 @@ -161,7 +161,7 @@ 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 msgid "Authorize Clipboard Access" -msgstr "Verleen Toegang Tot Klembord" +msgstr "Verleen toegang tot klembord" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" @@ -191,7 +191,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" -msgstr "Waarschuwing: Mogelijk Onveilige Plakactie" +msgstr "Waarschuwing: mogelijk onveilige plakactie" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 msgid "" @@ -203,7 +203,7 @@ msgstr "" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Terminal Inspecteur" +msgstr "Ghostty: terminal inspecteur" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" @@ -215,19 +215,19 @@ msgstr "Afsluiten" #: src/apprt/gtk/CloseDialog.zig:87 msgid "Quit Ghostty?" -msgstr "Wilt U Ghostty Afsluiten?" +msgstr "Wilt u Ghostty afsluiten?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" -msgstr "Wilt U Dit Venster Afsluiten?" +msgstr "Wilt u dit venster afsluiten?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Wil U Dit Tablad Afsluiten?" +msgstr "Wil u dit tablad afsluiten?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Wilt U Deze Splitsing Afsluiten?" +msgstr "Wilt u deze splitsing afsluiten?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -251,7 +251,7 @@ msgstr "Hoofdmenu" #: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" -msgstr "Open Tabladen Bekijken" +msgstr "Open tabladen bekijken" #: src/apprt/gtk/Window.zig:295 msgid "" @@ -265,4 +265,4 @@ msgstr "De configuratie is herladen" #: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" -msgstr "Ghostty Ontwikkelaars" +msgstr "Ghostty ontwikkelaars" From cd6a8f6a65dc3682ddeb66518f96a9648faded0e Mon Sep 17 00:00:00 2001 From: Nico Geesink Date: Wed, 26 Mar 2025 21:44:00 +0100 Subject: [PATCH 060/642] Fix typo --- po/nl_NL.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 809423223..abf3ea238 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -198,7 +198,7 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Het plakken van deze tekst tekst in de terminal is mogelijk gevaarlijk, omdat " +"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat " "het lijkt op een commando dat uitgevoerd kan worden." #: src/apprt/gtk/inspector.zig:144 From 960fcc275f0775009a0e94ed7bca523de2984019 Mon Sep 17 00:00:00 2001 From: Nico Geesink Date: Thu, 27 Mar 2025 20:41:50 +0100 Subject: [PATCH 061/642] Fix typos and make sentences more fluent --- po/nl_NL.UTF-8.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index abf3ea238..b64e5ee65 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -43,7 +43,7 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Een of meer configuratiefouten zijn gevonden. Bekijk de fouten hieronder, " +"Er zijn een of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " "en herlaad uw configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 @@ -80,7 +80,7 @@ msgstr "Herstellen" #: 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 "Splitsing" +msgstr "Splitsen" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 @@ -223,7 +223,7 @@ msgstr "Wilt u dit venster afsluiten?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Wil u dit tablad afsluiten?" +msgstr "Wilt u dit tablad afsluiten?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" From 059caef1183950bb5db59f961e4b2b6bd3872c6e Mon Sep 17 00:00:00 2001 From: Nico Geesink Date: Thu, 27 Mar 2025 21:10:45 +0100 Subject: [PATCH 062/642] Use informal 'you' and change verander to wijzig --- po/nl_NL.UTF-8.po | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index b64e5ee65..58bac50ee 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -19,7 +19,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" -msgstr "Verander de titel van de terminal" +msgstr "Titel van de terminal wijzigen" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 msgid "Leave blank to restore the default title." @@ -44,7 +44,7 @@ msgid "" "and either reload your configuration or ignore these errors." msgstr "" "Er zijn een of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " -"en herlaad uw configuratie of negeer deze fouten." +"en herlaad je configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -85,7 +85,7 @@ msgstr "Splitsen" #: 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 "Verander titel…" +msgstr "Wijzig titel…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 @@ -215,19 +215,19 @@ msgstr "Afsluiten" #: src/apprt/gtk/CloseDialog.zig:87 msgid "Quit Ghostty?" -msgstr "Wilt u Ghostty afsluiten?" +msgstr "Wil je Ghostty afsluiten?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" -msgstr "Wilt u dit venster afsluiten?" +msgstr "Wil je dit venster afsluiten?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Wilt u dit tablad afsluiten?" +msgstr "Wil je dit tablad afsluiten?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Wilt u deze splitsing afsluiten?" +msgstr "Wil je deze splitsing afsluiten?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -257,7 +257,7 @@ msgstr "Open tabladen bekijken" msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -"⚠️ U draait een debug versie van Ghostty! Prestaties zullen minder zijn dan normaal." +"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan normaal." #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" From b0b09bf034e3c62bee2928fede4f638abcfb20a3 Mon Sep 17 00:00:00 2001 From: Nico Geesink Date: Wed, 2 Apr 2025 21:03:01 +0200 Subject: [PATCH 063/642] Fix spell errors --- po/nl_NL.UTF-8.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 58bac50ee..6ebea478b 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -43,7 +43,7 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Er zijn een of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " +"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " "en herlaad je configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 @@ -109,18 +109,18 @@ msgstr "Splits naar rechts" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" -msgstr "Tablad" +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:246 msgid "New Tab" -msgstr "Nieuw tablad" +msgstr "Nieuw tabblad" #: 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 "Sluit tablad" +msgstr "Sluit tabblad" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 msgid "Window" @@ -223,7 +223,7 @@ msgstr "Wil je dit venster afsluiten?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Wil je dit tablad afsluiten?" +msgstr "Wil je dit tabblad afsluiten?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" @@ -239,7 +239,7 @@ msgstr "Alle terminalsessies binnen dit venster zullen worden beëindigd." #: src/apprt/gtk/CloseDialog.zig:98 msgid "All terminal sessions in this tab will be terminated." -msgstr "Alle terminalsessies binnen dit tablad zullen worden beëindigd." +msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." @@ -251,7 +251,7 @@ msgstr "Hoofdmenu" #: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" -msgstr "Open tabladen bekijken" +msgstr "Open tabbladen bekijken" #: src/apprt/gtk/Window.zig:295 msgid "" From 14cac67b98040ce8d7a1323987cbe7904780716b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Apr 2025 09:51:14 -0700 Subject: [PATCH 064/642] CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index e7066ed9f..a7992806a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -163,6 +163,7 @@ /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP /po/nb_NO.UTF-8.po @ghostty-org/nb_NO +/po/nl_NL.UTF-8.po @ghostty-org/nl_NL /po/pl_PL.UTF-8.po @ghostty-org/pl_PL /po/ru_RU.UTF-8.po @ghostty-org/ru_RU /po/uk_UA.UTF-8.po @ghostty-org/uk_UA From 913c6dc7dfc17e6385f1922c406383f5e00808a3 Mon Sep 17 00:00:00 2001 From: Emir SARI Date: Mon, 24 Mar 2025 22:18:41 +0300 Subject: [PATCH 065/642] feat: Add Turkish translations --- CODEOWNERS | 1 + po/tr_TR.UTF-8.po | 270 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 3 +- 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 po/tr_TR.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index a7992806a..7cd2c235b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -166,6 +166,7 @@ /po/nl_NL.UTF-8.po @ghostty-org/nl_NL /po/pl_PL.UTF-8.po @ghostty-org/pl_PL /po/ru_RU.UTF-8.po @ghostty-org/ru_RU +/po/tr_TR.UTF-8.po @ghostty-org/tr_TR /po/uk_UA.UTF-8.po @ghostty-org/uk_UA /po/zh_CN.UTF-8.po @ghostty-org/zh_CN diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po new file mode 100644 index 000000000..cee17a6a1 --- /dev/null +++ b/po/tr_TR.UTF-8.po @@ -0,0 +1,270 @@ +# Turkish translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Emir SARI , 2025. +# +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" +"PO-Revision-Date: 2025-03-24 22:01+0300\n" +"Last-Translator: Emir SARI \n" +"Language-Team: Turkish\n" +"Language: tr\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 "Uçbirim Başlığını Değiştir" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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 +msgid "Cancel" +msgstr "İptal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Tamam" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Yapılandırma Hataları" + +#: src/apprt/gtk/ui/1.5/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 "" +"Bir veya daha fazla yapılandırma hatası bulundu. Lütfen aşağıdaki hataları " +"gözden geçirin ve ardından ya yapılandırmanızı yeniden yükleyin ya da bu " +"hataları yok sayın." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Yapılandırmayı Yeniden Yükle" + +#: 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 "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 +msgid "Paste" +msgstr "Yapıştır" + +#: 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 "Temizle" + +#: 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 "Sıfırla" + +#: 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 "Böl" + +#: 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 "Başlığı Değiştir…" + +#: 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 "Yukarı Doğru Böl" + +#: 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 "Aşağı Doğru Böl" + +#: 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 "Sola Doğru Böl" + +#: 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 "Sağa Doğru Böl" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +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:246 +msgid "New Tab" +msgstr "Yeni Sekme" + +#: 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 "Sekmeyi Kapat" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Pencere" + +#: 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 "Yeni Pencere" + +#: 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 "Pencereyi Kapat" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +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 +msgid "Open Configuration" +msgstr "Yapılandırmayı Aç" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +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:960 +msgid "About Ghostty" +msgstr "Ghostty Hakkında" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Pano Erişimine İzin Ver" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Bir uygulama panodan okumaya çalışıyor. Geçerli pano içeriği aşağıda " +"gösterilmektedir." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/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 +msgid "Allow" +msgstr "İzin Ver" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Uyarı: Tehlikeli Olabilecek Yapıştırma" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Bu metni uçbirime yapıştırmak tehlikeli olabilir; çünkü bir komut " +"yürütülebilecekmiş gibi duruyor." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Uçbirim Denetçisi" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Kapat" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Ghostty’den Çık?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Pencereyi Kapat?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Sekmeyi Kapat?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Bölmeyi Kapat?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Bu penceredeki tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Bu sekmedeki tüm uçbirim oturumları sonlandırılacaktır." + +#: src/apprt/gtk/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Ana Menü" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Açık Sekmeleri Görüntüle" + +#: src/apprt/gtk/Window.zig:295 +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:725 +msgid "Reloaded the configuration" +msgstr "Yapılandırma yeniden yüklendi" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Ghostty Geliştiricileri" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index fd51f1b2e..81fcb35eb 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -33,12 +33,13 @@ pub const locales = [_][:0]const u8{ "zh_CN.UTF-8", "de_DE.UTF-8", "fr_FR.UTF-8", + "ja_JP.UTF-8", "nl_NL.UTF-8", "nb_NO.UTF-8", "ru_RU.UTF-8", "uk_UA.UTF-8", "pl_PL.UTF-8", - "ja_JP.UTF-8", + "tr_TR.UTF-8", "id_ID.UTF-8", }; From e5de0008950141a670312c61e054cdca9373667a Mon Sep 17 00:00:00 2001 From: MiguelElGallo <60221874+MiguelElGallo@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:23:01 +0200 Subject: [PATCH 066/642] add Spanish translations (419 = Latin America) for com.mitchellh.ghostty package --- po/es_419.UTF-8.po | 267 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 po/es_419.UTF-8.po diff --git a/po/es_419.UTF-8.po b/po/es_419.UTF-8.po new file mode 100644 index 000000000..0cbd05d1b --- /dev/null +++ b/po/es_419.UTF-8.po @@ -0,0 +1,267 @@ +# Spanish translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Miguel Peredo , 2025. +# +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" +"PO-Revision-Date: 2025-03-28 16:15+0200\n" +"Last-Translator: Miguel Peredo \n" +"Language-Team: Spanish \n" +"Language: es_419\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 "Cambiar el título de la terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "Aceptar" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Errores de configuración" + +#: src/apprt/gtk/ui/1.5/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 "" +"Se encontraron uno o más errores de configuración. Por favor revise los errores a continuación, " +"y recargue su configuración o ignore estos errores." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Recargar configuración" + +#: 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 "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 +msgid "Paste" +msgstr "Pegar" + +#: 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 "Limpiar" + +#: 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 "Reiniciar" + +#: 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 "Dividir" + +#: 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 "Cambiar título…" + +#: 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 "Dividir arriba" + +#: 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 "Dividir abajo" + +#: 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 "Dividir a la izquierda" + +#: 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 "Dividir a la derecha" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +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:246 +msgid "New Tab" +msgstr "Nueva pestaña" + +#: 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 "Cerrar pestaña" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Ventana" + +#: 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 "Nueva ventana" + +#: 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 "Cerrar ventana" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +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 +msgid "Open Configuration" +msgstr "Abrir configuración" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +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:960 +msgid "About Ghostty" +msgstr "Acerca de Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Autorizar acceso al portapapeles" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicación está intentando leer desde el portapapeles. El contenido " +"actual del portapapeles se muestra a continuación." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/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 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Advertencia: Pegado potencialmente inseguro" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Pegar este texto en la terminal puede ser peligroso ya que parece que " +"algunos comandos podrían ejecutarse." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de la terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Cerrar" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "¿Salir de Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "¿Cerrar ventana?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "¿Cerrar pestaña?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "¿Cerrar división?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Todas las sesiones de terminal serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas las sesiones de terminal en esta ventana serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas las sesiones de terminal en esta pestaña serán terminadas." + +#: src/apprt/gtk/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Ver pestañas abiertas" + +#: src/apprt/gtk/Window.zig:295 +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:725 +msgid "Reloaded the configuration" +msgstr "Configuración recargada" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Desarrolladores de Ghostty" From a9f9abd6154720d0584885322c4fac6e3159a804 Mon Sep 17 00:00:00 2001 From: MiguelElGallo <60221874+MiguelElGallo@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:47:39 +0200 Subject: [PATCH 067/642] add Spanish (Latin America) locale support --- src/os/i18n.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/os/i18n.zig b/src/os/i18n.zig index fd51f1b2e..6e62c52b6 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -40,6 +40,7 @@ pub const locales = [_][:0]const u8{ "pl_PL.UTF-8", "ja_JP.UTF-8", "id_ID.UTF-8", + "es_BO.UTF-8", }; /// Set for faster membership lookup of locales. From 5bbed046f6c7aa2156b6850aa7b54af68fc5539a Mon Sep 17 00:00:00 2001 From: MiguelElGallo <60221874+MiguelElGallo@users.noreply.github.com> Date: Fri, 28 Mar 2025 17:49:34 +0200 Subject: [PATCH 068/642] add Spanish (Bolivia) translations and locale support --- CODEOWNERS | 1 + po/{es_419.UTF-8.po => es_BO.UTF-8.po} | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename po/{es_419.UTF-8.po => es_BO.UTF-8.po} (99%) diff --git a/CODEOWNERS b/CODEOWNERS index a7992806a..05dab45f9 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -159,6 +159,7 @@ /po/README_TRANSLATORS.md @ghostty-org/localization /po/com.mitchellh.ghostty.pot @ghostty-org/localization /po/de_DE.UTF-8.po @ghostty-org/de_DE +/po/es_BO.UTF-8.po @ghostty-org/es_BO /po/fr_FR.UTF-8.po @ghostty-org/fr_FR /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP diff --git a/po/es_419.UTF-8.po b/po/es_BO.UTF-8.po similarity index 99% rename from po/es_419.UTF-8.po rename to po/es_BO.UTF-8.po index 0cbd05d1b..339ff54c4 100644 --- a/po/es_419.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -8,10 +8,10 @@ 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" -"PO-Revision-Date: 2025-03-28 16:15+0200\n" +"PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" -"Language: es_419\n" +"Language: es_BO\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" From c19f2aa1bc4f0b3ac0f7cb4eb3883b1977882acd Mon Sep 17 00:00:00 2001 From: g <199296466+asdkoasak@users.noreply.github.com> Date: Fri, 28 Mar 2025 15:00:16 -0300 Subject: [PATCH 069/642] Add pt-BR translations to ghostty --- po/pt_BR.utf8.po | 269 +++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 2 files changed, 270 insertions(+) create mode 100644 po/pt_BR.utf8.po diff --git a/po/pt_BR.utf8.po b/po/pt_BR.utf8.po new file mode 100644 index 000000000..b6d38948f --- /dev/null +++ b/po/pt_BR.utf8.po @@ -0,0 +1,269 @@ +# Portuguese translations for com.mitchellh.ghostty package +# Traduções em português brasileiro para o pacote com.mitchellh.ghostty. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# itz , 2025. +# +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" +"PO-Revision-Date: 2025-03-28 11:04-0300\n" +"Last-Translator: itz \n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\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 "Mudar título do Terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Deixe em branco para restaurar o título original." + +#: 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 +msgid "Cancel" +msgstr "Cancelar" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "OK" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Erros de configuração" + +#: src/apprt/gtk/ui/1.5/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 "" +"Um ou mais erros de configurações encontrados. Por favor revise os erros abaixo, " +"e ou recarregue as suas configurações, ou ignore esses erros." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Recarregar Configuração" + +#: 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 "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 +msgid "Paste" +msgstr "Colar" + +#: 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 "Limpar" + +#: 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 "Reiniciar" + +#: 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 "Dividir" + +#: 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 "Mudar Título..." + +#: 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 "Dividir Cima" + +#: 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 "Dividir Baixo" + +#: 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 "Dividir Esquerda" + +#: 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 "Dividir Direita" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Aba" + +#: 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 +msgid "New Tab" +msgstr "Nova Aba" + +#: 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 "Fechar Aba" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Janela" + +#: 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 "Nova Janela" + +#: 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 "Fechar Janela" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configurar" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Open Configuration" +msgstr "Abrir Configuração" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspetor de Terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Sobre Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +msgid "Quit" +msgstr "Sair" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Autorizar Acesso a Área de Transferência" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Uma aplicação está tentando ler da área de transferência. O conteúdo " +"atual da área de transferência está aparecendo abaixo." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Negar" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Permitir" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Uma aplicação está tentando escrever na área de transferência. O conteúdo " +"atual da área de transferência está aparecendo abaixo." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Aviso: Conteúdo Potencialmente Inseguro" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Colar esse texto em um terminal pode ser perigoso, pois parece que alguns " +"comandos podem ser executados." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspetor de Terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado para a área de transferência" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Fechar" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Fechar Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Fechar Janela?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Fechar Aba?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Fechar Divisão?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Todas as sessões de terminal serão finalizadas." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Todas as sessões de terminal nessa janela serão finalizadas." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Todas as sessões de terminal nessa aba serão finalizadas." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "O processo atual rodando nessa divisão será finalizado." + +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Menu Principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Visualizar Abas Abertas" + +#: src/apprt/gtk/Window.zig:295 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." + +#: src/apprt/gtk/Window.zig:725 +msgid "Reloaded the configuration" +msgstr "Configuração recarregada" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Desenvolvedores Ghostty" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 7cc5a8309..c1ff9bd4b 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -42,6 +42,7 @@ pub const locales = [_][:0]const u8{ "tr_TR.UTF-8", "id_ID.UTF-8", "es_BO.UTF-8", + "pt_BR.UTF-8", }; /// Set for faster membership lookup of locales. From 63ccdf2cff513a3d5174970a116946a8746377da Mon Sep 17 00:00:00 2001 From: g <199296466+asdkoasak@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:16:42 -0300 Subject: [PATCH 070/642] fix in capital letters --- po/pt_BR.utf8.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/pt_BR.utf8.po b/po/pt_BR.utf8.po index b6d38948f..78fa6c3a8 100644 --- a/po/pt_BR.utf8.po +++ b/po/pt_BR.utf8.po @@ -2,7 +2,7 @@ # Traduções em português brasileiro para o pacote com.mitchellh.ghostty. # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. -# itz , 2025. +# Gustavo Peres , 2025. # msgid "" msgstr "" @@ -10,7 +10,7 @@ msgstr "" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-03-28 11:04-0300\n" -"Last-Translator: itz \n" +"Last-Translator: Gustavo Peres \n" "Language-Team: Brazilian Portuguese \n" "Language: pt_BR\n" @@ -38,7 +38,7 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Erros de configuração" +msgstr "Erros de Configuração" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 msgid "" From f794afe2d85a379a14c290c2e820e311741d7d50 Mon Sep 17 00:00:00 2001 From: g <199296466+asdkoasak@users.noreply.github.com> Date: Mon, 31 Mar 2025 11:25:30 -0300 Subject: [PATCH 071/642] standard file extension name --- po/{pt_BR.utf8.po => pt_BR.UTF-8.po} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename po/{pt_BR.utf8.po => pt_BR.UTF-8.po} (100%) diff --git a/po/pt_BR.utf8.po b/po/pt_BR.UTF-8.po similarity index 100% rename from po/pt_BR.utf8.po rename to po/pt_BR.UTF-8.po From e31c8e09ed4d2589e59a13aa75b9849e1d3170f7 Mon Sep 17 00:00:00 2001 From: Gustavo <199296466+gpd0@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:47:03 -0300 Subject: [PATCH 072/642] Update po/pt_BR.UTF-8.po Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- po/pt_BR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index 78fa6c3a8..44f7df740 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -87,7 +87,7 @@ msgstr "Dividir" #: 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 "Mudar Título..." +msgstr "Mudar Título…" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50 From 11f5797a91436fcdc716cc240b87ff37f04d12ee Mon Sep 17 00:00:00 2001 From: g <199296466+asdkoasak@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:56:07 -0300 Subject: [PATCH 073/642] fix in lower case when required --- po/pt_BR.UTF-8.po | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index 44f7df740..eca4ce3dc 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -38,7 +38,7 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "Erros de Configuração" +msgstr "Erros de configuração" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 msgid "" @@ -87,27 +87,27 @@ msgstr "Dividir" #: 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 "Mudar Título…" +msgstr "Mudar título…" #: 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 "Dividir Cima" +msgstr "Dividir cima" #: 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 "Dividir Baixo" +msgstr "Dividir baixo" #: 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 "Dividir Esquerda" +msgstr "Dividir esquerda" #: 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 "Dividir Direita" +msgstr "Dividir direita" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" @@ -117,12 +117,12 @@ msgstr "Aba" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:30 #: src/apprt/gtk/Window.zig:246 msgid "New Tab" -msgstr "Nova Aba" +msgstr "Nova aba" #: 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 "Fechar Aba" +msgstr "Fechar aba" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 msgid "Window" @@ -131,12 +131,12 @@ msgstr "Janela" #: 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 "Nova Janela" +msgstr "Nova janela" #: 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 "Fechar Janela" +msgstr "Fechar janela" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" @@ -145,11 +145,11 @@ msgstr "Configurar" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Open Configuration" -msgstr "Abrir Configuração" +msgstr "Abrir configuração" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Terminal Inspector" -msgstr "Inspetor de Terminal" +msgstr "Inspetor de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 @@ -163,7 +163,7 @@ msgstr "Sair" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" -msgstr "Autorizar Acesso a Área de Transferência" +msgstr "Autorizar acesso a área de transferência" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" @@ -193,7 +193,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" -msgstr "Aviso: Conteúdo Potencialmente Inseguro" +msgstr "Aviso: Conteúdo potencialmente inseguro" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 msgid "" @@ -205,7 +205,7 @@ msgstr "" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspetor de Terminal" +msgstr "Ghostty: Inspetor de terminal" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" @@ -221,15 +221,15 @@ msgstr "Fechar Ghostty?" #: src/apprt/gtk/CloseDialog.zig:88 msgid "Close Window?" -msgstr "Fechar Janela?" +msgstr "Fechar janela?" #: src/apprt/gtk/CloseDialog.zig:89 msgid "Close Tab?" -msgstr "Fechar Aba?" +msgstr "Fechar aba?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Fechar Divisão?" +msgstr "Fechar divisão?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." @@ -253,7 +253,7 @@ msgstr "Menu Principal" #: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" -msgstr "Visualizar Abas Abertas" +msgstr "Visualizar abas abertas" #: src/apprt/gtk/Window.zig:295 msgid "" From 7e67312c61eb736db67f5743e9a0a989b70d35e8 Mon Sep 17 00:00:00 2001 From: g <199296466+asdkoasak@users.noreply.github.com> Date: Fri, 4 Apr 2025 17:35:11 -0300 Subject: [PATCH 074/642] grammar fix and correct form for some phrases --- po/pt_BR.UTF-8.po | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index eca4ce3dc..f9fadce66 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -45,8 +45,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Um ou mais erros de configurações encontrados. Por favor revise os erros abaixo, " -"e ou recarregue as suas configurações, ou ignore esses erros." +"Um ou mais erros de configuração encontrados. Por favor revise os erros abaixo, " +"e ou recarregue sua configuração, ou ignore esses erros." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,7 +56,7 @@ msgstr "Ignorar" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Reload Configuration" -msgstr "Recarregar Configuração" +msgstr "Recarregar configuração" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -92,22 +92,22 @@ msgstr "Mudar título…" #: 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 "Dividir cima" +msgstr "Dividir para cima" #: 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 "Dividir baixo" +msgstr "Dividir para baixo" #: 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 "Dividir esquerda" +msgstr "Dividir à esquerda" #: 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 "Dividir direita" +msgstr "Dividir à direita" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" @@ -154,7 +154,7 @@ msgstr "Inspetor de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 msgid "About Ghostty" -msgstr "Sobre Ghostty" +msgstr "Sobre o Ghostty" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 msgid "Quit" @@ -163,7 +163,7 @@ msgstr "Sair" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 msgid "Authorize Clipboard Access" -msgstr "Autorizar acesso a área de transferência" +msgstr "Autorizar acesso à área de transferência" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" @@ -171,7 +171,7 @@ msgid "" "clipboard contents are shown below." msgstr "" "Uma aplicação está tentando ler da área de transferência. O conteúdo " -"atual da área de transferência está aparecendo abaixo." +"atual da área de transferência está sendo exibido abaixo." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 From 24847293f2cc1917f3666934d6c560fef7c95042 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Apr 2025 10:12:15 -0700 Subject: [PATCH 075/642] update CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index 829316dcb..f1b1c9019 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -166,6 +166,7 @@ /po/nb_NO.UTF-8.po @ghostty-org/nb_NO /po/nl_NL.UTF-8.po @ghostty-org/nl_NL /po/pl_PL.UTF-8.po @ghostty-org/pl_PL +/po/pt_BR.UTF-8.po @ghostty-org/pt_BR /po/ru_RU.UTF-8.po @ghostty-org/ru_RU /po/tr_TR.UTF-8.po @ghostty-org/tr_TR /po/uk_UA.UTF-8.po @ghostty-org/uk_UA From a092d7ae42edc4748057d218f7dc4da8ec06c41d Mon Sep 17 00:00:00 2001 From: Francesc Arpi Roca Date: Thu, 20 Mar 2025 21:19:50 +0100 Subject: [PATCH 076/642] i18n: add catalan translations --- CODEOWNERS | 1 + po/ca_ES.UTF-8.po | 269 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 3 files changed, 271 insertions(+) create mode 100644 po/ca_ES.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index f1b1c9019..fa3d73fd3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -158,6 +158,7 @@ # Localization /po/README_TRANSLATORS.md @ghostty-org/localization /po/com.mitchellh.ghostty.pot @ghostty-org/localization +/po/ca_ES.UTF-8.po @ghostty-org/ca_ES /po/de_DE.UTF-8.po @ghostty-org/de_DE /po/es_BO.UTF-8.po @ghostty-org/es_BO /po/fr_FR.UTF-8.po @ghostty-org/fr_FR diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po new file mode 100644 index 000000000..dc017500b --- /dev/null +++ b/po/ca_ES.UTF-8.po @@ -0,0 +1,269 @@ +# Catalan translations for com.mitchellh.ghostty package. +# Copyright (C) 2025 Mitchell Hashimoto +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Francesc Arpi , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" +"PO-Revision-Date: 2025-03-20 08:07+0100\n" +"Last-Translator: Francesc Arpi \n" +"Language-Team: \n" +"Language: ca\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 "Canvia el títol del terminal" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +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 +msgid "Cancel" +msgstr "Cancel·la" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10 +msgid "OK" +msgstr "D'acord" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Errors de configuració" + +#: src/apprt/gtk/ui/1.5/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 "" +"S'han trobat un o més errors de configuració. Si us plau, revisa els errors a " +"continuació i torna a carregar la configuració o ignora aquests errors." + +#: src/apprt/gtk/ui/1.5/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 +msgid "Reload Configuration" +msgstr "Torna a carregar la configuració" + +#: 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 "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 +msgid "Paste" +msgstr "Enganxa" + +#: 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 "Neteja" + +#: 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 "Reinicia" + +#: 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 "Divideix" + +#: 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 "Canvia el títol…" + +#: 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 "Divideix cap amunt" + +#: 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 "Divideix cap avall" + +#: 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 "Divideix a l'esquerra" + +#: 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 "Divideix a la dreta" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +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:246 +msgid "New Tab" +msgstr "Nova pestanya" + +#: 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 "Tanca la pestanya" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73 +msgid "Window" +msgstr "Finestra" + +#: 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 "Nova finestra" + +#: 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 "Tanca la finestra" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +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 +msgid "Open Configuration" +msgstr "Obre la configuració" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Terminal Inspector" +msgstr "Inspector de terminal" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 +#: src/apprt/gtk/Window.zig:960 +msgid "About Ghostty" +msgstr "Sobre Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +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 +msgid "Authorize Clipboard Access" +msgstr "Autoritza l'accés al porta-retalls" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Una aplicació està intentant llegir del porta-retalls. El contingut actual " +"del porta-retalls es mostra a continuació." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Denega" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Permet" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +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 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Avís: Enganxament potencialment insegur" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Enganxar aquest text al terminal pot ser perillós, ja que sembla que es " +"podrien executar algunes ordres." + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspector de terminal" + +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiat al porta-retalls" + +#: src/apprt/gtk/CloseDialog.zig:47 +msgid "Close" +msgstr "Tanca" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Surt de Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Tanca la finestra?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Tanca la pestanya?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Tanca la divisió?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Totes les sessions del terminal es tancaran." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Totes les sessions del terminal en aquesta finestra es tancaran." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Totes les sessions del terminal en aquesta pestanya es tancaran." + +#: src/apprt/gtk/CloseDialog.zig:99 +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/Window.zig:200 +msgid "Main Menu" +msgstr "Menú principal" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Mostra les pestanyes obertes" + +#: src/apprt/gtk/Window.zig:295 +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:725 +msgid "Reloaded the configuration" +msgstr "S'ha tornat a carregar la configuració" + +#: src/apprt/gtk/Window.zig:941 +msgid "Ghostty Developers" +msgstr "Desenvolupadors de Ghostty" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index c1ff9bd4b..9c5c054ec 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -30,6 +30,7 @@ const log = std.log.scoped(.i18n); /// we don't have a good way to determine this. We can always reorder /// with some data. pub const locales = [_][:0]const u8{ + "ca_ES.UTF-8", "zh_CN.UTF-8", "de_DE.UTF-8", "fr_FR.UTF-8", From e30feb3bfbf6ce37c581d99c13b471ab92a898d7 Mon Sep 17 00:00:00 2001 From: Francesc Arpi Roca Date: Thu, 20 Mar 2025 21:30:55 +0100 Subject: [PATCH 077/642] i18n: fix string length --- po/ca_ES.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index dc017500b..ef4160d1b 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -54,7 +54,7 @@ msgstr "Ignora" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Reload Configuration" -msgstr "Torna a carregar la configuració" +msgstr "Carrega la configuració" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 From d749e1b87e75f128d0be80dd1ab756e5b08d025c Mon Sep 17 00:00:00 2001 From: Francesc Arpi Roca Date: Fri, 21 Mar 2025 08:05:03 +0100 Subject: [PATCH 078/642] i18n: fix the "deny" catalan translation --- po/ca_ES.UTF-8.po | 2 +- src/os/i18n.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index ef4160d1b..5cbb7efd5 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -174,7 +174,7 @@ 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 msgid "Deny" -msgstr "Denega" +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 diff --git a/src/os/i18n.zig b/src/os/i18n.zig index 9c5c054ec..5fc376417 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -30,7 +30,6 @@ const log = std.log.scoped(.i18n); /// we don't have a good way to determine this. We can always reorder /// with some data. pub const locales = [_][:0]const u8{ - "ca_ES.UTF-8", "zh_CN.UTF-8", "de_DE.UTF-8", "fr_FR.UTF-8", @@ -44,6 +43,7 @@ pub const locales = [_][:0]const u8{ "id_ID.UTF-8", "es_BO.UTF-8", "pt_BR.UTF-8", + "ca_ES.UTF-8", }; /// Set for faster membership lookup of locales. From deed2707a54ba6bf315ce84b89e029e873be6b68 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 11 Apr 2025 12:50:08 -0700 Subject: [PATCH 079/642] CODEOWNERS --- CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/CODEOWNERS b/CODEOWNERS index fa3d73fd3..0665aa407 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -164,6 +164,7 @@ /po/fr_FR.UTF-8.po @ghostty-org/fr_FR /po/id_ID.UTF-8.po @ghostty-org/id_ID /po/ja_JP.UTF-8.po @ghostty-org/ja_JP +/po/mk_MK.UTF-8.po @ghostty-org/mk_MK /po/nb_NO.UTF-8.po @ghostty-org/nb_NO /po/nl_NL.UTF-8.po @ghostty-org/nl_NL /po/pl_PL.UTF-8.po @ghostty-org/pl_PL From 6d80388155fe7cfd31005c20fc1c362bf6eaeeb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Apr 2025 12:45:37 -0700 Subject: [PATCH 080/642] macOS: only emit a mouse exited position if we're not dragging Fixes #7071 When the mouse is being actively dragged, AppKit continues to emit mouseDragged events which will update our position appropriately. The mouseExit event we were sending sends a synthetic (-1, -1) position which was causing a scroll up. --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index c6a3d7629..230d3a9e2 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -743,6 +743,13 @@ extension Ghostty { override func mouseExited(with event: NSEvent) { guard let surface = self.surface else { return } + // If the mouse is being dragged then we don't have to emit + // this because we get mouse drag events even if we've already + // exited the viewport (i.e. mouseDragged) + if NSEvent.pressedMouseButtons != 0 { + return + } + // Negative values indicate cursor has left the viewport let mods = Ghostty.ghosttyMods(event.modifierFlags) ghostty_surface_mouse_pos(surface, -1, -1, mods) From 6d3f97ec1ec3a56d6dd83d25790e3fa711e40815 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 13 Apr 2025 14:37:38 -0700 Subject: [PATCH 081/642] Mouse drag while clicked should cancel any mouse link actions Fixes #7077 This follows pretty standard behavior across native or popular applications on both platforms macOS and Linux. The basic behavior is that if you do a mouse down event and then drag the mouse beyond the current character, then any mouse up actions are canceled (beyond emiting the event itself). This fixes a specific scenario where you could do the following: 1. Click anywhere (mouse down) 2. Drag over a valid link 3. Press command/control (to activate the link) 4. Release the mouse button (mouse up) 5. The link is triggered Now, step 3 and step 5 do not happen. Links are not even highlighted in this scenario. This matches iTerm2 on macOS which has a similar command-to-activate-links behavior. --- src/Surface.zig | 100 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 89031a1b5..da8662040 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1031,9 +1031,64 @@ fn mouseRefreshLinks( // If the position is outside our viewport, do nothing if (pos.x < 0 or pos.y < 0) return; + // Update the last point that we checked for links so we don't + // recheck if the mouse moves some pixels to the same point. self.mouse.link_point = pos_vp; - if (try self.linkAtPos(pos)) |link| { + // We use an arena for everything below to make things easy to clean up. + // In the case we don't do any allocs this is very cheap to setup + // (effectively just struct init). + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const alloc = arena.allocator(); + + // Get our link at the current position. This returns null if there + // isn't a link OR if we shouldn't be showing links for some reason + // (see further comments for cases). + const link_: ?apprt.action.MouseOverLink = link: { + // If we clicked and our mouse moved cells then we never + // highlight links until the mouse is unclicked. This follows + // standard macOS and Linux behavior where a click and drag cancels + // mouse actions. + const left_idx = @intFromEnum(input.MouseButton.left); + if (self.mouse.click_state[left_idx] == .press) click: { + const pin = self.mouse.left_click_pin orelse break :click; + const click_pt = self.io.terminal.screen.pages.pointFromPin( + .viewport, + pin.*, + ) orelse break :click; + + if (!click_pt.coord().eql(pos_vp)) { + log.debug("mouse moved while left click held, ignoring link hover", .{}); + break :link null; + } + } + + const link = (try self.linkAtPos(pos)) orelse break :link null; + switch (link[0]) { + .open => { + const str = try self.io.terminal.screen.selectionString(alloc, .{ + .sel = link[1], + .trim = false, + }); + break :link .{ .url = str }; + }, + + ._open_osc8 => { + // Show the URL in the status bar + const pin = link[1].start(); + const uri = self.osc8URI(pin) orelse { + log.warn("failed to get URI for OSC8 hyperlink", .{}); + break :link null; + }; + break :link .{ .url = uri }; + }, + } + }; + + // If we found a link, setup our internal state and notify the + // apprt so it can highlight it. + if (link_) |link| { self.renderer_state.mouse.point = pos_vp; self.mouse.over_link = true; self.renderer_state.terminal.screen.dirty.hyperlink_hover = true; @@ -1042,38 +1097,18 @@ fn mouseRefreshLinks( .mouse_shape, .pointer, ); - - switch (link[0]) { - .open => { - const str = try self.io.terminal.screen.selectionString(self.alloc, .{ - .sel = link[1], - .trim = false, - }); - defer self.alloc.free(str); - _ = try self.rt_app.performAction( - .{ .surface = self }, - .mouse_over_link, - .{ .url = str }, - ); - }, - - ._open_osc8 => link: { - // Show the URL in the status bar - const pin = link[1].start(); - const uri = self.osc8URI(pin) orelse { - log.warn("failed to get URI for OSC8 hyperlink", .{}); - break :link; - }; - _ = try self.rt_app.performAction( - .{ .surface = self }, - .mouse_over_link, - .{ .url = uri }, - ); - }, - } - + _ = try self.rt_app.performAction( + .{ .surface = self }, + .mouse_over_link, + link, + ); try self.queueRender(); - } else if (over_link) { + return; + } + + // No link, if we're previously over a link then we need to clear + // the over-link apprt state. + if (over_link) { _ = try self.rt_app.performAction( .{ .surface = self }, .mouse_shape, @@ -1085,6 +1120,7 @@ fn mouseRefreshLinks( .{ .url = "" }, ); try self.queueRender(); + return; } } From b932d3552670f9dc91f35e3f6cd08b879a7cf6d2 Mon Sep 17 00:00:00 2001 From: cryptocode Date: Mon, 14 Apr 2025 16:27:07 +0200 Subject: [PATCH 082/642] i18: fix minor Norwegian grammar issues Changes the translation of 'clipboard' to definite singular form, and removes a misplaced verb. --- po/nb_NO.UTF-8.po | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index ab6252f85..bd7c8876a 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -4,13 +4,14 @@ # Hanna Rose , 2025. # Uzair Aftab , 2025. # Christoffer Tønnessen , 2025. +# cryptocode , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"PO-Revision-Date: 2025-03-19 09:52+0100\n" -"Last-Translator: Christoffer Tønnessen \n" +"PO-Revision-Date: 2025-04-14 16:25+0200\n" +"Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" "Language: nb\n" "MIME-Version: 1.0\n" @@ -162,7 +163,7 @@ 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 msgid "Authorize Clipboard Access" -msgstr "Gi tilgang til utklippstavle" +msgstr "Gi tilgang til utklippstavlen" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 msgid "" @@ -187,7 +188,7 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"En applikasjon er forsøker å skrive til utklippstavlen. Gjeldende " +"En applikasjon forsøker å skrive til utklippstavlen. Gjeldende " "utklippstavleinnhold er vist nedenfor." #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 @@ -208,7 +209,7 @@ msgstr "Ghostty: Terminalinspektør" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" -msgstr "Kopiert til utklippstavle" +msgstr "Kopiert til utklippstavlen" #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" From a0760cabd65fd4da46c6832bd44318c9526842d4 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 14 Apr 2025 21:43:02 +0800 Subject: [PATCH 083/642] gtk: implement bell Co-authored-by: Jeffrey C. Ollie --- include/ghostty.h | 1 + src/Surface.zig | 10 ++++++++++ src/apprt/action.zig | 3 +++ src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 8 ++++++++ src/apprt/gtk/Surface.zig | 4 ++++ src/apprt/surface.zig | 3 +++ src/termio/stream_handler.zig | 5 ++--- 8 files changed, 32 insertions(+), 3 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 2dc1bffef..f30275b2c 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -601,6 +601,7 @@ typedef enum { GHOSTTY_ACTION_RELOAD_CONFIG, GHOSTTY_ACTION_CONFIG_CHANGE, GHOSTTY_ACTION_CLOSE_WINDOW, + GHOSTTY_ACTION_RING_BELL, } ghostty_action_tag_e; typedef union { diff --git a/src/Surface.zig b/src/Surface.zig index 46fa476f7..03329fcb5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -932,6 +932,16 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .present_surface => try self.presentSurface(), .password_input => |v| try self.passwordInput(v), + + .ring_bell => { + _ = self.rt_app.performAction( + .{ .surface = self }, + .ring_bell, + {}, + ) catch |err| { + log.warn("apprt failed to ring bell={}", .{err}); + }; + }, } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 30cb2fa5e..30cbfb1e1 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -244,6 +244,8 @@ pub const Action = union(Key) { /// Closes the currently focused window. close_window, + ring_bell, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -287,6 +289,7 @@ pub const Action = union(Key) { reload_config, config_change, close_window, + ring_bell, }; /// Sync with: ghostty_action_u diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 998f88022..c5ee802c4 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -246,6 +246,7 @@ pub const App = struct { .toggle_maximize, .prompt_title, .reset_window_size, + .ring_bell, => { log.info("unimplemented action={}", .{action}); return false; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index b4bebe8ee..d2e39c4c2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -484,6 +484,7 @@ pub fn performAction( .prompt_title => try self.promptTitle(target), .toggle_quick_terminal => return try self.toggleQuickTerminal(), .secure_input => self.setSecureInput(target, value), + .ring_bell => try self.ringBell(target), // Unimplemented .close_all_windows, @@ -775,6 +776,13 @@ fn toggleQuickTerminal(self: *App) !bool { return true; } +fn ringBell(_: *App, target: apprt.Target) !void { + switch (target) { + .app => {}, + .surface => |surface| try surface.rt_surface.ringBell(), + } +} + fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index fe05fa63b..2ba21c871 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2439,3 +2439,7 @@ pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void { .toggle => self.is_secure_input = !self.is_secure_input, } } + +pub fn ringBell(self: *Surface) !void { + } +} diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index f3fd71432..6de41c544 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -81,6 +81,9 @@ pub const Message = union(enum) { /// The terminal has reported a change in the working directory. pwd_change: WriteReq, + /// The terminal encountered a bell character. + ring_bell, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 43d2888d2..299c7cd45 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -325,9 +325,8 @@ pub const StreamHandler = struct { try self.terminal.printRepeat(count); } - pub fn bell(self: StreamHandler) !void { - _ = self; - log.info("BELL", .{}); + pub fn bell(self: *StreamHandler) !void { + self.surfaceMessageWriter(.ring_bell); } pub fn backspace(self: *StreamHandler) !void { From 10a591fba25ac3605d7e46994c21a34f2f8a2b6a Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 14 Apr 2025 21:43:02 +0800 Subject: [PATCH 084/642] gtk(bell): use `gdk.Surface.beep` for bell Co-authored-by: Jeffrey C. Ollie --- src/apprt/gtk/Surface.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 2ba21c871..230c9a3c3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2441,5 +2441,13 @@ pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void { } pub fn ringBell(self: *Surface) !void { + const window = self.container.window() orelse { + log.warn("failed to ring bell: surface is not attached to any window", .{}); + return; + }; + + // System beep + if (window.window.as(gtk.Native).getSurface()) |surface| { + surface.beep(); } } From abd7d9202b977c62924769779a24c3bc6d8af964 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 14 Apr 2025 21:43:02 +0800 Subject: [PATCH 085/642] gtk(bell): mark tab as needing attention on bell --- src/apprt/gtk/Surface.zig | 8 ++++++++ src/apprt/gtk/TabView.zig | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 230c9a3c3..76de2a312 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2450,4 +2450,12 @@ pub fn ringBell(self: *Surface) !void { if (window.window.as(gtk.Native).getSurface()) |surface| { surface.beep(); } + + // Mark tab as needing attention + if (self.container.tab()) |tab| tab: { + const page = window.notebook.getTabPage(tab) orelse break :tab; + + // Need attention if we're not the currently selected tab + if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true)); + } } diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig index 85a9bbcb2..ddd0951d2 100644 --- a/src/apprt/gtk/TabView.zig +++ b/src/apprt/gtk/TabView.zig @@ -114,9 +114,12 @@ pub fn gotoNthTab(self: *TabView, position: c_int) bool { return true; } +pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage { + return self.tab_view.getPage(tab.box.as(gtk.Widget)); +} + pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int { - const page = self.tab_view.getPage(tab.box.as(gtk.Widget)); - return self.tab_view.getPagePosition(page); + return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null); } pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool { @@ -161,17 +164,16 @@ pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void { } pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void { - const page = self.tab_view.getPage(tab.box.as(gtk.Widget)); - _ = self.tab_view.reorderPage(page, position); + _ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position); } pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void { - const page = self.tab_view.getPage(tab.box.as(gtk.Widget)); + const page = self.getTabPage(tab) orelse return; page.setTitle(title.ptr); } pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void { - const page = self.tab_view.getPage(tab.box.as(gtk.Widget)); + const page = self.getTabPage(tab) orelse return; page.setTooltip(tooltip.ptr); } @@ -203,8 +205,7 @@ pub fn closeTab(self: *TabView, tab: *Tab) void { if (n > 1) self.forcing_close = false; } - const page = self.tab_view.getPage(tab.box.as(gtk.Widget)); - self.tab_view.closePage(page); + if (self.getTabPage(tab)) |page| self.tab_view.closePage(page); // If we have no more tabs we close the window if (self.nPages() == 0) { @@ -260,6 +261,11 @@ fn adwTabViewCreateWindow( fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void { const page = self.tab_view.getSelectedPage() orelse return; + + // If the tab was previously marked as needing attention + // (e.g. due to a bell character), we now unmark that + page.setNeedsAttention(@intFromBool(false)); + const title = page.getTitle(); self.window.setTitle(std.mem.span(title)); } From 3a973c692a2bebc879ab285af3cc21be4d4f3cfe Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 14 Apr 2025 23:27:05 +0800 Subject: [PATCH 086/642] gtk(bell): add `bell-features` config option Co-authored-by: Jeffrey C. Ollie --- src/apprt/gtk/Surface.zig | 4 +++- src/config/Config.zig | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 76de2a312..e99fe29ce 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2441,13 +2441,15 @@ pub fn setSecureInput(self: *Surface, value: apprt.action.SecureInput) void { } pub fn ringBell(self: *Surface) !void { + const features = self.app.config.@"bell-features"; const window = self.container.window() orelse { log.warn("failed to ring bell: surface is not attached to any window", .{}); return; }; // System beep - if (window.window.as(gtk.Native).getSurface()) |surface| { + if (features.system) system: { + const surface = window.window.as(gtk.Native).getSurface() orelse break :system; surface.beep(); } diff --git a/src/config/Config.zig b/src/config/Config.zig index 9cd285d3b..7997b9200 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1848,6 +1848,22 @@ keybind: Keybinds = .{}, /// open terminals. @"custom-shader-animation": CustomShaderAnimation = .true, +/// The list of enabled features that are activated after encountering +/// a bell character. +/// +/// Valid values are: +/// +/// * `system` (default) +/// +/// Instructs the system to notify the user using built-in system functions. +/// This could result in an audiovisual effect, a notification, or something +/// else entirely. Changing these effects require altering system settings: +/// for instance under the "Sound > Alert Sound" setting in GNOME, +/// or the "Accessibility > System Bell" settings in KDE Plasma. +/// +/// Currently only implemented on Linux. +@"bell-features": BellFeatures = .{}, + /// Control the in-app notifications that Ghostty shows. /// /// On Linux (GTK), in-app notifications show up as toasts. Toasts appear @@ -5669,6 +5685,11 @@ pub const AppNotifications = packed struct { @"clipboard-copy": bool = true, }; +/// See bell-features +pub const BellFeatures = packed struct { + system: bool = false, +}; + /// See mouse-shift-capture pub const MouseShiftCapture = enum { false, From 453e6590e8a648b7bf6617afb71e7fc07ad782a7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Apr 2025 10:37:54 -0700 Subject: [PATCH 087/642] macOS: non-native fullscreen should not hide menu on fullscreen space Fixes #7075 We have to use private APIs for this, I couldn't find a reliable way otherwise. --- macos/Ghostty.xcodeproj/project.pbxproj | 18 ++++- .../QuickTerminalController.swift | 12 +-- macos/Sources/Helpers/Fullscreen.swift | 13 ++- .../Sources/Helpers/NSWindow+Extension.swift | 8 ++ macos/Sources/Helpers/Private/CGS.swift | 81 +++++++++++++++++++ .../Sources/Helpers/{ => Private}/Dock.swift | 0 6 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 macos/Sources/Helpers/NSWindow+Extension.swift create mode 100644 macos/Sources/Helpers/Private/CGS.swift rename macos/Sources/Helpers/{ => Private}/Dock.swift (100%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b4c00946c..b69541504 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -55,6 +55,8 @@ A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; }; A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; }; + A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; }; + A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; A59630972AEE163600D64628 /* HostingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630962AEE163600D64628 /* HostingWindow.swift */; }; A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A59630992AEE1C6400D64628 /* Terminal.xib */; }; @@ -154,6 +156,8 @@ A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = ""; }; A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = ""; }; + A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = ""; }; + A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; A59630962AEE163600D64628 /* HostingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostingWindow.swift; sourceTree = ""; }; A59630992AEE1C6400D64628 /* Terminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Terminal.xib; sourceTree = ""; }; @@ -274,13 +278,13 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A5874D9B2DAD781100E83852 /* Private */, A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, A5D0AF3C2B37804400D21823 /* CodableBridge.swift */, - A5A2A3C92D4445E20033CF96 /* Dock.swift */, A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, @@ -293,6 +297,7 @@ A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, + A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, A5CC36142C9CDA03004D6760 /* View+Extension.swift */, A5CA378D2D31D6C100931030 /* Weak.swift */, @@ -403,6 +408,15 @@ path = "Secure Input"; sourceTree = ""; }; + A5874D9B2DAD781100E83852 /* Private */ = { + isa = PBXGroup; + children = ( + A5874D982DAD751A00E83852 /* CGS.swift */, + A5A2A3C92D4445E20033CF96 /* Dock.swift */, + ); + path = Private; + sourceTree = ""; + }; A59630982AEE1C4400D64628 /* Terminal */ = { isa = PBXGroup; children = ( @@ -634,6 +648,7 @@ A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, + A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */, @@ -669,6 +684,7 @@ A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, + A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index fac3a2fbb..896b25326 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -3,12 +3,6 @@ import Cocoa import SwiftUI import GhosttyKit -// This is a Apple's private function that we need to call to get the active space. -@_silgen_name("CGSGetActiveSpace") -func CGSGetActiveSpace(_ cid: Int) -> size_t -@_silgen_name("CGSMainConnectionID") -func CGSMainConnectionID() -> Int - /// Controller for the "quick" terminal. class QuickTerminalController: BaseTerminalController { override var windowNibName: NSNib.Name? { "QuickTerminal" } @@ -25,7 +19,7 @@ class QuickTerminalController: BaseTerminalController { private var previousApp: NSRunningApplication? = nil // The active space when the quick terminal was last shown. - private var previousActiveSpace: size_t = 0 + private var previousActiveSpace: CGSSpace? = nil /// Non-nil if we have hidden dock state. private var hiddenDock: HiddenDock? = nil @@ -154,7 +148,7 @@ class QuickTerminalController: BaseTerminalController { animateOut() case .move: - let currentActiveSpace = CGSGetActiveSpace(CGSMainConnectionID()) + let currentActiveSpace = CGSSpace.active() if previousActiveSpace == currentActiveSpace { // We haven't moved spaces. We lost focus to another app on the // current space. Animate out. @@ -224,7 +218,7 @@ class QuickTerminalController: BaseTerminalController { } // Set previous active space - self.previousActiveSpace = CGSGetActiveSpace(CGSMainConnectionID()) + self.previousActiveSpace = CGSSpace.active() // Animate the window in animateWindowIn(window: window, from: position) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 59865fc9e..1daf9f142 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -180,7 +180,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { } // Hide the menu if requested - if (properties.hideMenu) { + if (properties.hideMenu && savedState.menu) { hideMenu() } @@ -224,7 +224,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { if savedState.dock { unhideDock() } - unhideMenu() + if (properties.hideMenu && savedState.menu) { + unhideMenu() + } // Restore our saved state window.styleMask = savedState.styleMask @@ -340,6 +342,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { let contentFrame: NSRect let styleMask: NSWindow.StyleMask let dock: Bool + let menu: Bool init?(_ window: NSWindow) { guard let contentView = window.contentView else { return nil } @@ -350,6 +353,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.contentFrame = window.convertToScreen(contentView.frame) self.styleMask = window.styleMask self.dock = window.screen?.hasDock ?? false + + // We hide the menu only if this window is not on any fullscreen + // spaces. We do this because fullscreen spaces already hide the + // menu and if we insert/remove this presentation option we get + // issues (see #7075) + self.menu = CGSSpace.list(for: window.cgWindowId).allSatisfy { $0.type != .fullscreen } } } } diff --git a/macos/Sources/Helpers/NSWindow+Extension.swift b/macos/Sources/Helpers/NSWindow+Extension.swift new file mode 100644 index 000000000..c7523bdb7 --- /dev/null +++ b/macos/Sources/Helpers/NSWindow+Extension.swift @@ -0,0 +1,8 @@ +import AppKit + +extension NSWindow { + /// Get the CGWindowID type for the window (used for low level CoreGraphics APIs). + var cgWindowId: CGWindowID { + CGWindowID(windowNumber) + } +} diff --git a/macos/Sources/Helpers/Private/CGS.swift b/macos/Sources/Helpers/Private/CGS.swift new file mode 100644 index 000000000..f9c20afe9 --- /dev/null +++ b/macos/Sources/Helpers/Private/CGS.swift @@ -0,0 +1,81 @@ +import AppKit + +// MARK: - CGS Private API Declarations + +typealias CGSConnectionID = Int32 +typealias CGSSpaceID = size_t + +@_silgen_name("CGSMainConnectionID") +private func CGSMainConnectionID() -> CGSConnectionID + +@_silgen_name("CGSGetActiveSpace") +private func CGSGetActiveSpace(_ cid: CGSConnectionID) -> CGSSpaceID + +@_silgen_name("CGSSpaceGetType") +private func CGSSpaceGetType(_ cid: CGSConnectionID, _ spaceID: CGSSpaceID) -> CGSSpaceType + +@_silgen_name("CGSCopySpacesForWindows") +func CGSCopySpacesForWindows( + _ cid: CGSConnectionID, + _ mask: CGSSpaceMask, + _ windowIDs: CFArray +) -> Unmanaged? + +// MARK: - CGS Space + +/// https://github.com/NUIKit/CGSInternal/blob/c4f6f559d624dc1cfc2bf24c8c19dbf653317fcf/CGSSpace.h#L40 +/// converted to Swift +struct CGSSpaceMask: OptionSet { + let rawValue: UInt32 + + static let includesCurrent = CGSSpaceMask(rawValue: 1 << 0) + static let includesOthers = CGSSpaceMask(rawValue: 1 << 1) + static let includesUser = CGSSpaceMask(rawValue: 1 << 2) + + static let includesVisible = CGSSpaceMask(rawValue: 1 << 16) + + static let currentSpace: CGSSpaceMask = [.includesUser, .includesCurrent] + static let otherSpaces: CGSSpaceMask = [.includesOthers, .includesCurrent] + static let allSpaces: CGSSpaceMask = [.includesUser, .includesOthers, .includesCurrent] + static let allVisibleSpaces: CGSSpaceMask = [.includesVisible, .allSpaces] +} + +/// Represents a unique identifier for a macOS Space (Desktop, Fullscreen, etc). +struct CGSSpace: Hashable, CustomStringConvertible { + let rawValue: CGSSpaceID + + var description: String { + "SpaceID(\(rawValue))" + } + + /// Returns the currently active space. + static func active() -> CGSSpace { + let space = CGSGetActiveSpace(CGSMainConnectionID()) + return .init(rawValue: space) + } + + /// List the sapces for the given window. + static func list(for windowID: CGWindowID, mask: CGSSpaceMask = .allSpaces) -> [CGSSpace] { + guard let spaces = CGSCopySpacesForWindows( + CGSMainConnectionID(), + mask, + [windowID] as CFArray + ) else { return [] } + guard let spaceIDs = spaces.takeRetainedValue() as? [CGSSpaceID] else { return [] } + return spaceIDs.map(CGSSpace.init) + } +} + +// MARK: - CGS Space Types + +enum CGSSpaceType: UInt32 { + case user = 0 + case system = 2 + case fullscreen = 4 +} + +extension CGSSpace { + var type: CGSSpaceType { + CGSSpaceGetType(CGSMainConnectionID(), rawValue) + } +} diff --git a/macos/Sources/Helpers/Dock.swift b/macos/Sources/Helpers/Private/Dock.swift similarity index 100% rename from macos/Sources/Helpers/Dock.swift rename to macos/Sources/Helpers/Private/Dock.swift From d1c15dbf0768ecdb7fdde6a590c1d4b252b9532e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Apr 2025 10:50:30 -0700 Subject: [PATCH 088/642] macOS: quick terminal should retain menu if not frontmost This is a bug I noticed in the following scenario: 1. Open Ghostty 2. Fullscreen normal terminal window (native fullscreen) 3. Open quick terminal 4. Move spaces, QT follows 5. Fullscreen the quick terminal The result was that the menu bar would not disappear since our app is not frontmost but we set the fullscreen frame such that we expected it. --- .../QuickTerminalController.swift | 34 ++++++++++++++++--- macos/Sources/Helpers/Fullscreen.swift | 11 ++++-- .../Helpers/NSApplication+Extension.swift | 12 +++++++ macos/Sources/Helpers/Private/CGS.swift | 2 +- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 896b25326..6e5607c6f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -45,7 +45,7 @@ class QuickTerminalController: BaseTerminalController { object: nil) center.addObserver( self, - selector: #selector(onToggleFullscreen), + selector: #selector(onToggleFullscreen(notification:)), name: Ghostty.Notification.ghosttyToggleFullscreen, object: nil) center.addObserver( @@ -154,8 +154,18 @@ class QuickTerminalController: BaseTerminalController { // current space. Animate out. animateOut() } else { - // We've moved to a different space. Bring the quick terminal back - // into view. + // We've moved to a different space. + + // If we're fullscreen, we need to exit fullscreen because the visible + // bounds may have changed causing a new behavior. + if let fullscreenStyle, fullscreenStyle.isFullscreen { + fullscreenStyle.exit() + DispatchQueue.main.async { + self.onToggleFullscreen() + } + } + + // Make the window visible again on this space DispatchQueue.main.async { self.window?.makeKeyAndOrderFront(nil) } @@ -479,9 +489,23 @@ class QuickTerminalController: BaseTerminalController { @objc private func onToggleFullscreen(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard target == self.focusedSurface else { return } + onToggleFullscreen() + } - // We ignore the requested mode and always use non-native for the quick terminal - toggleFullscreen(mode: .nonNative) + private func onToggleFullscreen() { + // We ignore the configured fullscreen style and always use non-native + // because the way the quick terminal works doesn't support native. + // + // An additional detail is that if the is NOT frontmost, then our + // NSApp.presentationOptions will not take effect so we must always + // do the visible menu mode since we can't get rid of the menu. + let mode: FullscreenMode = if (NSApp.isFrontmost) { + .nonNative + } else { + .nonNativeVisibleMenu + } + + toggleFullscreen(mode: mode) } @objc private func ghosttyConfigDidChange(_ notification: Notification) { diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 1daf9f142..b6fb08271 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -275,7 +275,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // calculate this ourselves. var frame = screen.frame - if (!properties.hideMenu) { + if (!NSApp.presentationOptions.contains(.autoHideMenuBar) && + !NSApp.presentationOptions.contains(.hideMenuBar)) { // We need to subtract the menu height since we're still showing it. frame.size.height -= NSApp.mainMenu?.menuBarHeight ?? 0 @@ -358,7 +359,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // spaces. We do this because fullscreen spaces already hide the // menu and if we insert/remove this presentation option we get // issues (see #7075) - self.menu = CGSSpace.list(for: window.cgWindowId).allSatisfy { $0.type != .fullscreen } + let activeSpace = CGSSpace.active() + let spaces = CGSSpace.list(for: window.cgWindowId) + if spaces.contains(activeSpace) { + self.menu = activeSpace.type != .fullscreen + } else { + self.menu = spaces.allSatisfy { $0.type != .fullscreen } + } } } } diff --git a/macos/Sources/Helpers/NSApplication+Extension.swift b/macos/Sources/Helpers/NSApplication+Extension.swift index 0580cd5fc..d8e41523a 100644 --- a/macos/Sources/Helpers/NSApplication+Extension.swift +++ b/macos/Sources/Helpers/NSApplication+Extension.swift @@ -1,5 +1,7 @@ import Cocoa +// MARK: Presentation Options + extension NSApplication { private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:] @@ -29,3 +31,13 @@ extension NSApplication.PresentationOptions.Element: @retroactive Hashable { hasher.combine(rawValue) } } + +// MARK: Frontmost + +extension NSApplication { + /// True if the application is frontmost. This isn't exactly the same as isActive because + /// an app can be active but not be frontmost if the window with activity is an NSPanel. + var isFrontmost: Bool { + NSWorkspace.shared.frontmostApplication?.bundleIdentifier == Bundle.main.bundleIdentifier + } +} diff --git a/macos/Sources/Helpers/Private/CGS.swift b/macos/Sources/Helpers/Private/CGS.swift index f9c20afe9..0d3b9aa4c 100644 --- a/macos/Sources/Helpers/Private/CGS.swift +++ b/macos/Sources/Helpers/Private/CGS.swift @@ -54,7 +54,7 @@ struct CGSSpace: Hashable, CustomStringConvertible { return .init(rawValue: space) } - /// List the sapces for the given window. + /// List the spaces for the given window. static func list(for windowID: CGWindowID, mask: CGSSpaceMask = .allSpaces) -> [CGSSpace] { guard let spaces = CGSCopySpacesForWindows( CGSMainConnectionID(), From 8bc91933cda382f7eb2b913e14e59d4385107fe2 Mon Sep 17 00:00:00 2001 From: trag1c Date: Mon, 14 Apr 2025 23:40:41 +0200 Subject: [PATCH 089/642] ci: add logging to localization-review script --- .github/scripts/request_review.py | 162 ++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 44 deletions(-) diff --git a/.github/scripts/request_review.py b/.github/scripts/request_review.py index 1a53e82e4..d799e7c58 100644 --- a/.github/scripts/request_review.py +++ b/.github/scripts/request_review.py @@ -2,113 +2,187 @@ # requires-python = ">=3.9" # dependencies = [ # "githubkit", +# "loguru", # ] # /// +from __future__ import annotations + import asyncio import os import re +import sys +from collections.abc import Iterator +from contextlib import contextmanager from itertools import chain from githubkit import GitHub +from githubkit.exception import RequestFailed +from loguru import logger ORG_NAME = "ghostty-org" REPO_NAME = "ghostty" ALLOWED_PARENT_TEAM = "localization" LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}") +LEVEL_MAP = {"DEBUG": "DBG", "WARNING": "WRN", "ERROR": "ERR"} + +logger.remove() +logger.add( + sys.stderr, + format=lambda record: ( + "{time:YYYY-MM-DD HH:mm:ss.SSS} | " + f"{LEVEL_MAP[record['level'].name]} | " + "{function}:{line} - " + "{message}\n" + ), + backtrace=True, + diagnose=True, +) + + +@contextmanager +def log_fail(message: str, *, die: bool = True) -> Iterator[None]: + try: + yield + except RequestFailed as exc: + logger.error(message) + logger.error(exc) + logger.error(exc.response.raw_response.json()) + if die: + sys.exit(1) + gh = GitHub(os.environ["GITHUB_TOKEN"]) +with log_fail("Invalid token"): + # Do the simplest request as a test + gh.rest.rate_limit.get() + async def fetch_and_parse_codeowners() -> dict[str, str]: - content = ( - await gh.rest.repos.async_get_content( - ORG_NAME, - REPO_NAME, - "CODEOWNERS", - headers={"Accept": "application/vnd.github.raw+json"}, - ) - ).text + logger.debug("Fetching CODEOWNERS file...") + with log_fail("Failed to fetch CODEOWNERS file"): + content = ( + await gh.rest.repos.async_get_content( + ORG_NAME, + REPO_NAME, + "CODEOWNERS", + headers={"Accept": "application/vnd.github.raw+json"}, + ) + ).text + logger.debug("Parsing CODEOWNERS file...") codeowners: dict[str, str] = {} for line in content.splitlines(): if not line or line.lstrip().startswith("#"): continue + # This assumes that all entries only list one owner # and that this owner is a team (ghostty-org/foobar) path, owner = line.split() - codeowners[path.lstrip("/")] = owner.removeprefix(f"@{ORG_NAME}/") + path = path.lstrip("/") + owner = owner.removeprefix(f"@{ORG_NAME}/") + + if not is_localization_team(owner): + logger.debug(f"Skipping non-l11n codeowner {owner!r} for {path}") + continue + + codeowners[path] = owner + logger.debug(f"Found codeowner {owner!r} for {path}") return codeowners async def get_team_members(team_name: str) -> list[str]: - team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data + logger.debug(f"Fetching team {team_name!r}...") + with log_fail(f"Failed to fetch team {team_name!r}"): + team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data + if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM: - members = ( - await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) - ).parsed_data - return [m.login for m in members] + logger.debug(f"Fetching team {team_name!r} members...") + with log_fail(f"Failed to fetch team {team_name!r} members"): + resp = await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) + members = [m.login for m in resp.parsed_data] + logger.debug(f"Team {team_name!r} members: {', '.join(members)}") + return members + + logger.warning(f"Team {team_name} does not have a {ALLOWED_PARENT_TEAM!r} parent") return [] async def get_changed_files(pr_number: int) -> list[str]: - diff_entries = ( - await gh.rest.pulls.async_list_files( - ORG_NAME, - REPO_NAME, - pr_number, - per_page=3000, - headers={"Accept": "application/vnd.github+json"}, - ) - ).parsed_data - return [d.filename for d in diff_entries] - - -async def request_review(pr_number: int, pr_author: str, *users: str) -> None: - await asyncio.gather( - *( - gh.rest.pulls.async_request_reviewers( + logger.debug("Gathering changed files...") + with log_fail("Failed to gather changed files"): + diff_entries = ( + await gh.rest.pulls.async_list_files( ORG_NAME, REPO_NAME, pr_number, + per_page=3000, headers={"Accept": "application/vnd.github+json"}, - data={"reviewers": [user]}, ) - for user in users - if user != pr_author + ).parsed_data + return [d.filename for d in diff_entries] + + +async def request_review(pr_number: int, user: str, pr_author: str) -> None: + if user == pr_author: + logger.debug(f"Skipping review request for {user!r} (is PR author)") + logger.debug(f"Requesting review from {user!r}...") + with log_fail(f"Failed to request review from {user}", die=False): + await gh.rest.pulls.async_request_reviewers( + ORG_NAME, + REPO_NAME, + pr_number, + headers={"Accept": "application/vnd.github+json"}, + data={"reviewers": [user]}, ) - ) def is_localization_team(team_name: str) -> bool: return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None +async def get_pr_author(pr_number: int) -> str: + logger.debug("Fetching PR author...") + with log_fail("Failed to fetch PR author"): + resp = await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) + pr_author = resp.parsed_data.user.login + logger.debug(f"Found author: {pr_author!r}") + return pr_author + + async def main() -> None: + logger.debug("Reading PR number...") pr_number = int(os.environ["PR_NUMBER"]) + logger.debug(f"Starting review request process for PR #{pr_number}...") + changed_files = await get_changed_files(pr_number) - pr_author = ( - await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) - ).parsed_data.user.login - localization_codewners = { - path: owner - for path, owner in (await fetch_and_parse_codeowners()).items() - if is_localization_team(owner) - } + logger.debug(f"Changed files: {', '.join(map(repr, changed_files))}") + + pr_author = await get_pr_author(pr_number) + codeowners = await fetch_and_parse_codeowners() found_owners = set[str]() for file in changed_files: - for path, owner in localization_codewners.items(): + logger.debug(f"Finding owner for {file!r}...") + for path, owner in codeowners.items(): if file.startswith(path): + logger.debug(f"Found owner: {owner!r}") break else: + logger.debug("No owner found") continue found_owners.add(owner) member_lists = await asyncio.gather( *(get_team_members(owner) for owner in found_owners) ) - await request_review(pr_number, pr_author, *chain.from_iterable(member_lists)) + await asyncio.gather( + *( + request_review(pr_number, user, pr_author) + for user in chain.from_iterable(member_lists) + ) + ) if __name__ == "__main__": From da6b478fbe0565c6b780267a51c4198efd3693bc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Apr 2025 12:34:24 -0700 Subject: [PATCH 090/642] terminal: clear correct row on index operation in certain edge cases Fixes #7066 This fixes an issue where under certain conditions (expanded below), we would not clear the correct row, leading to the screen having duplicate data. This was triggered by a page state of the following: ``` +----------+ = PAGE 0 ... : : 4305 |1ABCD00000| 4306 |2EFGH00000| :^ : = PIN 0 +-------------+ ACTIVE 4307 |3IJKL00000| | 0 +----------+ : +----------+ : = PAGE 1 0 | | | 1 1 | | | 2 +----------+ : +-------------+ ``` Namely, the cursor had to NOT be on the last row of the first page, but somewhere on the first page. Then, when an `index` (LF) operation was performed the result would look like this: ``` +----------+ = PAGE 0 ... : : 4305 |1ABCD00000| 4306 |2EFGH00000| +-------------+ ACTIVE 4307 |3IJKL00000| | 0 :^ : : = PIN 0 +----------+ : +----------+ : = PAGE 1 0 |3IJKL00000| | 1 1 | | | 2 +----------+ : +-------------+ ``` The `3IJKL` line was duplicated. What was happening here is that we performed the index operation correctly but failed to clear the cursor line as expected. This is because we were always clearing the first row in the page instead of the row of the cursor. Test added. --- src/terminal/Screen.zig | 92 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 0772bfa75..9ab4b23e2 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -924,8 +924,8 @@ fn cursorScrollAboveRotate(self: *Screen) !void { fastmem.rotateOnceR(Row, cur_rows[self.cursor.page_pin.y..cur_page.size.rows]); self.clearCells( cur_page, - &cur_rows[0], - cur_page.getCells(&cur_rows[0]), + &cur_rows[self.cursor.page_pin.y], + cur_page.getCells(&cur_rows[self.cursor.page_pin.y]), ); // Set all the rows we rotated and cleared dirty @@ -1256,6 +1256,17 @@ pub fn clearCells( self.assertIntegrity(); } + if (comptime std.debug.runtime_safety) { + // Our row and cells should be within the page. + const page_rows = page.rows.ptr(page.memory.ptr); + assert(@intFromPtr(row) >= @intFromPtr(&page_rows[0])); + assert(@intFromPtr(row) <= @intFromPtr(&page_rows[page.size.rows - 1])); + + const row_cells = page.getCells(row); + assert(@intFromPtr(&cells[0]) >= @intFromPtr(&row_cells[0])); + assert(@intFromPtr(&cells[cells.len - 1]) <= @intFromPtr(&row_cells[row_cells.len - 1])); + } + // If this row has graphemes, then we need go through a slow path // and delete the cell graphemes. if (row.grapheme) { @@ -4818,6 +4829,83 @@ test "Screen: scroll above creates new page" { try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); } +test "Screen: scroll above with cursor on non-final row" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 10, 4, 10); + defer s.deinit(); + + // Get the cursor to be 2 rows above a new page + const first_page_size = s.pages.pages.first.?.data.capacity.rows; + s.pages.pages.first.?.data.pauseIntegrityChecks(true); + for (0..first_page_size - 3) |_| try s.testWriteString("\n"); + s.pages.pages.first.?.data.pauseIntegrityChecks(false); + + // Write 3 lines of text, forcing the last line into the first + // row of a new page. Move our cursor onto the previous page. + try s.setAttribute(.{ .direct_color_bg = .{ .r = 155 } }); + try s.testWriteString("1AB\n2BC\n3DE\n4FG"); + s.cursorAbsolute(0, 1); + s.pages.clearDirty(); + + // Ensure we're still on the first page. So our cursor is on the first + // page but we have two pages of data. + try testing.expect(s.cursor.page_pin.node == s.pages.pages.first.?); + + // +----------+ = PAGE 0 + // ... : : + // +-------------+ ACTIVE + // 4305 |1AB0000000| | 0 + // 4306 |2BC0000000| | 1 + // :^ : : = PIN 0 + // 4307 |3DE0000000| | 2 + // +----------+ : + // +----------+ : = PAGE 1 + // 0 |4FG0000000| | 3 + // +----------+ : + // +-------------+ + try s.cursorScrollAbove(); + + // +----------+ = PAGE 0 + // ... : : + // 4305 |1AB0000000| + // +-------------+ ACTIVE + // 4306 |2BC0000000| | 0 + // 4307 | | | 1 + // :^ : : = PIN 0 + // +----------+ : + // +----------+ : = PAGE 1 + // 0 |3DE0000000| | 2 + // 1 |4FG0000000| | 3 + // +----------+ : + // +-------------+ + // try s.pages.diagram(std.io.getStdErr().writer()); + + { + const contents = try s.dumpStringAlloc(alloc, .{ .viewport = .{} }); + defer alloc.free(contents); + try testing.expectEqualStrings("2BC\n\n3DE\n4FG", contents); + } + { + const list_cell = s.pages.getCell(.{ .active = .{ .x = 0, .y = 1 } }).?; + const cell = list_cell.cell; + try testing.expect(cell.content_tag == .bg_color_rgb); + try testing.expectEqual(Cell.RGB{ + .r = 155, + .g = 0, + .b = 0, + }, cell.content.color_rgb); + } + + // Page 0's penultimate row is dirty because the cursor moved off of it. + try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 0 } })); + // Page 0's final row is dirty because it was cleared. + try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 1 } })); + // Page 1's row is dirty because it's new. + try testing.expect(s.pages.isDirty(.{ .active = .{ .x = 0, .y = 2 } })); +} + test "Screen: scroll above no scrollback bottom of page" { const testing = std.testing; const alloc = testing.allocator; From b77c5634f0ad4e43128ba9c614a743cbd1dcdd37 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Apr 2025 08:52:00 -0700 Subject: [PATCH 091/642] macos: quick terminal uses padded notch mode if notch is visible Fixes #6612 --- .../QuickTerminalController.swift | 20 ++++++++++++------- .../Sources/Helpers/NSScreen+Extension.swift | 7 +++++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 6e5607c6f..1abe30da1 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -495,14 +495,20 @@ class QuickTerminalController: BaseTerminalController { private func onToggleFullscreen() { // We ignore the configured fullscreen style and always use non-native // because the way the quick terminal works doesn't support native. - // - // An additional detail is that if the is NOT frontmost, then our - // NSApp.presentationOptions will not take effect so we must always - // do the visible menu mode since we can't get rid of the menu. - let mode: FullscreenMode = if (NSApp.isFrontmost) { - .nonNative + let mode: FullscreenMode + if (NSApp.isFrontmost) { + // If we're frontmost and we have a notch then we keep padding + // so all lines of the terminal are visible. + if (window?.screen?.hasNotch ?? false) { + mode = .nonNativePaddedNotch + } else { + mode = .nonNative + } } else { - .nonNativeVisibleMenu + // An additional detail is that if the is NOT frontmost, then our + // NSApp.presentationOptions will not take effect so we must always + // do the visible menu mode since we can't get rid of the menu. + mode = .nonNativeVisibleMenu } toggleFullscreen(mode: mode) diff --git a/macos/Sources/Helpers/NSScreen+Extension.swift b/macos/Sources/Helpers/NSScreen+Extension.swift index ef2c02908..675e0b2ec 100644 --- a/macos/Sources/Helpers/NSScreen+Extension.swift +++ b/macos/Sources/Helpers/NSScreen+Extension.swift @@ -34,4 +34,11 @@ extension NSScreen { return visibleFrame.height < (frame.height - max(menuHeight, notchInset) - boundaryAreaPadding) } + + /// Returns true if the screen has a visible notch (i.e., a non-zero safe area inset at the top). + var hasNotch: Bool { + // We assume that a top safe area means notch, since we don't currently + // know any other situation this is true. + return safeAreaInsets.top > 0 + } } From cc690eddb54126b16961e2d5d21782d61e0fa74a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 15 Apr 2025 09:47:52 -0700 Subject: [PATCH 092/642] macOS: Implement basic bell features (no sound) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #7099 This adds basic bell features to macOS to conceptually match the GTK implementation. When a bell is triggered, macOS will do the following: 1. Bounce the dock icon once, if the app isn't already in focus. 2. Add a bell emoji (🔔) to the title of the surface that triggered the bell. This emoji will be removed after the surface is focused or a keyboard event if the surface is already focused. This behavior matches iTerm2. This doesn't add an icon badge because macOS's dockTitle.badgeLabel API wasn't doing anything for me and I wasn't able to fully figure out why... --- macos/Sources/App/macOS/AppDelegate.swift | 11 ++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 27 +++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Config.swift | 14 ++++++++++ macos/Sources/Ghostty/Package.swift | 3 +++ macos/Sources/Ghostty/SurfaceView.swift | 11 +++++++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 21 ++++++++++++++- macos/Sources/Ghostty/SurfaceView_UIKit.swift | 3 +++ src/config/Config.zig | 8 +++++- 8 files changed, 95 insertions(+), 3 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7d5e7cd25..9d866d734 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -186,6 +186,12 @@ class AppDelegate: NSObject, name: .ghosttyConfigDidChange, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(ghosttyBellDidRing(_:)), + name: .ghosttyBellDidRing, + object: nil + ) // Configure user notifications let actions = [ @@ -502,6 +508,11 @@ class AppDelegate: NSObject, ghosttyConfigDidChange(config: config) } + @objc private func ghosttyBellDidRing(_ notification: Notification) { + // Bounce the dock icon if we're not focused. + NSApp.requestUserAttention(.informationalRequest) + } + private func ghosttyConfigDidChange(config: Ghostty.Config) { // Update the config we need to store self.derivedConfig = DerivedConfig(config) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index ddb954e04..dfd066870 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -538,6 +538,9 @@ extension Ghostty { case GHOSTTY_ACTION_COLOR_CHANGE: colorChange(app, target: target, change: action.action.color_change) + case GHOSTTY_ACTION_RING_BELL: + ringBell(app, target: target) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -747,6 +750,30 @@ extension Ghostty { appDelegate.toggleVisibility(self) } + private static func ringBell( + _ app: ghostty_app_t, + target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + // Technically we could still request app attention here but there + // are no known cases where the bell is rang with an app target so + // I think its better to warn. + Ghostty.logger.warning("ring bell does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + NotificationCenter.default.post( + name: .ghosttyBellDidRing, + object: surfaceView + ) + + default: + assertionFailure() + } + } + private static func moveTab( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 20a43aa2b..d146477dc 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -116,6 +116,14 @@ extension Ghostty { /// details on what each means. We only add documentation if there is a strange conversion /// due to the embedded library and Swift. + var bellFeatures: BellFeatures { + guard let config = self.config else { return .init() } + var v: CUnsignedInt = 0 + let key = "bell-features" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .init() } + return .init(rawValue: v) + } + var initialWindow: Bool { guard let config = self.config else { return true } var v = true; @@ -543,6 +551,12 @@ extension Ghostty.Config { case download } + struct BellFeatures: OptionSet { + let rawValue: CUnsignedInt + + static let system = BellFeatures(rawValue: 1 << 0) + } + enum MacHidden : String { case never case always diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index cda4b557e..3afca56aa 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -253,6 +253,9 @@ extension Notification.Name { /// Resize the window to a default size. static let ghosttyResetWindowSize = Notification.Name("com.mitchellh.ghostty.resetWindowSize") + + /// Ring the bell + static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing") } // NOTE: I am moving all of these to Notification.Name extensions over time. This diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index beae50331..7eebd3ef1 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -59,6 +59,15 @@ extension Ghostty { @EnvironmentObject private var ghostty: Ghostty.App + var title: String { + var result = surfaceView.title + if (surfaceView.bell) { + result = "🔔 \(result)" + } + + return result + } + var body: some View { let center = NotificationCenter.default @@ -74,7 +83,7 @@ extension Ghostty { Surface(view: surfaceView, size: geo.size) .focused($surfaceFocus) - .focusedValue(\.ghosttySurfaceTitle, surfaceView.title) + .focusedValue(\.ghosttySurfaceTitle, title) .focusedValue(\.ghosttySurfacePwd, surfaceView.pwd) .focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 230d3a9e2..1269f2314 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -63,6 +63,9 @@ extension Ghostty { /// dynamically updated. Otherwise, the background color is the default background color. @Published private(set) var backgroundColor: Color? = nil + /// True when the bell is active. This is set inactive on focus or event. + @Published private(set) var bell: Bool = false + // An initial size to request for a window. This will only affect // then the view is moved to a new window. var initialSize: NSSize? = nil @@ -190,6 +193,11 @@ extension Ghostty { selector: #selector(ghosttyColorDidChange(_:)), name: .ghosttyColorDidChange, object: self) + center.addObserver( + self, + selector: #selector(ghosttyBellDidRing(_:)), + name: .ghosttyBellDidRing, + object: self) center.addObserver( self, selector: #selector(windowDidChangeScreen), @@ -300,9 +308,12 @@ extension Ghostty { SecureInput.shared.setScoped(ObjectIdentifier(self), focused: focused) } - // On macOS 13+ we can store our continuous clock... if (focused) { + // On macOS 13+ we can store our continuous clock... focusInstant = ContinuousClock.now + + // We unset our bell state if we gained focus + bell = false } } @@ -556,6 +567,11 @@ extension Ghostty { } } + @objc private func ghosttyBellDidRing(_ notification: SwiftUI.Notification) { + // Bell state goes to true + bell = true + } + @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { guard let window = self.window else { return } guard let object = notification.object as? NSWindow, window == object else { return } @@ -855,6 +871,9 @@ extension Ghostty { return } + // On any keyDown event we unset our bell state + bell = false + // We need to translate the mods (maybe) to handle configs such as option-as-alt let translationModsGhostty = Ghostty.eventModifierFlags( mods: ghostty_surface_key_translation_mods( diff --git a/macos/Sources/Ghostty/SurfaceView_UIKit.swift b/macos/Sources/Ghostty/SurfaceView_UIKit.swift index 8ac08d0bd..8d5b3038f 100644 --- a/macos/Sources/Ghostty/SurfaceView_UIKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_UIKit.swift @@ -35,6 +35,9 @@ extension Ghostty { // on supported platforms. @Published var focusInstant: ContinuousClock.Instant? = nil + /// True when the bell is active. This is set inactive on focus or event. + @Published var bell: Bool = false + // Returns sizing information for the surface. This is the raw C // structure because I'm lazy. var surfaceSize: ghostty_surface_size_s? { diff --git a/src/config/Config.zig b/src/config/Config.zig index f648e8a28..d57ed161b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1874,7 +1874,13 @@ keybind: Keybinds = .{}, /// for instance under the "Sound > Alert Sound" setting in GNOME, /// or the "Accessibility > System Bell" settings in KDE Plasma. /// -/// Currently only implemented on Linux. +/// On macOS this has no affect. +/// +/// On macOS, if the app is unfocused, it will bounce the app icon in the dock +/// once. Additionally, the title of the window with the alerted terminal +/// surface will contain a bell emoji (🔔) until the terminal is focused +/// or a key is pressed. These are not currently configurable since they're +/// considered unobtrusive. @"bell-features": BellFeatures = .{}, /// Control the in-app notifications that Ghostty shows. From b3cb38c3fa65c2b4ff631bc9b73a5eeba3c01348 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Apr 2025 10:15:47 -0700 Subject: [PATCH 093/642] macOS/libghostty: rework keyboard input handling This is a large refactor of the keyboard input handling code in libghostty and macOS. Previously, libghostty did a lot of things that felt out of scope or was repeated work due to lacking context. For example, libghostty would do full key translation from key event to character (including unshifted translation) as well as managing dead key states and setting the proper preedit text. This is all information the apprt can and should have on its own. NSEvent on macOS already provides us with all of this information, there's no need to redo the work. The reason we did in the first place is mostly historical: libghostty powered our initial macOS port years ago when we didn't have an AppKit runtime yet. This cruft has already practically been the source of numerous issues, e.g. #5558, but many other hacks along the way, too. This commit pushes all preedit (e.g. dead key) handling and key translation including unshifted keys up into the caller of libghostty. Besides code cleanup, a practical benefit of this is that key event handling on macOS is now about 10x faster on average. That's because we're avoiding repeated key translations as well as other unnecessary work. This should have a meaningful impact on input latency but I didn't measure the full end-to-end latency. A scarier part of this commit is that key handling is not well tested since its a GUI component. I suspect we'll have some fallout for certain keyboard layouts or input methods, but I did my best to run through everything I could think of. --- include/ghostty.h | 3 + macos/Sources/Ghostty/NSEvent+Extension.swift | 47 ++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 84 ++-- src/apprt/embedded.zig | 367 +++++------------- 4 files changed, 176 insertions(+), 325 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index f30275b2c..c4ef11930 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -254,8 +254,10 @@ typedef enum { typedef struct { ghostty_input_action_e action; ghostty_input_mods_e mods; + ghostty_input_mods_e consumed_mods; uint32_t keycode; const char* text; + uint32_t unshifted_codepoint; bool composing; } ghostty_input_key_s; @@ -725,6 +727,7 @@ ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); +void ghostty_surface_preedit(ghostty_surface_t, const char*, uintptr_t); bool ghostty_surface_mouse_captured(ghostty_surface_t); bool ghostty_surface_mouse_button(ghostty_surface_t, ghostty_input_mouse_state_e, diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index 4118cd94d..5c13003b3 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -3,13 +3,56 @@ import GhosttyKit extension NSEvent { /// Create a Ghostty key event for a given keyboard action. + /// + /// This will not set the "text" or "composing" fields since these can't safely be set + /// with the information or lifetimes given. func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s { - var key_ev = ghostty_input_key_s() + var key_ev: ghostty_input_key_s = .init() key_ev.action = action - key_ev.mods = Ghostty.ghosttyMods(modifierFlags) key_ev.keycode = UInt32(keyCode) + + // We can't infer or set these safely from this method. Since text is + // a cString, we can't use self.characters because of garbage collection. + // We have to let the caller handle this. key_ev.text = nil key_ev.composing = false + + // macOS provides no easy way to determine the consumed modifiers for + // producing text. We apply a simple heuristic here that has worked for years + // so far: control and command never contribute to the translation of text, + // assume everything else did. + key_ev.mods = Ghostty.ghosttyMods(modifierFlags) + key_ev.consumed_mods = Ghostty.ghosttyMods(modifierFlags.subtracting([.control, .command])) + + // Our unshifted codepoint is the codepoint with no modifiers. We + // ignore multi-codepoint values. + key_ev.unshifted_codepoint = 0 + if let charactersIgnoringModifiers, + let codepoint = charactersIgnoringModifiers.unicodeScalars.first + { + key_ev.unshifted_codepoint = codepoint.value + } + return key_ev } + + /// Returns the text to set for a key event for Ghostty. + /// + /// This namely contains logic to avoid control characters, since we handle control character + /// mapping manually within Ghostty. + var ghosttyCharacters: String? { + // If we have no characters associated with this event we do nothing. + guard let characters else { return nil } + + // If we have a single control character, then we return the characters + // without control pressed. We do this because we handle control character + // encoding directly within Ghostty's KeyEncoder. + if characters.count == 1, + let scalar = characters.unicodeScalars.first, + scalar.value < 0x20 { + return self.characters(byApplyingModifiers: modifierFlags.subtracting(.control)) + } + + return nil + } } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 1269f2314..9e5dc0559 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -951,29 +951,39 @@ extension Ghostty { return } - // If we have text, then we've composed a character, send that down. We do this - // first because if we completed a preedit, the text will be available here - // AND we'll have a preedit. - var handled: Bool = false - if let list = keyTextAccumulator, list.count > 0 { - handled = true - for text in list { - _ = keyAction(action, event: event, text: text) + // If we have marked text, we're in a preedit state. The order we + // do this and the key event callbacks below doesn't matter since + // we control the preedit state only through the preedit API. + if markedText.length > 0 { + let str = markedText.string + let len = str.utf8CString.count + if len > 0 { + markedText.string.withCString { ptr in + // Subtract 1 for the null terminator + ghostty_surface_preedit(surface, ptr, UInt(len - 1)) + } } + } else if markedTextBefore { + // If we had marked text before but don't now, we're no longer + // in a preedit state so we can clear it. + ghostty_surface_preedit(surface, nil, 0) } - // If we have marked text, we're in a preedit state. Send that down. - // If we don't have marked text but we had marked text before, then the preedit - // was cleared so we want to send down an empty string to ensure we've cleared - // the preedit. - if (markedText.length > 0 || markedTextBefore) { - handled = true - _ = keyAction(action, event: event, preedit: markedText.string) - } - - if (!handled) { - // No text or anything, we want to handle this manually. - _ = keyAction(action, event: event) + if let list = keyTextAccumulator, list.count > 0 { + // If we have text, then we've composed a character, send that down. + // These never have "composing" set to true because these are the + // result of a composition. + for text in list { + _ = keyAction(action, event: translationEvent, text: text) + } + } else { + // We have no accumulated text so this is a normal key event. + _ = keyAction( + action, + event: translationEvent, + text: translationEvent.ghosttyCharacters, + composing: markedText.length > 0 + ) } } @@ -1165,34 +1175,22 @@ extension Ghostty { _ = keyAction(action, event: event) } - private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) -> Bool { - guard let surface = self.surface else { return false } - return ghostty_surface_key(surface, event.ghosttyKeyEvent(action)) - } - private func keyAction( _ action: ghostty_input_action_e, - event: NSEvent, preedit: String + event: NSEvent, + text: String? = nil, + composing: Bool = false ) -> Bool { guard let surface = self.surface else { return false } - return preedit.withCString { ptr in - var key_ev = event.ghosttyKeyEvent(action) - key_ev.text = ptr - key_ev.composing = true - return ghostty_surface_key(surface, key_ev) - } - } - - private func keyAction( - _ action: ghostty_input_action_e, - event: NSEvent, text: String - ) -> Bool { - guard let surface = self.surface else { return false } - - return text.withCString { ptr in - var key_ev = event.ghosttyKeyEvent(action) - key_ev.text = ptr + var key_ev = event.ghosttyKeyEvent(action) + key_ev.composing = composing + if let text { + return text.withCString { ptr in + key_ev.text = ptr + return ghostty_surface_key(surface, key_ev) + } + } else { return ghostty_surface_key(surface, key_ev) } } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 50b54435d..e8da8612c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -73,16 +73,68 @@ pub const App = struct { /// This is the key event sent for ghostty_surface_key and /// ghostty_app_key. pub const KeyEvent = struct { - /// The three below are absolutely required. action: input.Action, mods: input.Mods, + consumed_mods: input.Mods, keycode: u32, - - /// Optionally, the embedder can handle text translation and send - /// the text value here. If text is non-nil, it is assumed that the - /// embedder also handles dead key states and sets composing as necessary. text: ?[:0]const u8, + unshifted_codepoint: u32, composing: bool, + + /// Convert a libghostty key event into a core key event. + fn core(self: KeyEvent) ?input.KeyEvent { + const text: []const u8 = if (self.text) |v| v else ""; + const unshifted_codepoint: u21 = std.math.cast( + u21, + self.unshifted_codepoint, + ) orelse 0; + + // We want to get the physical unmapped key to process keybinds. + const physical_key = keycode: for (input.keycodes.entries) |entry| { + if (entry.native == self.keycode) break :keycode entry.key; + } else .invalid; + + // If the resulting text has length 1 then we can take its key + // and attempt to translate it to a key enum and call the key callback. + // If the length is greater than 1 then we're going to call the + // charCallback. + // + // We also only do key translation if this is not a dead key. + const key = if (!self.composing) key: { + // If our physical key is a keypad key, we use that. + if (physical_key.keypad()) break :key physical_key; + + // A completed key. If the length of the key is one then we can + // attempt to translate it to a key enum and call the key + // callback. First try plain ASCII. + if (text.len > 0) { + if (input.Key.fromASCII(text[0])) |key| { + break :key key; + } + } + + // If the above doesn't work, we use the unmodified value. + if (std.math.cast(u8, unshifted_codepoint)) |ascii| { + if (input.Key.fromASCII(ascii)) |key| { + break :key key; + } + } + + break :key physical_key; + } else .invalid; + + // Build our final key event + return .{ + .action = self.action, + .key = key, + .physical_key = physical_key, + .mods = self.mods, + .consumed_mods = self.consumed_mods, + .composing = self.composing, + .utf8 = text, + .unshifted_codepoint = unshifted_codepoint, + }; + } }; core_app: *CoreApp, @@ -92,10 +144,6 @@ pub const App = struct { /// The configuration for the app. This is owned by this structure. config: Config, - /// The keymap state is used for global keybinds only. Each surface - /// also has its own keymap state for focused keybinds. - keymap_state: input.Keymap.State, - pub fn init( core_app: *CoreApp, config: *const Config, @@ -114,7 +162,6 @@ pub const App = struct { .config = config_clone, .opts = opts, .keymap = keymap, - .keymap_state = .{}, }; } @@ -148,219 +195,6 @@ pub const App = struct { self.core_app.focusEvent(focused); } - /// Convert a C key event into a Zig key event. - /// - /// The buffer is needed for possibly storing translated UTF-8 text. - /// This buffer may (or may not) be referenced by the resulting KeyEvent - /// so it should be valid for the lifetime of the KeyEvent. - /// - /// The size of the buffer doesn't need to be large, we always - /// used to hardcode 128 bytes and never ran into issues. If it isn't - /// large enough an error will be returned. - fn coreKeyEvent( - self: *App, - buf: []u8, - target: KeyTarget, - event: KeyEvent, - ) !?input.KeyEvent { - const action = event.action; - const keycode = event.keycode; - const mods = event.mods; - - // True if this is a key down event - const is_down = action == .press or action == .repeat; - - // If we're on macOS and we have macos-option-as-alt enabled, - // then we strip the alt modifier from the mods for translation. - const translate_mods = translate_mods: { - var translate_mods = mods; - if ((comptime builtin.target.os.tag.isDarwin()) and translate_mods.alt) { - // Note: the keyboardLayout() function is not super cheap - // so we only want to run it if alt is already pressed hence - // the above condition. - const option_as_alt: configpkg.OptionAsAlt = - self.config.@"macos-option-as-alt" orelse - self.keyboardLayout().detectOptionAsAlt(); - - const strip = switch (option_as_alt) { - .false => false, - .true => mods.alt, - .left => mods.sides.alt == .left, - .right => mods.sides.alt == .right, - }; - if (strip) translate_mods.alt = false; - } - - // We strip super on macOS because its not used for translation - // it results in a bad translation. - if (comptime builtin.target.os.tag.isDarwin()) { - translate_mods.super = false; - } - - break :translate_mods translate_mods; - }; - - const event_text: ?[]const u8 = event_text: { - // This logic only applies to macOS. - if (comptime builtin.os.tag != .macos) break :event_text event.text; - - // If we're in a preedit state then we allow it through. This - // allows ctrl sequences that affect IME to work. For example, - // Ctrl+H deletes a character with Japanese input. - if (event.composing) break :event_text event.text; - - // If the modifiers are ONLY "control" then we never process - // the event text because we want to do our own translation so - // we can handle ctrl+c, ctrl+z, etc. - // - // This is specifically because on macOS using the - // "Dvorak - QWERTY ⌘" keyboard layout, ctrl+z is translated as - // "/" (the physical key that is z on a qwerty keyboard). But on - // other layouts, ctrl+ is not translated by AppKit. So, - // we just avoid this by never allowing AppKit to translate - // ctrl+ and instead do it ourselves. - const ctrl_only = comptime (input.Mods{ .ctrl = true }).int(); - break :event_text if (mods.binding().int() == ctrl_only) null else event.text; - }; - - // Translate our key using the keymap for our localized keyboard layout. - // We only translate for keydown events. Otherwise, we only care about - // the raw keycode. - const result: input.Keymap.Translation = if (is_down) translate: { - // If the event provided us with text, then we use this as a result - // and do not do manual translation. - const result: input.Keymap.Translation = if (event_text) |text| .{ - .text = text, - .composing = event.composing, - .mods = translate_mods, - } else try self.keymap.translate( - buf, - switch (target) { - .app => &self.keymap_state, - .surface => |surface| &surface.keymap_state, - }, - @intCast(keycode), - translate_mods, - ); - - // TODO(mitchellh): I think we can get rid of the above keymap - // translation code completely and defer to AppKit/Swift - // (for macOS) for handling all translations. The translation - // within libghostty is an artifact of an earlier design and - // it is buggy (see #5558). We should move closer to a GTK-style - // model of tracking composing states and preedit in the apprt - // and not in libghostty. - - // If this is a dead key, then we're composing a character and - // we need to set our proper preedit state if we're targeting a - // surface. - if (result.composing) { - switch (target) { - .app => {}, - .surface => |surface| surface.core_surface.preeditCallback( - result.text, - ) catch |err| { - log.err("error in preedit callback err={}", .{err}); - return null; - }, - } - } else { - switch (target) { - .app => {}, - .surface => |surface| surface.core_surface.preeditCallback(null) catch |err| { - log.err("error in preedit callback err={}", .{err}); - return null; - }, - } - - // If the text is just a single non-printable ASCII character - // then we clear the text. We handle non-printables in the - // key encoder manual (such as tab, ctrl+c, etc.) - if (result.text.len == 1 and result.text[0] < 0x20) { - break :translate .{}; - } - } - - break :translate result; - } else .{}; - - // We need to always do a translation with no modifiers at all in - // order to get the "unshifted_codepoint" for the key event. - const unshifted_codepoint: u21 = unshifted: { - var nomod_buf: [128]u8 = undefined; - var nomod_state: input.Keymap.State = .{}; - const nomod = try self.keymap.translate( - &nomod_buf, - &nomod_state, - @intCast(keycode), - .{}, - ); - - const view = std.unicode.Utf8View.init(nomod.text) catch |err| { - log.warn("cannot build utf8 view over text: {}", .{err}); - break :unshifted 0; - }; - var it = view.iterator(); - break :unshifted it.nextCodepoint() orelse 0; - }; - - // log.warn("TRANSLATE: action={} keycode={x} dead={} key_len={} key={any} key_str={s} mods={}", .{ - // action, - // keycode, - // result.composing, - // result.text.len, - // result.text, - // result.text, - // mods, - // }); - - // We want to get the physical unmapped key to process keybinds. - const physical_key = keycode: for (input.keycodes.entries) |entry| { - if (entry.native == keycode) break :keycode entry.key; - } else .invalid; - - // If the resulting text has length 1 then we can take its key - // and attempt to translate it to a key enum and call the key callback. - // If the length is greater than 1 then we're going to call the - // charCallback. - // - // We also only do key translation if this is not a dead key. - const key = if (!result.composing) key: { - // If our physical key is a keypad key, we use that. - if (physical_key.keypad()) break :key physical_key; - - // A completed key. If the length of the key is one then we can - // attempt to translate it to a key enum and call the key - // callback. First try plain ASCII. - if (result.text.len > 0) { - if (input.Key.fromASCII(result.text[0])) |key| { - break :key key; - } - } - - // If the above doesn't work, we use the unmodified value. - if (std.math.cast(u8, unshifted_codepoint)) |ascii| { - if (input.Key.fromASCII(ascii)) |key| { - break :key key; - } - } - - break :key physical_key; - } else .invalid; - - // Build our final key event - return .{ - .action = action, - .key = key, - .physical_key = physical_key, - .mods = mods, - .consumed_mods = result.mods, - .composing = result.composing, - .utf8 = result.text, - .unshifted_codepoint = unshifted_codepoint, - }; - } - /// See CoreApp.keyEvent. pub fn keyEvent( self: *App, @@ -368,12 +202,8 @@ pub const App = struct { event: KeyEvent, ) !bool { // Convert our C key event into a Zig one. - var buf: [128]u8 = undefined; - const input_event: input.KeyEvent = (try self.coreKeyEvent( - &buf, - target, - event, - )) orelse return false; + const input_event: input.KeyEvent = event.core() orelse + return false; // Invoke the core Ghostty logic to handle this input. const effect: CoreSurface.InputEffect = switch (target) { @@ -390,23 +220,7 @@ pub const App = struct { return switch (effect) { .closed => true, .ignored => false, - .consumed => consumed: { - const is_down = input_event.action == .press or - input_event.action == .repeat; - - if (is_down) { - // If we consume the key then we want to reset the dead - // key state. - self.keymap_state = .{}; - - switch (target) { - .app => {}, - .surface => |surface| surface.core_surface.preeditCallback(null) catch {}, - } - } - - break :consumed true; - }, + .consumed => true, }; } @@ -414,13 +228,6 @@ pub const App = struct { pub fn reloadKeymap(self: *App) !void { // Reload the keymap try self.keymap.reload(); - - // Clear the dead key state since we changed the keymap, any - // dead key state is just forgotten. i.e. if you type ' on us-intl - // and then switch to us and type a, you'll get a rather than á. - for (self.core_app.surfaces.items) |surface| { - surface.keymap_state = .{}; - } } /// Loads the keyboard layout. @@ -607,7 +414,6 @@ pub const Surface = struct { content_scale: apprt.ContentScale, size: apprt.SurfaceSize, cursor_pos: apprt.CursorPos, - keymap_state: input.Keymap.State, inspector: ?*Inspector = null, /// The current title of the surface. The embedded apprt saves this so @@ -656,7 +462,6 @@ pub const Surface = struct { }, .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = -1, .y = -1 }, - .keymap_state = .{}, }; // Add ourselves to the list of surfaces on the app. @@ -992,6 +797,13 @@ pub const Surface = struct { }; } + pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) void { + _ = self.core_surface.preeditCallback(preedit_) catch |err| { + log.err("error in preedit callback err={}", .{err}); + return; + }; + } + pub fn textCallback(self: *Surface, text: []const u8) void { _ = self.core_surface.textCallback(text) catch |err| { log.err("error in key callback err={}", .{err}); @@ -1082,7 +894,6 @@ pub const Inspector = struct { surface: *Surface, ig_ctx: *cimgui.c.ImGuiContext, backend: ?Backend = null, - keymap_state: input.Keymap.State = .{}, content_scale: f64 = 1, /// Our previous instant used to calculate delta time for animations. @@ -1328,11 +1139,13 @@ pub const CAPI = struct { const KeyEvent = extern struct { action: input.Action, mods: c_int, + consumed_mods: c_int, keycode: u32, text: ?[*:0]const u8, + unshifted_codepoint: u32, composing: bool, - /// Convert to surface key event. + /// Convert to Zig key event. fn keyEvent(self: KeyEvent) App.KeyEvent { return .{ .action = self.action, @@ -1340,8 +1153,13 @@ pub const CAPI = struct { input.Mods.Backing, @truncate(@as(c_uint, @bitCast(self.mods))), )), + .consumed_mods = @bitCast(@as( + input.Mods.Backing, + @truncate(@as(c_uint, @bitCast(self.consumed_mods))), + )), .keycode = self.keycode, .text = if (self.text) |ptr| std.mem.sliceTo(ptr, 0) else null, + .unshifted_codepoint = self.unshifted_codepoint, .composing = self.composing, }; } @@ -1447,15 +1265,7 @@ pub const CAPI = struct { app: *App, event: KeyEvent, ) bool { - var buf: [128]u8 = undefined; - const core_event = app.coreKeyEvent( - &buf, - .app, - event.keyEvent(), - ) catch |err| { - log.warn("error processing key event err={}", .{err}); - return false; - } orelse { + const core_event = event.keyEvent().core() orelse { log.warn("error processing key event", .{}); return false; }; @@ -1701,20 +1511,7 @@ pub const CAPI = struct { surface: *Surface, event: KeyEvent, ) bool { - var buf: [128]u8 = undefined; - const core_event = surface.app.coreKeyEvent( - &buf, - // Note: this "app" target here looks like a bug, but it is - // intentional. coreKeyEvent uses the target only as a way to - // trigger preedit callbacks for keymap translation and we don't - // want to trigger that here. See the todo item in coreKeyEvent - // for a long term solution to this and removing target altogether. - .app, - event.keyEvent(), - ) catch |err| { - log.warn("error processing key event err={}", .{err}); - return false; - } orelse { + const core_event = event.keyEvent().core() orelse { log.warn("error processing key event", .{}); return false; }; @@ -1733,6 +1530,16 @@ pub const CAPI = struct { surface.textCallback(ptr[0..len]); } + /// Set the preedit text for the surface. This is used for IME + /// composition. If the length is 0, then the preedit text is cleared. + export fn ghostty_surface_preedit( + surface: *Surface, + ptr: [*]const u8, + len: usize, + ) void { + surface.preeditCallback(if (len == 0) null else ptr[0..len]); + } + /// Returns true if the surface currently has mouse capturing /// enabled. export fn ghostty_surface_mouse_captured(surface: *Surface) bool { From ded9be39c03df45e6a3b81f5f4d53935563ae737 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 17 Apr 2025 13:36:15 -0700 Subject: [PATCH 094/642] macOS: handle preedit text changes outside of key event --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 9e5dc0559..52314f534 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -954,20 +954,7 @@ extension Ghostty { // If we have marked text, we're in a preedit state. The order we // do this and the key event callbacks below doesn't matter since // we control the preedit state only through the preedit API. - if markedText.length > 0 { - let str = markedText.string - let len = str.utf8CString.count - if len > 0 { - markedText.string.withCString { ptr in - // Subtract 1 for the null terminator - ghostty_surface_preedit(surface, ptr, UInt(len - 1)) - } - } - } else if markedTextBefore { - // If we had marked text before but don't now, we're no longer - // in a preedit state so we can clear it. - ghostty_surface_preedit(surface, nil, 0) - } + syncPreedit(clearIfNeeded: markedTextBefore) if let list = keyTextAccumulator, list.count > 0 { // If we have text, then we've composed a character, send that down. @@ -1466,10 +1453,21 @@ extension Ghostty.SurfaceView: NSTextInputClient { default: print("unknown marked text: \(string)") } + + // If we're not in a keyDown event, then we want to update our preedit + // text immediately. This can happen due to external events, for example + // changing keyboard layouts while composing: (1) set US intl (2) type ' + // to enter dead key state (3) + if keyTextAccumulator == nil { + syncPreedit() + } } func unmarkText() { - self.markedText.mutableString.setString("") + if self.markedText.length > 0 { + self.markedText.mutableString.setString("") + syncPreedit() + } } func validAttributesForMarkedText() -> [NSAttributedString.Key] { @@ -1608,6 +1606,26 @@ extension Ghostty.SurfaceView: NSTextInputClient { print("SEL: \(selector)") } + + /// Sync the preedit state based on the markedText value to libghostty + private func syncPreedit(clearIfNeeded: Bool = true) { + guard let surface else { return } + + if markedText.length > 0 { + let str = markedText.string + let len = str.utf8CString.count + if len > 0 { + markedText.string.withCString { ptr in + // Subtract 1 for the null terminator + ghostty_surface_preedit(surface, ptr, UInt(len - 1)) + } + } + } else if clearIfNeeded { + // If we had marked text before but don't now, we're no longer + // in a preedit state so we can clear it. + ghostty_surface_preedit(surface, nil, 0) + } + } } // MARK: Services From bef0d5d88e2289a48bef29bf0aa8017b52ca9e68 Mon Sep 17 00:00:00 2001 From: ekusiadadus Date: Fri, 18 Apr 2025 07:22:40 +0900 Subject: [PATCH 095/642] fix: spelling inconsistencies for Japanese Translations --- po/ja_JP.UTF-8.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index 7a4ee6929..e06ec2fbc 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -1,5 +1,5 @@ # Japanese translations for com.mitchellh.ghostty package -# com.mitchellh.ghostty パッケージに対する英訳. +# com.mitchellh.ghostty パッケージに対する和訳. # Copyright (C) 2025 Mitchell Hashimoto # This file is distributed under the same license as the com.mitchellh.ghostty package. # Lon Sagisawa , 2025. @@ -37,7 +37,7 @@ msgstr "OK" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "設定エラー" +msgstr "設定ファイルにエラーがあります" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 msgid "" @@ -192,7 +192,7 @@ msgstr "" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 msgid "Warning: Potentially Unsafe Paste" -msgstr "警告: 危険な可能性のあるペースト" +msgstr "警告: 危険な可能性のある貼り付け" #: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 msgid "" @@ -232,19 +232,19 @@ msgstr "分割ウィンドウを閉じますか?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." -msgstr "すべてのターミナルセッションが終了されます。" +msgstr "すべてのターミナルセッションが終了します。" #: src/apprt/gtk/CloseDialog.zig:97 msgid "All terminal sessions in this window will be terminated." -msgstr "ウィンドウ内のすべてのターミナルセッションが終了されます。" +msgstr "ウィンドウ内のすべてのターミナルセッションが終了します。" #: src/apprt/gtk/CloseDialog.zig:98 msgid "All terminal sessions in this tab will be terminated." -msgstr "タブ内のすべてのターミナルセッションが終了されます。" +msgstr "タブ内のすべてのターミナルセッションが終了します。" #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "分割ウィンドウ内のすべてのターミナルセッションが終了されます。" +msgstr "分割ウィンドウ内のすべてのプロセスが終了します。" #: src/apprt/gtk/Window.zig:200 msgid "Main Menu" From 90499f074987e6deab04ae6156a12eabea268d3e Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Fri, 18 Apr 2025 08:58:36 +0800 Subject: [PATCH 096/642] macOS: Add dock badge notification for bell events --- macos/Sources/App/macOS/AppDelegate.swift | 55 ++++++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 9d866d734..643cdaf36 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -199,6 +199,7 @@ class AppDelegate: NSObject, ] let center = UNUserNotificationCenter.current() + center.setNotificationCategories([ UNNotificationCategory( identifier: Ghostty.userNotificationCategory, @@ -231,6 +232,9 @@ class AppDelegate: NSObject, // If we're back manually then clear the hidden state because macOS handles it. self.hiddenState = nil + // Clear the dock badge when the app becomes active + self.setDockBadge(nil) + // First launch stuff if (!applicationHasBecomeActive) { applicationHasBecomeActive = true @@ -511,6 +515,53 @@ class AppDelegate: NSObject, @objc private func ghosttyBellDidRing(_ notification: Notification) { // Bounce the dock icon if we're not focused. NSApp.requestUserAttention(.informationalRequest) + + // Handle setting the dock badge based on permissions + ghosttyUpdateBadgeForBell() + } + + private func ghosttyUpdateBadgeForBell() { + let center = UNUserNotificationCenter.current() + center.getNotificationSettings { settings in + switch settings.authorizationStatus { + case .authorized: + // Already authorized, check badge setting and set if enabled + if settings.badgeSetting == .enabled { + DispatchQueue.main.async { + self.setDockBadge() + } + } + + case .notDetermined: + // Not determined yet, request authorization for badge + center.requestAuthorization(options: [.badge]) { granted, error in + if let error = error { + Self.logger.warning("Error requesting badge authorization: \(error)") + return + } + + if granted { + // Permission granted, set the badge + DispatchQueue.main.async { + self.setDockBadge() + } + } + } + + case .denied, .provisional, .ephemeral: + // In these known non-authorized states, do not attempt to set the badge. + break + + @unknown default: + // Handle future unknown states by doing nothing. + break + } + } + } + + private func setDockBadge(_ label: String? = "•") { + NSApp.dockTile.badgeLabel = label + NSApp.dockTile.display() } private func ghosttyConfigDidChange(config: Ghostty.Config) { @@ -790,12 +841,12 @@ class AppDelegate: NSObject, hiddenState?.restore() hiddenState = nil } - + @IBAction func bringAllToFront(_ sender: Any) { if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) } - + NSApplication.shared.arrangeInFront(sender) } From 9bab900c753ae75b85bde4a9615a3e0f60492f0b Mon Sep 17 00:00:00 2001 From: phanium <91544758+phanen@users.noreply.github.com> Date: Fri, 18 Apr 2025 02:16:27 +0800 Subject: [PATCH 097/642] vim: fix syntax highlight on scratch buffer --- src/config/vim.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/vim.zig b/src/config/vim.zig index 6084bd248..17ab0bc2e 100644 --- a/src/config/vim.zig +++ b/src/config/vim.zig @@ -88,6 +88,7 @@ fn writeSyntax(writer: anytype) !void { \\let s:cpo_save = &cpo \\set cpo&vim \\ + \\syn iskeyword @,48-57,- \\syn keyword ghosttyConfigKeyword ); From 34ddd3d9e5a8f12191306a1207692d8fd38a9ca8 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Fri, 18 Apr 2025 23:34:18 +0800 Subject: [PATCH 098/642] Refine Chinese translations for daily usage consistency --- po/zh_CN.UTF-8.po | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 795a93585..cdb4c3873 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -36,14 +36,14 @@ msgstr "确认" #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5 msgid "Configuration Errors" -msgstr "设置错误" +msgstr "配置错误" #: src/apprt/gtk/ui/1.5/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 msgid "Ignore" @@ -53,7 +53,7 @@ msgstr "忽略" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 msgid "Reload Configuration" -msgstr "重新加载设置" +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 @@ -69,7 +69,7 @@ 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 "清除界面" +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 @@ -137,16 +137,16 @@ msgstr "关闭窗口" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 msgid "Config" -msgstr "设置" +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 msgid "Open Configuration" -msgstr "打开设置文件" +msgstr "打开配置文件" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Terminal Inspector" -msgstr "终端检视器" +msgstr "终端调试器" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 #: src/apprt/gtk/Window.zig:960 @@ -160,13 +160,13 @@ 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 msgid "Authorize Clipboard Access" -msgstr "剪切板访问授权" +msgstr "剪贴板访问授权" #: src/apprt/gtk/ui/1.5/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 @@ -182,7 +182,7 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -196,11 +196,11 @@ msgstr "将以下内容粘贴至终端内将可能执行有害命令。" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty 终端检视器" +msgstr "Ghostty 终端调试器" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" -msgstr "已复制至剪切板" +msgstr "已复制至剪贴板" #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" @@ -253,7 +253,7 @@ msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" #: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" -msgstr "已重新加载设置" +msgstr "已重新加载配置" #: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" From 793c727986c005a61103973e31a6f598448e69b5 Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Fri, 18 Apr 2025 12:41:59 -0500 Subject: [PATCH 099/642] apprt/gtk: refactor action callbacks to reduce code duplication It was getting very monotonous reading the code. Signed-off-by: Tristan Partin --- src/apprt/gtk/Window.zig | 86 ++++++++++------------------------------ 1 file changed, 20 insertions(+), 66 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 5fcb0d42b..129c149e7 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -811,16 +811,19 @@ fn gtkWindowUpdateScaleFactor( }; } -// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab -// sends an undefined value. -fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { +/// Perform a binding action on the window's action surface. +fn performBindingAction(self: *Window, action: input.Binding.Action) void { const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .new_tab = {} }) catch |err| { + _ = surface.performBindingAction(action) catch |err| { log.warn("error performing binding action error={}", .{err}); return; }; } +fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { + self.performBindingAction(.{ .new_tab = {} }); +} + /// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick /// because we need to return an AdwTabPage from this function. fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage { @@ -1007,11 +1010,7 @@ fn gtkActionNewWindow( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .new_window = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .new_window = {} }); } fn gtkActionNewTab( @@ -1019,8 +1018,7 @@ fn gtkActionNewTab( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - // We can use undefined because the button is not used. - gtkTabNewClick(undefined, self); + self.performBindingAction(.{ .new_tab = {} }); } fn gtkActionCloseTab( @@ -1028,11 +1026,7 @@ fn gtkActionCloseTab( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .close_tab = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .close_tab = {} }); } fn gtkActionSplitRight( @@ -1040,11 +1034,7 @@ fn gtkActionSplitRight( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .new_split = .right }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .new_split = .right }); } fn gtkActionSplitDown( @@ -1052,11 +1042,7 @@ fn gtkActionSplitDown( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .new_split = .down }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .new_split = .down }); } fn gtkActionSplitLeft( @@ -1064,11 +1050,7 @@ fn gtkActionSplitLeft( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .new_split = .left }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .new_split = .left }); } fn gtkActionSplitUp( @@ -1076,11 +1058,7 @@ fn gtkActionSplitUp( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .new_split = .up }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .new_split = .right }); } fn gtkActionToggleInspector( @@ -1088,11 +1066,7 @@ fn gtkActionToggleInspector( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .inspector = .toggle }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .inspector = .toggle }); } fn gtkActionCopy( @@ -1100,11 +1074,7 @@ fn gtkActionCopy( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .copy_to_clipboard = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .copy_to_clipboard = {} }); } fn gtkActionPaste( @@ -1112,11 +1082,7 @@ fn gtkActionPaste( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .paste_from_clipboard = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .paste_from_clipboard = {} }); } fn gtkActionReset( @@ -1124,11 +1090,7 @@ fn gtkActionReset( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .reset = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .reset = {} }); } fn gtkActionClear( @@ -1136,11 +1098,7 @@ fn gtkActionClear( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .clear_screen = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .clear_screen = {} }); } fn gtkActionPromptTitle( @@ -1148,11 +1106,7 @@ fn gtkActionPromptTitle( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - const surface = self.actionSurface() orelse return; - _ = surface.performBindingAction(.{ .prompt_surface_title = {} }) catch |err| { - log.warn("error performing binding action error={}", .{err}); - return; - }; + self.performBindingAction(.{ .prompt_surface_title = {} }); } /// Returns the surface to use for an action. From edb861634186936c677d48c79c95937929ad0d02 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 18 Apr 2025 14:25:34 -0700 Subject: [PATCH 100/642] macos: translationMods should be used for consumed mods calculation Fixes #7131 Regression from #7121 Our consumed mods should not include "alt" if `macos-option-as-alt` is set. To do this, we need to calculate our consumed mods based on the actual translation event mods (if available, only available during keyDown). --- macos/Sources/Ghostty/NSEvent+Extension.swift | 12 ++++++++++-- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 13 ++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index 5c13003b3..041a5a199 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -6,7 +6,13 @@ extension NSEvent { /// /// This will not set the "text" or "composing" fields since these can't safely be set /// with the information or lifetimes given. - func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s { + /// + /// The translationMods should be set to the modifiers used for actual character + /// translation if available. + func ghosttyKeyEvent( + _ action: ghostty_input_action_e, + translationMods: NSEvent.ModifierFlags? = nil, + ) -> ghostty_input_key_s { var key_ev: ghostty_input_key_s = .init() key_ev.action = action key_ev.keycode = UInt32(keyCode) @@ -22,7 +28,9 @@ extension NSEvent { // so far: control and command never contribute to the translation of text, // assume everything else did. key_ev.mods = Ghostty.ghosttyMods(modifierFlags) - key_ev.consumed_mods = Ghostty.ghosttyMods(modifierFlags.subtracting([.control, .command])) + key_ev.consumed_mods = Ghostty.ghosttyMods( + (translationMods ?? modifierFlags) + .subtracting([.control, .command])) // Our unshifted codepoint is the codepoint with no modifiers. We // ignore multi-codepoint values. diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 52314f534..e182e210f 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -961,13 +961,19 @@ extension Ghostty { // These never have "composing" set to true because these are the // result of a composition. for text in list { - _ = keyAction(action, event: translationEvent, text: text) + _ = keyAction( + action, + event: event, + translationEvent: translationEvent, + text: text + ) } } else { // We have no accumulated text so this is a normal key event. _ = keyAction( action, - event: translationEvent, + event: event, + translationEvent: translationEvent, text: translationEvent.ghosttyCharacters, composing: markedText.length > 0 ) @@ -1165,12 +1171,13 @@ extension Ghostty { private func keyAction( _ action: ghostty_input_action_e, event: NSEvent, + translationEvent: NSEvent? = nil, text: String? = nil, composing: Bool = false ) -> Bool { guard let surface = self.surface else { return false } - var key_ev = event.ghosttyKeyEvent(action) + var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing if let text { return text.withCString { ptr in From 18d6faf597c60dd646b2bb0278692e87d2054482 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 18 Apr 2025 15:07:27 -0700 Subject: [PATCH 101/642] macOS: translation mods should never have "control" This also lets us get rid of our `C-/` special handling to prevent a system beep. --- macos/Sources/Ghostty/NSEvent+Extension.swift | 2 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 10 ------ src/input/key.zig | 36 ++++++++++++------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index 041a5a199..d3c052f64 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -11,7 +11,7 @@ extension NSEvent { /// translation if available. func ghosttyKeyEvent( _ action: ghostty_input_action_e, - translationMods: NSEvent.ModifierFlags? = nil, + translationMods: NSEvent.ModifierFlags? = nil ) -> ghostty_input_key_s { var key_ev: ghostty_input_key_s = .init() key_ev.action = action diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index e182e210f..574c88044 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1046,16 +1046,6 @@ extension Ghostty { let equivalent: String switch (event.charactersIgnoringModifiers) { - case "/": - // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep - // sound and we don't like the beep sound. - if (!event.modifierFlags.contains(.control) || - !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { - return false - } - - equivalent = "_" - case "\r": // Pass C- through verbatim // (prevent the default context menu equivalent) diff --git a/src/input/key.zig b/src/input/key.zig index f9db4a04a..ec65170f2 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -150,21 +150,25 @@ pub const Mods = packed struct(Mods.Backing) { /// like macos-option-as-alt. The translation mods should be used for /// translation but never sent back in for the key callback. pub fn translation(self: Mods, option_as_alt: config.OptionAsAlt) Mods { - // We currently only process macos-option-as-alt so other - // platforms don't need to do anything. - if (comptime !builtin.target.os.tag.isDarwin()) return self; + var result = self; - // Alt has to be set only on the correct side - switch (option_as_alt) { - .false => return self, - .true => {}, - .left => if (self.sides.alt == .right) return self, - .right => if (self.sides.alt == .left) return self, + // Control is never used for translation. + result.ctrl = false; + + // macos-option-as-alt for darwin + if (comptime builtin.target.os.tag.isDarwin()) alt: { + // Alt has to be set only on the correct side + switch (option_as_alt) { + .false => break :alt, + .true => {}, + .left => if (self.sides.alt == .right) break :alt, + .right => if (self.sides.alt == .left) break :alt, + } + + // Unset alt + result.alt = false; } - // Unset alt - var result = self; - result.alt = false; return result; } @@ -186,6 +190,14 @@ pub const Mods = packed struct(Mods.Backing) { ); } + test "translation removes control" { + const testing = std.testing; + + const mods: Mods = .{ .ctrl = true }; + const result = mods.translation(.true); + try testing.expectEqual(Mods{}, result); + } + test "translation macos-option-as-alt" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; From e4a37dd3836994a9ac7448ab17418b9224f1300b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 18 Apr 2025 15:14:14 -0700 Subject: [PATCH 102/642] macOS: only set unshifted codepoint on keyDown/Up events Other event types trigger an AppKit assertion that doesn't crash the app but logs some nasty stuff. --- macos/Sources/Ghostty/NSEvent+Extension.swift | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index d3c052f64..754bb7a3a 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -35,10 +35,12 @@ extension NSEvent { // Our unshifted codepoint is the codepoint with no modifiers. We // ignore multi-codepoint values. key_ev.unshifted_codepoint = 0 - if let charactersIgnoringModifiers, - let codepoint = charactersIgnoringModifiers.unicodeScalars.first - { - key_ev.unshifted_codepoint = codepoint.value + if type == .keyDown || type == .keyUp { + if let charactersIgnoringModifiers, + let codepoint = charactersIgnoringModifiers.unicodeScalars.first + { + key_ev.unshifted_codepoint = codepoint.value + } } return key_ev From 410761d4e30025690ea92536d31e10537cbeae5c Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Fri, 18 Apr 2025 13:11:46 -0500 Subject: [PATCH 103/642] apprt/gtk: add menu to new tab button to create splits Closes: https://github.com/ghostty-org/ghostty/discussions/6828 Co-authored-by: Leah Amelia Chen Signed-off-by: Tristan Partin --- po/ca_ES.UTF-8.po | 81 ++++++++------ po/com.mitchellh.ghostty.pot | 72 ++++++------ po/de_DE.UTF-8.po | 73 +++++++------ po/es_BO.UTF-8.po | 81 ++++++++------ po/fr_FR.UTF-8.po | 93 +++++++++------- po/id_ID.UTF-8.po | 88 ++++++++------- po/ja_JP.UTF-8.po | 92 +++++++++------- po/mk_MK.UTF-8.po | 93 +++++++++------- po/nb_NO.UTF-8.po | 76 +++++++------ po/nl_NL.UTF-8.po | 87 ++++++++------- po/pl_PL.UTF-8.po | 103 ++++++++++-------- po/pt_BR.UTF-8.po | 84 +++++++------- po/ru_RU.UTF-8.po | 101 +++++++++-------- po/tr_TR.UTF-8.po | 73 +++++++------ po/uk_UA.UTF-8.po | 78 +++++++------ po/zh_CN.UTF-8.po | 73 +++++++------ src/apprt/gtk/Window.zig | 19 +++- src/apprt/gtk/gresource.zig | 1 + .../gtk/ui/1.0/menu-headerbar-split_menu.blp | 25 +++++ 19 files changed, 798 insertions(+), 595 deletions(-) create mode 100644 src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 5cbb7efd5..8a6a359cf 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-20 08:07+0100\n" "Last-Translator: Francesc Arpi \n" "Language-Team: \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"S'han trobat un o més errors de configuració. Si us plau, revisa els errors a " -"continuació i torna a carregar la configuració o ignora aquests errors." +"S'han trobat un o més errors de configuració. Si us plau, revisa els errors " +"a continuació i torna a carregar la configuració o ignora aquests errors." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +56,30 @@ msgstr "Ignora" msgid "Reload Configuration" msgstr "Carrega la configuració" +#: 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 "Divideix cap amunt" + +#: 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 "Divideix cap avall" + +#: 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 "Divideix a l'esquerra" + +#: 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 "Divideix a la dreta" + #: 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,33 +111,13 @@ msgstr "Divideix" msgid "Change Title…" msgstr "Canvia el títol…" -#: 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 "Divideix cap amunt" - -#: 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 "Divideix cap avall" - -#: 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 "Divideix a l'esquerra" - -#: 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 "Divideix a la dreta" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nova pestanya" @@ -150,7 +154,7 @@ msgid "Terminal Inspector" msgstr "Inspector de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Sobre Ghostty" @@ -205,10 +209,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copiat al porta-retalls" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tanca" @@ -245,25 +245,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiat al porta-retalls" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menú principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Mostra les pestanyes obertes" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Divideix" + +#: 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." +"⚠️ Estàs executant una versió de depuració de Ghostty! El rendiment es veurà " +"afectat." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "S'ha tornat a carregar la configuració" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Desenvolupadors de Ghostty" diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index 4bf47da53..f1cd41f94 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,6 +54,30 @@ msgstr "" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -85,33 +109,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:248 msgid "New Tab" msgstr "" @@ -148,7 +152,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "" @@ -197,10 +201,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "" @@ -237,23 +237,31 @@ msgstr "" msgid "The currently running process in this split will be terminated." msgstr "" -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "" -#: src/apprt/gtk/Window.zig:295 +#: 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 "" -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 1de7a7b96..bcfc13b76 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-06 14:57+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" @@ -55,6 +55,30 @@ msgstr "" msgid "Reload Configuration" msgstr "Konfiguration neu laden" +#: 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 "Fenster nach oben teilen" + +#: 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 "Fenster nach unten teilen" + +#: 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 "Fenter nach links teilen" + +#: 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 "Fenster nach rechts teilen" + #: 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,33 +110,13 @@ msgstr "Fenster teilen" msgid "Change Title…" msgstr "Titel bearbeiten…" -#: 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 "Fenster nach oben teilen" - -#: 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 "Fenster nach unten teilen" - -#: 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 "Fenter nach links teilen" - -#: 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 "Fenster nach rechts teilen" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Neuer Tab" @@ -149,7 +153,7 @@ msgid "Terminal Inspector" msgstr "Terminalinspektor" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Über Ghostty" @@ -204,10 +208,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "In die Zwischenablage kopiert" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Schließen" @@ -244,25 +244,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "In die Zwischenablage kopiert" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Hauptmenü" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Offene Tabs einblenden" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Fenster teilen" + +#: 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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Konfiguration wurde neu geladen" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty-Entwickler" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index 339ff54c4..b43550cb1 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Se encontraron uno o más errores de configuración. Por favor revise los errores a continuación, " -"y recargue su configuración o ignore estos errores." +"Se encontraron uno o más errores de configuración. Por favor revise los " +"errores a continuación, y recargue su configuración o ignore estos errores." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +56,30 @@ msgstr "Ignorar" msgid "Reload Configuration" msgstr "Recargar configuración" +#: 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 "Dividir arriba" + +#: 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 "Dividir abajo" + +#: 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 "Dividir a la izquierda" + +#: 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 "Dividir a la derecha" + #: 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,33 +111,13 @@ msgstr "Dividir" msgid "Change Title…" msgstr "Cambiar título…" -#: 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 "Dividir arriba" - -#: 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 "Dividir abajo" - -#: 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 "Dividir a la izquierda" - -#: 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 "Dividir a la derecha" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nueva pestaña" @@ -150,7 +154,7 @@ 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:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Acerca de Ghostty" @@ -205,10 +209,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de la terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copiado al portapapeles" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Cerrar" @@ -245,23 +245,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menú principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Ver pestañas abiertas" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Dividir" + +#: 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." +msgstr "" +"⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Configuración recargada" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Desarrolladores de Ghostty" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index fc5bfd054..cc7d323fd 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-22 09:31+0100\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" @@ -43,8 +43,9 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire les erreurs ci-dessous," -"et recharger votre configuration ou bien ignorer ces erreurs." +"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire " +"les erreurs ci-dessous,et recharger votre configuration ou bien ignorer ces " +"erreurs." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +57,30 @@ msgstr "Ignorer" msgid "Reload Configuration" msgstr "Recharger la configuration" +#: 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 "Panneau en haut" + +#: 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 "Panneau en bas" + +#: 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 "Panneau à gauche" + +#: 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 "Panneau à droite" + #: 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,33 +112,13 @@ msgstr "Créer panneau" msgid "Change Title…" msgstr "Changer le titre…" -#: 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 "Panneau en haut" - -#: 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 "Panneau en bas" - -#: 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 "Panneau à gauche" - -#: 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 "Panneau à droite" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nouvel onglet" @@ -150,7 +155,7 @@ msgid "Terminal Inspector" msgstr "Inspecteur de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "À propos de Ghostty" @@ -168,8 +173,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essaie de lire depuis le presse-papiers." -"Le contenu actuel du presse-papiers est affiché ci-dessous." +"Une application essaie de lire depuis le presse-papiers. Le contenu actuel " +"du presse-papiers est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -186,8 +191,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essaie d'écrire dans le presse-papiers." -"Le contenu actuel du presse-papiers est affiché ci-dessous." +"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 msgid "Warning: Potentially Unsafe Paste" @@ -198,17 +203,13 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Coller ce texte dans le terminal pourrait être dangereux, " -"il semblerait que certaines commandes pourraient être exécutées." +"Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " +"certaines commandes pourraient être exécutées." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspecteur" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copié dans le presse-papiers" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Fermer" @@ -245,24 +246,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copié dans le presse-papiers" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menu principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Voir les onglets ouverts" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Créer panneau" + +#: 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." +"⚠️ Vous utilisez une version de débogage de Ghostty ! Les performances seront " +"dégradées." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Recharger la configuration" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Les développeurs de Ghostty" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index c8b89a89e..8829e3da5 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-20 15:19+0700\n" "Last-Translator: Satrio Bayu Aji \n" "Language-Team: Indonesian \n" @@ -42,8 +42,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di bawah ini, " -"dan muat ulang konfigurasi anda atau abaikan kesalahan ini." +"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di " +"bawah ini, dan muat ulang konfigurasi anda atau abaikan kesalahan ini." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -55,6 +55,30 @@ msgstr "Abaikan" msgid "Reload Configuration" msgstr "Muat ulang konfigurasi" +#: 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 "Belah atas" + +#: 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 "Belah bawah" + +#: 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 "Belah kiri" + +#: 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 "Belah kanan" + #: 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,33 +110,13 @@ msgstr "Belah" msgid "Change Title…" msgstr "Ubah judul…" -#: 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 "Belah atas" - -#: 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 "Belah bawah" - -#: 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 "Belah kiri" - -#: 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 "Belah kanan" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Tab baru" @@ -149,7 +153,7 @@ msgid "Terminal Inspector" msgstr "Inspektur terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Tentang Ghostty" @@ -167,8 +171,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip " -"saat ini ditampilkan di bawah ini." +"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip saat ini " +"ditampilkan di bawah ini." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -185,8 +189,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Aplikasi sedang mencoba menulis ke papan klip. Isi papan klip " -"saat ini ditampilkan di bawah ini." +"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 msgid "Warning: Potentially Unsafe Paste" @@ -204,10 +208,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspektur terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Disalin ke papan klip" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tutup" @@ -244,23 +244,33 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Disalin ke papan klip" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menu utama" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Lihat tab terbuka" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Belah" + +#: 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." +msgstr "" +"⚠️ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Memuat ulang konfigurasi" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Pengembang Ghostty" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index e06ec2fbc..362d58a7f 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-21 00:08+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" @@ -44,8 +44,8 @@ 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 msgid "Ignore" @@ -57,6 +57,30 @@ msgstr "無視" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,33 +112,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:248 msgid "New Tab" msgstr "新しいタブ" @@ -151,7 +155,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "Ghostty について" @@ -169,8 +173,8 @@ 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 @@ -187,8 +191,8 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -199,17 +203,13 @@ 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 "クリップボードにコピーしました" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "閉じる" @@ -246,23 +246,33 @@ msgstr "タブ内のすべてのターミナルセッションが終了します msgid "The currently running process in this split will be terminated." msgstr "分割ウィンドウ内のすべてのプロセスが終了します。" -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "クリップボードにコピーしました" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "メインメニュー" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "開いているすべてのタブを表示" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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 のデバッグビルドを実行しています! パフォーマンスが低下しています。" +msgstr "" +"⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "設定を再読み込みしました" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty 開発者" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 5552cc6e4..fc26a2fbb 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-23 14:17+0100\n" "Last-Translator: Andrej Daskalov \n" "Language-Team: Macedonian\n" @@ -41,7 +41,10 @@ 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 msgid "Ignore" @@ -53,6 +56,30 @@ msgstr "Игнорирај" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -84,33 +111,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:248 msgid "New Tab" msgstr "Ново јазиче" @@ -147,7 +154,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "За Ghostty" @@ -164,7 +171,9 @@ msgstr "Авторизирај пристап до привремена мемо 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 @@ -180,7 +189,9 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -190,16 +201,14 @@ msgstr "Предупредување: Потенцијално небезбед msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи изгледа како да ќе се извршат одредени команди." +msgstr "" +"Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи " +"изгледа како да ќе се извршат одредени команди." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Инспектор на терминал" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Копирано во привремена меморија" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Затвори" @@ -236,23 +245,33 @@ msgstr "Сите сесии во ова јазиче ќе бидат преки msgid "The currently running process in this split will be terminated." msgstr "Процесот кој моментално се извршува во оваа поделба ќе биде прекинат." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Копирано во привремена меморија" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Главно мени" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Прегледај отворени јазичиња" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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! Перформансите ќе бидат намалени." +msgstr "" +"⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Конфигурацијата е одново вчитана" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Развивачи на Ghostty" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index bd7c8876a..844aae864 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -10,6 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-04-14 16:25+0200\n" "Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" @@ -45,8 +46,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgå feilene under, " -"og enten last konfigurasjonen din på nytt eller ignorer disse feilene." +"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgå feilene " +"under, og enten last konfigurasjonen din på nytt eller ignorer disse feilene." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -58,6 +59,30 @@ msgstr "Ignorer" msgid "Reload Configuration" msgstr "Last konfigurasjon på nytt" +#: 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 "Splitt opp" + +#: 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 "Splitt ned" + +#: 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 "Splitt venstre" + +#: 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 "Splitt høyre" + #: 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,33 +114,13 @@ msgstr "Splitt" msgid "Change Title…" msgstr "Endre tittel…" -#: 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 "Splitt opp" - -#: 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 "Splitt ned" - -#: 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 "Splitt venstre" - -#: 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 "Splitt høyre" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Ny fane" @@ -152,7 +157,7 @@ msgid "Terminal Inspector" msgstr "Terminalinspektør" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Om Ghostty" @@ -207,10 +212,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Terminalinspektør" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Kopiert til utklippstavlen" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Lukk" @@ -247,23 +248,32 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Kopiert til utklippstavlen" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Hovedmeny" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Se åpne faner" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Splitt" + +#: 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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Konfigurasjonen ble lastet på nytt" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty-utviklere" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 6ebea478b..9e2b54105 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" "Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " -"en herlaad je configuratie of negeer deze fouten." +"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande " +"fouten en herlaad je configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +56,30 @@ msgstr "Negeer" msgid "Reload Configuration" msgstr "Herlaad configuratie" +#: 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 "Splits naar boven" + +#: 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 "Splits naar beneden" + +#: 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 "Splits naar links" + +#: 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 "Splits naar rechts" + #: 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,33 +111,13 @@ msgstr "Splitsen" msgid "Change Title…" msgstr "Wijzig titel…" -#: 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 "Splits naar boven" - -#: 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 "Splits naar beneden" - -#: 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 "Splits naar links" - -#: 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 "Splits naar rechts" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nieuw tabblad" @@ -150,7 +154,7 @@ msgid "Terminal Inspector" msgstr "Terminal inspecteur" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Over Ghostty" @@ -198,17 +202,13 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat " -"het lijkt op een commando dat uitgevoerd kan worden." +"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " +"lijkt op een commando dat uitgevoerd kan worden." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: terminal inspecteur" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Gekopieerd naar klembord" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Afsluiten" @@ -243,26 +243,37 @@ msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "Alle processen die nu draaien in deze splitsing zullen worden beëindigd." +msgstr "" +"Alle processen die nu draaien in deze splitsing zullen worden beëindigd." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Gekopieerd naar klembord" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Hoofdmenu" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Open tabbladen bekijken" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Splitsen" + +#: 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." +"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan " +"normaal." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "De configuratie is herladen" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty ontwikkelaars" diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index 492326c17..53439b3b1 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-03-18 11:48+0100\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-17 12:15+0100\n" "Last-Translator: Bartosz Sokorski \n" "Language-Team: Polish \n" @@ -45,8 +45,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Znaleziono jeden lub więcej błędów konfiguracji. Sprawdź błędy wylistowane poniżej " -"i przeładuj konfigurację lub zignoruj je." +"Znaleziono jeden lub więcej błędów konfiguracji. Sprawdź błędy wylistowane " +"poniżej i przeładuj konfigurację lub zignoruj je." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -58,6 +58,30 @@ msgstr "Zignoruj" msgid "Reload Configuration" msgstr "Przeładuj konfigurację" +#: 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 "Podziel w górę" + +#: 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 "Podziel w dół" + +#: 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 "Podziel w lewo" + +#: 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 "Podziel w prawo" + #: 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,33 +113,13 @@ msgstr "Podział" msgid "Change Title…" msgstr "Zmień tytuł…" -#: 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 "Podziel w górę" - -#: 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 "Podziel w dół" - -#: 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 "Podziel w lewo" - -#: 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 "Podziel w prawo" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nowa karta" @@ -152,7 +156,7 @@ msgid "Terminal Inspector" msgstr "Inspektor terminala" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:958 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "O Ghostty" @@ -203,27 +207,6 @@ msgstr "" "Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " "spowodować wykonanie komend." -#: src/apprt/gtk/Window.zig:200 -msgid "Main Menu" -msgstr "Menu główne" - -#: src/apprt/gtk/Window.zig:221 -msgid "View Open Tabs" -msgstr "Zobacz otwarte karty" - -#: src/apprt/gtk/Window.zig:295 -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:725 -msgid "Reloaded the configuration" -msgstr "Przeładowano konfigurację" - -#: src/apprt/gtk/Window.zig:939 -msgid "Ghostty Developers" -msgstr "Twórcy Ghostty" - #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Inspektor terminala Ghostty" @@ -264,6 +247,32 @@ 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:1242 +#: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" msgstr "Skopiowano do schowka" + +#: 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 +#, fuzzy +msgid "New Split" +msgstr "Podział" + +#: 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" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index f9fadce66..a18ef4657 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-28 11:04-0300\n" "Last-Translator: Gustavo Peres \n" "Language-Team: Brazilian Portuguese \n" "Language-Team: Russian \n" @@ -44,9 +44,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 msgid "Ignore" @@ -58,6 +58,30 @@ msgstr "Игнорировать" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -89,33 +113,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:248 msgid "New Tab" msgstr "Новая вкладка" @@ -152,7 +156,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "О Ghostty" @@ -169,9 +173,9 @@ msgstr "Разрешить доступ к буферу обмена" 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 @@ -187,9 +191,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 msgid "Warning: Potentially Unsafe Paste" @@ -199,18 +202,14 @@ msgstr "Внимание! Вставляемые данные могут нан msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "" -"Вставка этого текста в терминал может быть опасной. Это выглядит " -"как команды, которые могут быть исполнены." +msgstr "" +"Вставка этого текста в терминал может быть опасной. Это выглядит как " +"команды, которые могут быть исполнены." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: инспектор терминала" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Скопировано в буфер обмена" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрыть" @@ -247,24 +246,34 @@ msgstr "Все сессии терминала в этой вкладке буд msgid "The currently running process in this split will be terminated." msgstr "Процесс, работающий в этой сплит-области, будет остановлен." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопировано в буфер обмена" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Главное меню" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Просмотреть открытые вкладки" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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! Это может влиять на производительность." +msgstr "" +"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на " +"производительность." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Конфигурация была обновлена" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Разработчики Ghostty" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index cee17a6a1..d1103309c 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-24 22:01+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" @@ -57,6 +57,30 @@ msgstr "Yok Say" msgid "Reload Configuration" msgstr "Yapılandırmayı Yeniden Yükle" +#: 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 "Yukarı Doğru Böl" + +#: 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 "Aşağı Doğru Böl" + +#: 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 "Sola Doğru Böl" + +#: 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 "Sağa Doğru Böl" + #: 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,33 +112,13 @@ msgstr "Böl" msgid "Change Title…" msgstr "Başlığı Değiştir…" -#: 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 "Yukarı Doğru Böl" - -#: 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 "Aşağı Doğru Böl" - -#: 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 "Sola Doğru Böl" - -#: 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 "Sağa Doğru Böl" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Yeni Sekme" @@ -151,7 +155,7 @@ 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:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Ghostty Hakkında" @@ -206,10 +210,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Uçbirim Denetçisi" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Panoya kopyalandı" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Kapat" @@ -246,25 +246,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Ana Menü" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Açık Sekmeleri Görüntüle" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Böl" + +#: 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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Yapılandırma yeniden yüklendi" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty Geliştiricileri" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 662117071..48807e782 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-03-16 20:16+0200\n" "Last-Translator: Danylo Zalizchuk \n" "Language-Team: Ukrainian \n" @@ -44,8 +44,9 @@ 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 msgid "Ignore" @@ -57,6 +58,30 @@ msgstr "Ігнорувати" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,33 +113,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:248 msgid "New Tab" msgstr "Нова вкладка" @@ -151,7 +156,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "Про Ghostty" @@ -206,10 +211,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Інспектор терміналу" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Скопійовано в буфер обміну" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрити" @@ -247,24 +248,33 @@ msgid "The currently running process in this split will be terminated." msgstr "" "Поточний процес, що виконується в цій розділеній панелі, буде завершено." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопійовано в буфер обміну" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Головне меню" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Переглянути відкриті вкладки" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Конфігурацію перезавантажено" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Розробники Ghostty" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index cdb4c3873..9806c362a 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -55,6 +55,30 @@ msgstr "忽略" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -86,33 +110,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:248 msgid "New Tab" msgstr "新建标签页" @@ -149,7 +153,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "关于 Ghostty" @@ -198,10 +202,6 @@ msgstr "将以下内容粘贴至终端内将可能执行有害命令。" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty 终端调试器" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "已复制至剪贴板" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "关闭" @@ -238,23 +238,32 @@ msgstr "标签页内所有运行中的进程将被终止。" msgid "The currently running process in this split will be terminated." msgstr "分屏内正在运行中的进程将被终止。" -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "已复制至剪贴板" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "主菜单" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "浏览标签页" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "已重新加载配置" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty 开发团队" diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 129c149e7..fded6078a 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -25,6 +25,7 @@ const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); +const Builder = @import("Builder.zig"); const Color = configpkg.Config.Color; const Surface = @import("Surface.zig"); const Menu = @import("menu.zig").Menu; @@ -242,12 +243,19 @@ pub fn init(self: *Window, app: *App) !void { } { - const btn = gtk.Button.newFromIconName("tab-new-symbolic"); + const btn = adw.SplitButton.new(); + btn.setIconName("tab-new-symbolic"); btn.as(gtk.Widget).setTooltipText(i18n._("New Tab")); - _ = gtk.Button.signals.clicked.connect( + btn.setDropdownTooltip(i18n._("New Split")); + + var builder = Builder.init("menu-headerbar-split_menu", 1, 0, .blp); + defer builder.deinit(); + btn.setMenuModel(builder.getObject(gio.MenuModel, "menu")); + + _ = adw.SplitButton.signals.clicked.connect( btn, *Window, - gtkTabNewClick, + adwNewTabClick, self, .{}, ); @@ -824,6 +832,11 @@ fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { self.performBindingAction(.{ .new_tab = {} }); } +/// Create a new surface (tab or split). +fn adwNewTabClick(_: *adw.SplitButton, self: *Window) callconv(.c) void { + self.performBindingAction(.{ .new_tab = {} }); +} + /// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick /// because we need to return an AdwTabPage from this function. fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage { diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 64067c199..7ced9fc45 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -75,6 +75,7 @@ pub const VersionedBlueprint = struct { pub const blueprint_files = [_]VersionedBlueprint{ .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" }, .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, + .{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" }, .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" }, .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" }, diff --git a/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp b/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp new file mode 100644 index 000000000..90de02845 --- /dev/null +++ b/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp @@ -0,0 +1,25 @@ +using Gtk 4.0; + +menu menu { + section { + item { + label: _("Split Up"); + action: "win.split-up"; + } + + item { + label: _("Split Down"); + action: "win.split-down"; + } + + item { + label: _("Split Left"); + action: "win.split-left"; + } + + item { + label: _("Split Right"); + action: "win.split-right"; + } + } +} From 31b2ac4b7933a1002473a870c3708ae6501f1279 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 06:43:19 -0700 Subject: [PATCH 104/642] macOS: Do not send control characters as UTF8 keyboard text Fixes a regression where `ctrl+enter` was not encoding properly since our input stack changes. --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 7 ++++++- src/input/KeyEncoder.zig | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 574c88044..72a324525 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1169,7 +1169,12 @@ extension Ghostty { var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags) key_ev.composing = composing - if let text { + + // For text, we only encode UTF8 if we don't have a single control + // character. Control characters are encoded by Ghostty itself. + // Without this, `ctrl+enter` does the wrong thing. + if let text, text.count > 0, + let codepoint = text.utf8.first, codepoint >= 0x20 { return text.withCString { ptr in key_ev.text = ptr return ghostty_surface_key(surface, key_ev) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 4aaae25e9..e79856a94 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -35,7 +35,7 @@ pub fn encode( self: *const KeyEncoder, buf: []u8, ) ![]const u8 { - // log.warn("KEYENCODER self={}", .{self.j}); + // log.warn("KEYENCODER self={}", .{self.*}); if (self.kitty_flags.int() != 0) return try self.kitty(buf); return try self.legacy(buf); } From 9beaed45f806db1fd573e47f3f44fa35659b3b57 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 07:05:38 -0700 Subject: [PATCH 105/642] Revert "apprt/gtk: add menu to new tab button to create splits (#7127)" This reverts commit 14134d61fb4b1bbf4ce80bb9b3ed849908bf9344, reversing changes made to 6a876ef8ec3e2aeb3d15df0dfb0e07677e49ff03. This causes translation failures, this should be reintroduced when the CI check passes. --- po/ca_ES.UTF-8.po | 81 ++++++-------- po/com.mitchellh.ghostty.pot | 72 ++++++------ po/de_DE.UTF-8.po | 73 ++++++------- po/es_BO.UTF-8.po | 81 ++++++-------- po/fr_FR.UTF-8.po | 93 +++++++--------- po/id_ID.UTF-8.po | 88 +++++++-------- po/ja_JP.UTF-8.po | 92 +++++++--------- po/mk_MK.UTF-8.po | 93 +++++++--------- po/nb_NO.UTF-8.po | 76 ++++++------- po/nl_NL.UTF-8.po | 87 +++++++-------- po/pl_PL.UTF-8.po | 103 ++++++++---------- po/pt_BR.UTF-8.po | 84 +++++++------- po/ru_RU.UTF-8.po | 101 ++++++++--------- po/tr_TR.UTF-8.po | 73 ++++++------- po/uk_UA.UTF-8.po | 78 ++++++------- po/zh_CN.UTF-8.po | 73 ++++++------- src/apprt/gtk/Window.zig | 19 +--- src/apprt/gtk/gresource.zig | 1 - .../gtk/ui/1.0/menu-headerbar-split_menu.blp | 25 ----- 19 files changed, 595 insertions(+), 798 deletions(-) delete mode 100644 src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 8a6a359cf..5cbb7efd5 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-20 08:07+0100\n" "Last-Translator: Francesc Arpi \n" "Language-Team: \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"S'han trobat un o més errors de configuració. Si us plau, revisa els errors " -"a continuació i torna a carregar la configuració o ignora aquests errors." +"S'han trobat un o més errors de configuració. Si us plau, revisa els errors a " +"continuació i torna a carregar la configuració o ignora aquests errors." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,30 +56,6 @@ msgstr "Ignora" msgid "Reload Configuration" msgstr "Carrega la configuració" -#: 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 "Divideix cap amunt" - -#: 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 "Divideix cap avall" - -#: 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 "Divideix a l'esquerra" - -#: 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 "Divideix a la dreta" - #: 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" @@ -111,13 +87,33 @@ msgstr "Divideix" msgid "Change Title…" msgstr "Canvia el títol…" +#: 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 "Divideix cap amunt" + +#: 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 "Divideix cap avall" + +#: 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 "Divideix a l'esquerra" + +#: 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 "Divideix a la dreta" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Nova pestanya" @@ -154,7 +150,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Sobre Ghostty" @@ -209,6 +205,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de terminal" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiat al porta-retalls" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tanca" @@ -245,34 +245,25 @@ 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 -msgid "Copied to clipboard" -msgstr "Copiat al porta-retalls" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Menú principal" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Mostra les pestanyes obertes" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Divideix" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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." +"⚠️ Estàs executant una versió de depuració de Ghostty! El rendiment es " +"veurà afectat." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "S'ha tornat a carregar la configuració" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Desenvolupadors de Ghostty" diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index f1cd41f94..4bf47da53 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-04-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,30 +54,6 @@ msgstr "" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -109,13 +85,33 @@ 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:248 +#: src/apprt/gtk/Window.zig:246 msgid "New Tab" msgstr "" @@ -152,7 +148,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "" @@ -201,6 +197,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "" @@ -237,31 +237,23 @@ msgstr "" msgid "The currently running process in this split will be terminated." msgstr "" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "" -#: src/apprt/gtk/Window.zig:249 -msgid "New Split" -msgstr "" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index bcfc13b76..1de7a7b96 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-03-06 14:57+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" @@ -55,30 +55,6 @@ msgstr "" msgid "Reload Configuration" msgstr "Konfiguration neu laden" -#: 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 "Fenster nach oben teilen" - -#: 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 "Fenster nach unten teilen" - -#: 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 "Fenter nach links teilen" - -#: 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 "Fenster nach rechts teilen" - #: 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" @@ -110,13 +86,33 @@ msgstr "Fenster teilen" msgid "Change Title…" msgstr "Titel bearbeiten…" +#: 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 "Fenster nach oben teilen" + +#: 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 "Fenster nach unten teilen" + +#: 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 "Fenter nach links teilen" + +#: 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 "Fenster nach rechts teilen" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Neuer Tab" @@ -153,7 +149,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Über Ghostty" @@ -208,6 +204,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "In die Zwischenablage kopiert" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Schließen" @@ -244,34 +244,25 @@ 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 -msgid "Copied to clipboard" -msgstr "In die Zwischenablage kopiert" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Hauptmenü" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Offene Tabs einblenden" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Fenster teilen" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Konfiguration wurde neu geladen" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Ghostty-Entwickler" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index b43550cb1..339ff54c4 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Se encontraron uno o más errores de configuración. Por favor revise los " -"errores a continuación, y recargue su configuración o ignore estos errores." +"Se encontraron uno o más errores de configuración. Por favor revise los errores a continuación, " +"y recargue su configuración o ignore estos errores." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,30 +56,6 @@ msgstr "Ignorar" msgid "Reload Configuration" msgstr "Recargar configuración" -#: 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 "Dividir arriba" - -#: 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 "Dividir abajo" - -#: 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 "Dividir a la izquierda" - -#: 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 "Dividir a la derecha" - #: 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" @@ -111,13 +87,33 @@ msgstr "Dividir" msgid "Change Title…" msgstr "Cambiar título…" +#: 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 "Dividir arriba" + +#: 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 "Dividir abajo" + +#: 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 "Dividir a la izquierda" + +#: 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 "Dividir a la derecha" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Nueva pestaña" @@ -154,7 +150,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Acerca de Ghostty" @@ -209,6 +205,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de la terminal" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Cerrar" @@ -245,34 +245,23 @@ 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 -msgid "Copied to clipboard" -msgstr "Copiado al portapapeles" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Menú principal" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Ver pestañas abiertas" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Dividir" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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." +msgstr "⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no será óptimo." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Configuración recargada" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Desarrolladores de Ghostty" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index cc7d323fd..fc5bfd054 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-22 09:31+0100\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" @@ -43,9 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire " -"les erreurs ci-dessous,et recharger votre configuration ou bien ignorer ces " -"erreurs." +"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire les erreurs ci-dessous," +"et recharger votre configuration ou bien ignorer ces erreurs." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -57,30 +56,6 @@ msgstr "Ignorer" msgid "Reload Configuration" msgstr "Recharger la configuration" -#: 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 "Panneau en haut" - -#: 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 "Panneau en bas" - -#: 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 "Panneau à gauche" - -#: 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 "Panneau à droite" - #: 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" @@ -112,13 +87,33 @@ msgstr "Créer panneau" msgid "Change Title…" msgstr "Changer le titre…" +#: 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 "Panneau en haut" + +#: 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 "Panneau en bas" + +#: 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 "Panneau à gauche" + +#: 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 "Panneau à droite" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Nouvel onglet" @@ -155,7 +150,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "À propos de Ghostty" @@ -173,8 +168,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essaie de lire depuis le presse-papiers. Le contenu actuel " -"du presse-papiers est affiché ci-dessous." +"Une application essaie de lire depuis le presse-papiers." +"Le contenu actuel du presse-papiers est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -191,8 +186,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essaie d'écrire dans le presse-papiers.Le contenu actuel du " -"presse-papiers est affiché ci-dessous." +"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 msgid "Warning: Potentially Unsafe Paste" @@ -203,13 +198,17 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " -"certaines commandes pourraient être exécutées." +"Coller ce texte dans le terminal pourrait être dangereux, " +"il semblerait que certaines commandes pourraient être exécutées." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspecteur" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copié dans le presse-papiers" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Fermer" @@ -246,34 +245,24 @@ 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 -msgid "Copied to clipboard" -msgstr "Copié dans le presse-papiers" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Menu principal" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Voir les onglets ouverts" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Créer panneau" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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." +"⚠️ Vous utilisez une version de débogage de Ghostty ! Les performances seront dégradées." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Recharger la configuration" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Les développeurs de Ghostty" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index 8829e3da5..c8b89a89e 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-20 15:19+0700\n" "Last-Translator: Satrio Bayu Aji \n" "Language-Team: Indonesian \n" @@ -42,8 +42,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di " -"bawah ini, dan muat ulang konfigurasi anda atau abaikan kesalahan ini." +"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di bawah ini, " +"dan muat ulang konfigurasi anda atau abaikan kesalahan ini." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -55,30 +55,6 @@ msgstr "Abaikan" msgid "Reload Configuration" msgstr "Muat ulang konfigurasi" -#: 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 "Belah atas" - -#: 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 "Belah bawah" - -#: 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 "Belah kiri" - -#: 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 "Belah kanan" - #: 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" @@ -110,13 +86,33 @@ msgstr "Belah" msgid "Change Title…" msgstr "Ubah judul…" +#: 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 "Belah atas" + +#: 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 "Belah bawah" + +#: 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 "Belah kiri" + +#: 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 "Belah kanan" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Tab baru" @@ -153,7 +149,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Tentang Ghostty" @@ -171,8 +167,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip saat ini " -"ditampilkan di bawah ini." +"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip " +"saat ini ditampilkan di bawah ini." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -189,8 +185,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Aplikasi sedang mencoba menulis ke papan klip. Isi papan klip saat ini " -"ditampilkan di bawah ini." +"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 msgid "Warning: Potentially Unsafe Paste" @@ -208,6 +204,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspektur terminal" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Disalin ke papan klip" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tutup" @@ -244,33 +244,23 @@ 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 -msgid "Copied to clipboard" -msgstr "Disalin ke papan klip" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Menu utama" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Lihat tab terbuka" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Belah" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." +msgstr "⚠️ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Memuat ulang konfigurasi" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Pengembang Ghostty" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index 362d58a7f..e06ec2fbc 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-21 00:08+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" @@ -44,8 +44,8 @@ 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 msgid "Ignore" @@ -57,30 +57,6 @@ msgstr "無視" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -112,13 +88,33 @@ 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:248 +#: src/apprt/gtk/Window.zig:246 msgid "New Tab" msgstr "新しいタブ" @@ -155,7 +151,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Ghostty について" @@ -173,8 +169,8 @@ 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 @@ -191,8 +187,8 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -203,13 +199,17 @@ 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 "クリップボードにコピーしました" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "閉じる" @@ -246,33 +246,23 @@ msgstr "タブ内のすべてのターミナルセッションが終了します msgid "The currently running process in this split will be terminated." msgstr "分割ウィンドウ内のすべてのプロセスが終了します。" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "クリップボードにコピーしました" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "メインメニュー" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "開いているすべてのタブを表示" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "分割" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" +msgstr "⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "設定を再読み込みしました" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Ghostty 開発者" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index fc26a2fbb..5552cc6e4 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-23 14:17+0100\n" "Last-Translator: Andrej Daskalov \n" "Language-Team: Macedonian\n" @@ -41,10 +41,7 @@ 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 msgid "Ignore" @@ -56,30 +53,6 @@ msgstr "Игнорирај" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -111,13 +84,33 @@ 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:248 +#: src/apprt/gtk/Window.zig:246 msgid "New Tab" msgstr "Ново јазиче" @@ -154,7 +147,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "За Ghostty" @@ -171,9 +164,7 @@ msgstr "Авторизирај пристап до привремена мемо 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 @@ -189,9 +180,7 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -201,14 +190,16 @@ msgstr "Предупредување: Потенцијално небезбед msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "" -"Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи " -"изгледа како да ќе се извршат одредени команди." +msgstr "Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи изгледа како да ќе се извршат одредени команди." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Инспектор на терминал" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Копирано во привремена меморија" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Затвори" @@ -245,33 +236,23 @@ msgstr "Сите сесии во ова јазиче ќе бидат преки msgid "The currently running process in this split will be terminated." msgstr "Процесот кој моментално се извршува во оваа поделба ќе биде прекинат." -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Копирано во привремена меморија" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Главно мени" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Прегледај отворени јазичиња" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Подели" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." +msgstr "⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Конфигурацијата е одново вчитана" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Развивачи на Ghostty" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 844aae864..bd7c8876a 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -10,7 +10,6 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" -"POT-Creation-Date: 2025-04-19 01:03-0500\n" "PO-Revision-Date: 2025-04-14 16:25+0200\n" "Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" @@ -46,8 +45,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgå feilene " -"under, og enten last konfigurasjonen din på nytt eller ignorer disse feilene." +"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgå feilene under, " +"og enten last konfigurasjonen din på nytt eller ignorer disse feilene." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -59,30 +58,6 @@ msgstr "Ignorer" msgid "Reload Configuration" msgstr "Last konfigurasjon på nytt" -#: 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 "Splitt opp" - -#: 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 "Splitt ned" - -#: 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 "Splitt venstre" - -#: 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 "Splitt høyre" - #: 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" @@ -114,13 +89,33 @@ msgstr "Splitt" msgid "Change Title…" msgstr "Endre tittel…" +#: 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 "Splitt opp" + +#: 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 "Splitt ned" + +#: 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 "Splitt venstre" + +#: 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 "Splitt høyre" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Ny fane" @@ -157,7 +152,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Om Ghostty" @@ -212,6 +207,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Terminalinspektør" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Kopiert til utklippstavlen" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Lukk" @@ -248,32 +247,23 @@ 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 -msgid "Copied to clipboard" -msgstr "Kopiert til utklippstavlen" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Hovedmeny" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Se åpne faner" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Splitt" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Konfigurasjonen ble lastet på nytt" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Ghostty-utviklere" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 9e2b54105..6ebea478b 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:28-0700\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" "Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande " -"fouten en herlaad je configuratie of negeer deze fouten." +"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " +"en herlaad je configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,30 +56,6 @@ msgstr "Negeer" msgid "Reload Configuration" msgstr "Herlaad configuratie" -#: 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 "Splits naar boven" - -#: 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 "Splits naar beneden" - -#: 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 "Splits naar links" - -#: 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 "Splits naar rechts" - #: 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" @@ -111,13 +87,33 @@ msgstr "Splitsen" msgid "Change Title…" msgstr "Wijzig titel…" +#: 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 "Splits naar boven" + +#: 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 "Splits naar beneden" + +#: 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 "Splits naar links" + +#: 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 "Splits naar rechts" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Nieuw tabblad" @@ -154,7 +150,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Over Ghostty" @@ -202,13 +198,17 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " -"lijkt op een commando dat uitgevoerd kan worden." +"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat " +"het lijkt op een commando dat uitgevoerd kan worden." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: terminal inspecteur" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Gekopieerd naar klembord" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Afsluiten" @@ -243,37 +243,26 @@ msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "" -"Alle processen die nu draaien in deze splitsing zullen worden beëindigd." +msgstr "Alle processen die nu draaien in deze splitsing zullen worden beëindigd." -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Gekopieerd naar klembord" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Hoofdmenu" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Open tabbladen bekijken" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Splitsen" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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." +"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan normaal." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "De configuratie is herladen" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Ghostty ontwikkelaars" diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index 53439b3b1..492326c17 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-18 11:48+0100\n" "PO-Revision-Date: 2025-03-17 12:15+0100\n" "Last-Translator: Bartosz Sokorski \n" "Language-Team: Polish \n" @@ -45,8 +45,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Znaleziono jeden lub więcej błędów konfiguracji. Sprawdź błędy wylistowane " -"poniżej i przeładuj konfigurację lub zignoruj je." +"Znaleziono jeden lub więcej błędów konfiguracji. Sprawdź błędy wylistowane poniżej " +"i przeładuj konfigurację lub zignoruj je." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -58,30 +58,6 @@ msgstr "Zignoruj" msgid "Reload Configuration" msgstr "Przeładuj konfigurację" -#: 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 "Podziel w górę" - -#: 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 "Podziel w dół" - -#: 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 "Podziel w lewo" - -#: 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 "Podziel w prawo" - #: 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" @@ -113,13 +89,33 @@ msgstr "Podział" msgid "Change Title…" msgstr "Zmień tytuł…" +#: 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 "Podziel w górę" + +#: 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 "Podziel w dół" + +#: 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 "Podziel w lewo" + +#: 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 "Podziel w prawo" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Nowa karta" @@ -156,7 +152,7 @@ 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/Window.zig:958 msgid "About Ghostty" msgstr "O Ghostty" @@ -207,6 +203,27 @@ msgstr "" "Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " "spowodować wykonanie komend." +#: src/apprt/gtk/Window.zig:200 +msgid "Main Menu" +msgstr "Menu główne" + +#: src/apprt/gtk/Window.zig:221 +msgid "View Open Tabs" +msgstr "Zobacz otwarte karty" + +#: src/apprt/gtk/Window.zig:295 +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:725 +msgid "Reloaded the configuration" +msgstr "Przeładowano konfigurację" + +#: src/apprt/gtk/Window.zig:939 +msgid "Ghostty Developers" +msgstr "Twórcy Ghostty" + #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Inspektor terminala Ghostty" @@ -247,32 +264,6 @@ 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:1242 msgid "Copied to clipboard" msgstr "Skopiowano do schowka" - -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Podział" - -#: 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" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index a18ef4657..f9fadce66 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-03-28 11:04-0300\n" "Last-Translator: Gustavo Peres \n" "Language-Team: Brazilian Portuguese \n" "Language-Team: Russian \n" @@ -44,9 +44,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 msgid "Ignore" @@ -58,30 +58,6 @@ msgstr "Игнорировать" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -113,13 +89,33 @@ 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:248 +#: src/apprt/gtk/Window.zig:246 msgid "New Tab" msgstr "Новая вкладка" @@ -156,7 +152,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "О Ghostty" @@ -173,9 +169,9 @@ msgstr "Разрешить доступ к буферу обмена" 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 @@ -191,8 +187,9 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -202,14 +199,18 @@ msgstr "Внимание! Вставляемые данные могут нан msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "" -"Вставка этого текста в терминал может быть опасной. Это выглядит как " -"команды, которые могут быть исполнены." +msgstr "" +"Вставка этого текста в терминал может быть опасной. Это выглядит " +"как команды, которые могут быть исполнены." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: инспектор терминала" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопировано в буфер обмена" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрыть" @@ -246,34 +247,24 @@ msgstr "Все сессии терминала в этой вкладке буд msgid "The currently running process in this split will be terminated." msgstr "Процесс, работающий в этой сплит-области, будет остановлен." -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Скопировано в буфер обмена" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Главное меню" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Просмотреть открытые вкладки" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Сплит" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "" -"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на " -"производительность." +msgstr "" +"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на производительность." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Конфигурация была обновлена" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Разработчики Ghostty" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index d1103309c..cee17a6a1 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-03-24 22:01+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" @@ -57,30 +57,6 @@ msgstr "Yok Say" msgid "Reload Configuration" msgstr "Yapılandırmayı Yeniden Yükle" -#: 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 "Yukarı Doğru Böl" - -#: 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 "Aşağı Doğru Böl" - -#: 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 "Sola Doğru Böl" - -#: 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 "Sağa Doğru Böl" - #: 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" @@ -112,13 +88,33 @@ msgstr "Böl" msgid "Change Title…" msgstr "Başlığı Değiştir…" +#: 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 "Yukarı Doğru Böl" + +#: 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 "Aşağı Doğru Böl" + +#: 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 "Sola Doğru Böl" + +#: 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 "Sağa Doğru Böl" + #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 msgid "New Tab" msgstr "Yeni Sekme" @@ -155,7 +151,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Ghostty Hakkında" @@ -210,6 +206,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Uçbirim Denetçisi" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Kapat" @@ -246,34 +246,25 @@ 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 -msgid "Copied to clipboard" -msgstr "Panoya kopyalandı" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Ana Menü" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Açık Sekmeleri Görüntüle" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Böl" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 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 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Yapılandırma yeniden yüklendi" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Ghostty Geliştiricileri" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 48807e782..662117071 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-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-03-16 20:16+0200\n" "Last-Translator: Danylo Zalizchuk \n" "Language-Team: Ukrainian \n" @@ -44,9 +44,8 @@ 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 msgid "Ignore" @@ -58,30 +57,6 @@ msgstr "Ігнорувати" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -113,13 +88,33 @@ 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:248 +#: src/apprt/gtk/Window.zig:246 msgid "New Tab" msgstr "Нова вкладка" @@ -156,7 +151,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "Про Ghostty" @@ -211,6 +206,10 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Інспектор терміналу" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопійовано в буфер обміну" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрити" @@ -248,33 +247,24 @@ msgid "The currently running process in this split will be terminated." msgstr "" "Поточний процес, що виконується в цій розділеній панелі, буде завершено." -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Скопійовано в буфер обміну" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "Головне меню" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "Переглянути відкриті вкладки" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "Розділена панель" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" "⚠️ Ви використовуєте відладочну збірку Ghostty! Продуктивність буде погіршено." -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "Конфігурацію перезавантажено" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Розробники Ghostty" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 9806c362a..cdb4c3873 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-04-19 01:03-0500\n" +"POT-Creation-Date: 2025-03-19 08:54-0700\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -55,30 +55,6 @@ msgstr "忽略" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -110,13 +86,33 @@ 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:248 +#: src/apprt/gtk/Window.zig:246 msgid "New Tab" msgstr "新建标签页" @@ -153,7 +149,7 @@ 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/Window.zig:960 msgid "About Ghostty" msgstr "关于 Ghostty" @@ -202,6 +198,10 @@ msgstr "将以下内容粘贴至终端内将可能执行有害命令。" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty 终端调试器" +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "已复制至剪贴板" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "关闭" @@ -238,32 +238,23 @@ msgstr "标签页内所有运行中的进程将被终止。" msgid "The currently running process in this split will be terminated." msgstr "分屏内正在运行中的进程将被终止。" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "已复制至剪贴板" - -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:200 msgid "Main Menu" msgstr "主菜单" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:221 msgid "View Open Tabs" msgstr "浏览标签页" -#: src/apprt/gtk/Window.zig:249 -#, fuzzy -msgid "New Split" -msgstr "分屏" - -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:295 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "⚠️ Ghostty 正在以调试模式运行!性能将大打折扣。" -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:725 msgid "Reloaded the configuration" msgstr "已重新加载配置" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:941 msgid "Ghostty Developers" msgstr "Ghostty 开发团队" diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index fded6078a..129c149e7 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -25,7 +25,6 @@ const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); -const Builder = @import("Builder.zig"); const Color = configpkg.Config.Color; const Surface = @import("Surface.zig"); const Menu = @import("menu.zig").Menu; @@ -243,19 +242,12 @@ pub fn init(self: *Window, app: *App) !void { } { - const btn = adw.SplitButton.new(); - btn.setIconName("tab-new-symbolic"); + const btn = gtk.Button.newFromIconName("tab-new-symbolic"); btn.as(gtk.Widget).setTooltipText(i18n._("New Tab")); - btn.setDropdownTooltip(i18n._("New Split")); - - var builder = Builder.init("menu-headerbar-split_menu", 1, 0, .blp); - defer builder.deinit(); - btn.setMenuModel(builder.getObject(gio.MenuModel, "menu")); - - _ = adw.SplitButton.signals.clicked.connect( + _ = gtk.Button.signals.clicked.connect( btn, *Window, - adwNewTabClick, + gtkTabNewClick, self, .{}, ); @@ -832,11 +824,6 @@ fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { self.performBindingAction(.{ .new_tab = {} }); } -/// Create a new surface (tab or split). -fn adwNewTabClick(_: *adw.SplitButton, self: *Window) callconv(.c) void { - self.performBindingAction(.{ .new_tab = {} }); -} - /// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick /// because we need to return an AdwTabPage from this function. fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage { diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 7ced9fc45..64067c199 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -75,7 +75,6 @@ pub const VersionedBlueprint = struct { pub const blueprint_files = [_]VersionedBlueprint{ .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" }, .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, - .{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" }, .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" }, .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" }, diff --git a/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp b/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp deleted file mode 100644 index 90de02845..000000000 --- a/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp +++ /dev/null @@ -1,25 +0,0 @@ -using Gtk 4.0; - -menu menu { - section { - item { - label: _("Split Up"); - action: "win.split-up"; - } - - item { - label: _("Split Down"); - action: "win.split-down"; - } - - item { - label: _("Split Left"); - action: "win.split-left"; - } - - item { - label: _("Split Right"); - action: "win.split-right"; - } - } -} From b05826ac9d747d5de78183c1726c203636bddfe8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 14:16:14 -0700 Subject: [PATCH 106/642] macOS: use KeyboardShortcut rather than homegrown KeyEquivalent This replaces the use of our custom `Ghostty.KeyEquivalent` with the SwiftUI `KeyboardShortcut` type. This is a more standard way to represent keyboard shortcuts and lets us more tightly integrate with SwiftUI/AppKit when necessary over our custom type. Note that not all Ghostty triggers can be represented as KeyboardShortcut values because macOS itself does not support binding keys such as function keys (e.g. F1-F12) to KeyboardShortcuts. This isn't an issue since all input also passes through a lower level libghostty API which can handle all key events, we just can't show these keyboard shortcuts on things like the menu bar. This was already true before this commit. --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++ macos/Sources/App/macOS/AppDelegate.swift | 6 +- .../Terminal/TerminalController.swift | 2 +- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- macos/Sources/Ghostty/Ghostty.Config.swift | 4 +- macos/Sources/Ghostty/Ghostty.Input.swift | 115 ++++++------------ .../Sources/Ghostty/SurfaceView_AppKit.swift | 4 +- .../Helpers/EventModifiers+Extension.swift | 27 ++++ .../Helpers/KeyboardShortcut+Extension.swift | 45 +++++++ 9 files changed, 126 insertions(+), 87 deletions(-) create mode 100644 macos/Sources/Helpers/EventModifiers+Extension.swift create mode 100644 macos/Sources/Helpers/KeyboardShortcut+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index b69541504..5d02ba12b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; }; A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; + A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */; }; + A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */; }; A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; }; A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; @@ -138,6 +140,8 @@ A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = ""; }; A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventModifiers+Extension.swift"; sourceTree = ""; }; + A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyboardShortcut+Extension.swift"; sourceTree = ""; }; A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = ""; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; @@ -288,8 +292,10 @@ A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, + A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, + A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, @@ -667,6 +673,7 @@ A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, + A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, A59630972AEE163600D64628 /* HostingWindow.swift in Sources */, @@ -691,6 +698,7 @@ A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, + A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 9d866d734..94626f808 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -419,15 +419,15 @@ class AppDelegate: NSObject, /// action string used for the Ghostty configuration. private func syncMenuShortcut(_ config: Ghostty.Config, action: String, menuItem: NSMenuItem?) { guard let menu = menuItem else { return } - guard let equiv = config.keyEquivalent(for: action) else { + guard let shortcut = config.keyboardShortcut(for: action) else { // No shortcut, clear the menu item menu.keyEquivalent = "" menu.keyEquivalentModifierMask = [] return } - menu.keyEquivalent = equiv.key - menu.keyEquivalentModifierMask = equiv.modifiers + menu.keyEquivalent = shortcut.key.character.description + menu.keyEquivalentModifierMask = .init(swiftUIFlags: shortcut.modifiers) } private func focusedSurface() -> ghostty_surface_t? { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f54eb6539..f384b97ed 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -192,7 +192,7 @@ class TerminalController: BaseTerminalController { } let action = "goto_tab:\(tab)" - if let equiv = ghostty.config.keyEquivalent(for: action) { + if let equiv = ghostty.config.keyboardShortcut(for: action) { window.keyEquivalent = "\(equiv)" } else { window.keyEquivalent = "" diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index dfd066870..d7fd0c777 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -1306,7 +1306,7 @@ extension Ghostty { name: Notification.didContinueKeySequence, object: surfaceView, userInfo: [ - Notification.KeySequenceKey: keyEquivalent(for: v.trigger) as Any + Notification.KeySequenceKey: keyboardShortcut(for: v.trigger) as Any ] ) } else { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index d146477dc..d7be4eb5b 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -102,11 +102,11 @@ extension Ghostty { /// configuration would be "quit" action. /// /// Returns nil if there is no key equivalent for the given action. - func keyEquivalent(for action: String) -> KeyEquivalent? { + func keyboardShortcut(for action: String) -> KeyboardShortcut? { guard let cfg = self.config else { return nil } let trigger = ghostty_config_trigger(cfg, action, UInt(action.count)) - return Ghostty.keyEquivalent(for: trigger) + return Ghostty.keyboardShortcut(for: trigger) } #endif diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 0a279ea1f..cb4fdc451 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -1,66 +1,52 @@ import Cocoa +import SwiftUI import GhosttyKit extension Ghostty { - // MARK: Key Equivalents + // MARK: Keyboard Shortcuts - /// Returns the "keyEquivalent" string for a given input key. This doesn't always have a corresponding key. - static func keyEquivalent(key: ghostty_input_key_e) -> String? { + /// Returns the SwiftUI KeyEquivalent for a given key. Note that not all keys known by + /// Ghostty have a macOS equivalent since macOS doesn't allow all keys as equivalents. + static func keyEquivalent(key: ghostty_input_key_e) -> KeyEquivalent? { return Self.keyToEquivalent[key] } - /// A convenience struct that has the key + modifiers for some keybinding. - struct KeyEquivalent: CustomStringConvertible { - let key: String - let modifiers: NSEvent.ModifierFlags - - var description: String { - var key = self.key - - // Note: the order below matters; it matches the ordering modifiers - // shown for macOS menu shortcut labels. - if modifiers.contains(.command) { key = "⌘\(key)" } - if modifiers.contains(.shift) { key = "⇧\(key)" } - if modifiers.contains(.option) { key = "⌥\(key)" } - if modifiers.contains(.control) { key = "⌃\(key)" } - - return key - } - } - - /// Return the key equivalent for the given trigger. + /// Return the keyboard shortcut for a trigger. /// - /// Returns nil if the trigger can't be processed. This should only happen for unknown trigger types - /// or keys. - static func keyEquivalent(for trigger: ghostty_input_trigger_s) -> KeyEquivalent? { - let equiv: String + /// Returns nil if the trigger doesn't have an equivalent KeyboardShortcut. This is possible + /// because Ghostty input triggers are a superset of what can be represented by a macOS + /// KeyboardShortcut. For example, macOS doesn't have any way to represent function keys + /// (F1, F2, ...) with a KeyboardShortcut. This doesn't represent a practical issue because input + /// handling for Ghostty is handled at a lower level (usually). This function should generally only + /// be used for things like NSMenu that only support keyboard shortcuts anyways. + static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? { + let key: KeyEquivalent switch (trigger.tag) { case GHOSTTY_TRIGGER_TRANSLATED: if let v = Ghostty.keyEquivalent(key: trigger.key.translated) { - equiv = v + key = v } else { return nil } case GHOSTTY_TRIGGER_PHYSICAL: if let v = Ghostty.keyEquivalent(key: trigger.key.physical) { - equiv = v + key = v } else { return nil } case GHOSTTY_TRIGGER_UNICODE: guard let scalar = UnicodeScalar(trigger.key.unicode) else { return nil } - equiv = String(scalar) + key = KeyEquivalent(Character(scalar)) default: return nil } - return KeyEquivalent( - key: equiv, - modifiers: Ghostty.eventModifierFlags(mods: trigger.mods) - ) + return KeyboardShortcut( + key, + modifiers: EventModifiers(nsFlags: Ghostty.eventModifierFlags(mods: trigger.mods))) } // MARK: Mods @@ -96,8 +82,10 @@ extension Ghostty { return ghostty_input_mods_e(mods) } - /// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. - static let keyToEquivalent: [ghostty_input_key_e : String] = [ + /// A map from the Ghostty key enum to the keyEquivalent string for shortcuts. Note that + /// not all ghostty key enum values are represented here because not all of them can be + /// mapped to a KeyEquivalent. + static let keyToEquivalent: [ghostty_input_key_e : KeyEquivalent] = [ // 0-9 GHOSTTY_KEY_ZERO: "0", GHOSTTY_KEY_ONE: "1", @@ -152,48 +140,19 @@ extension Ghostty { GHOSTTY_KEY_SLASH: "/", // Function keys - GHOSTTY_KEY_UP: "\u{F700}", - GHOSTTY_KEY_DOWN: "\u{F701}", - GHOSTTY_KEY_LEFT: "\u{F702}", - GHOSTTY_KEY_RIGHT: "\u{F703}", - GHOSTTY_KEY_HOME: "\u{F729}", - GHOSTTY_KEY_END: "\u{F72B}", - GHOSTTY_KEY_INSERT: "\u{F727}", - GHOSTTY_KEY_DELETE: "\u{F728}", - GHOSTTY_KEY_PAGE_UP: "\u{F72C}", - GHOSTTY_KEY_PAGE_DOWN: "\u{F72D}", - GHOSTTY_KEY_ESCAPE: "\u{1B}", - GHOSTTY_KEY_ENTER: "\r", - GHOSTTY_KEY_TAB: "\t", - GHOSTTY_KEY_BACKSPACE: "\u{7F}", - GHOSTTY_KEY_PRINT_SCREEN: "\u{F72E}", - GHOSTTY_KEY_PAUSE: "\u{F72F}", - - GHOSTTY_KEY_F1: "\u{F704}", - GHOSTTY_KEY_F2: "\u{F705}", - GHOSTTY_KEY_F3: "\u{F706}", - GHOSTTY_KEY_F4: "\u{F707}", - GHOSTTY_KEY_F5: "\u{F708}", - GHOSTTY_KEY_F6: "\u{F709}", - GHOSTTY_KEY_F7: "\u{F70A}", - GHOSTTY_KEY_F8: "\u{F70B}", - GHOSTTY_KEY_F9: "\u{F70C}", - GHOSTTY_KEY_F10: "\u{F70D}", - GHOSTTY_KEY_F11: "\u{F70E}", - GHOSTTY_KEY_F12: "\u{F70F}", - GHOSTTY_KEY_F13: "\u{F710}", - GHOSTTY_KEY_F14: "\u{F711}", - GHOSTTY_KEY_F15: "\u{F712}", - GHOSTTY_KEY_F16: "\u{F713}", - GHOSTTY_KEY_F17: "\u{F714}", - GHOSTTY_KEY_F18: "\u{F715}", - GHOSTTY_KEY_F19: "\u{F716}", - GHOSTTY_KEY_F20: "\u{F717}", - GHOSTTY_KEY_F21: "\u{F718}", - GHOSTTY_KEY_F22: "\u{F719}", - GHOSTTY_KEY_F23: "\u{F71A}", - GHOSTTY_KEY_F24: "\u{F71B}", - GHOSTTY_KEY_F25: "\u{F71C}", + GHOSTTY_KEY_UP: .upArrow, + GHOSTTY_KEY_DOWN: .downArrow, + GHOSTTY_KEY_LEFT: .leftArrow, + GHOSTTY_KEY_RIGHT: .rightArrow, + GHOSTTY_KEY_HOME: .home, + GHOSTTY_KEY_END: .end, + GHOSTTY_KEY_DELETE: .delete, + GHOSTTY_KEY_PAGE_UP: .pageUp, + GHOSTTY_KEY_PAGE_DOWN: .pageDown, + GHOSTTY_KEY_ESCAPE: .escape, + GHOSTTY_KEY_ENTER: .return, + GHOSTTY_KEY_TAB: .tab, + GHOSTTY_KEY_BACKSPACE: .delete, ] static let asciiToKey: [UInt8 : ghostty_input_key_e] = [ diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 72a324525..5985d64a0 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -43,7 +43,7 @@ extension Ghostty { @Published var hoverUrl: String? = nil // The currently active key sequence. The sequence is not active if this is empty. - @Published var keySequence: [Ghostty.KeyEquivalent] = [] + @Published var keySequence: [KeyboardShortcut] = [] // The time this surface last became focused. This is a ContinuousClock.Instant // on supported platforms. @@ -526,7 +526,7 @@ extension Ghostty { @objc private func ghosttyDidContinueKeySequence(notification: SwiftUI.Notification) { guard let keyAny = notification.userInfo?[Ghostty.Notification.KeySequenceKey] else { return } - guard let key = keyAny as? Ghostty.KeyEquivalent else { return } + guard let key = keyAny as? KeyboardShortcut else { return } DispatchQueue.main.async { [weak self] in self?.keySequence.append(key) } diff --git a/macos/Sources/Helpers/EventModifiers+Extension.swift b/macos/Sources/Helpers/EventModifiers+Extension.swift new file mode 100644 index 000000000..8d379bd99 --- /dev/null +++ b/macos/Sources/Helpers/EventModifiers+Extension.swift @@ -0,0 +1,27 @@ +import SwiftUI + +// MARK: EventModifiers to NSEvent and Back + +extension EventModifiers { + init(nsFlags: NSEvent.ModifierFlags) { + var result: SwiftUI.EventModifiers = [] + if nsFlags.contains(.shift) { result.insert(.shift) } + if nsFlags.contains(.control) { result.insert(.control) } + if nsFlags.contains(.option) { result.insert(.option) } + if nsFlags.contains(.command) { result.insert(.command) } + if nsFlags.contains(.capsLock) { result.insert(.capsLock) } + self = result + } +} + +extension NSEvent.ModifierFlags { + init(swiftUIFlags: SwiftUI.EventModifiers) { + var result: NSEvent.ModifierFlags = [] + if swiftUIFlags.contains(.shift) { result.insert(.shift) } + if swiftUIFlags.contains(.control) { result.insert(.control) } + if swiftUIFlags.contains(.option) { result.insert(.option) } + if swiftUIFlags.contains(.command) { result.insert(.command) } + if swiftUIFlags.contains(.capsLock) { result.insert(.capsLock) } + self = result + } +} diff --git a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift new file mode 100644 index 000000000..b953f5755 --- /dev/null +++ b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift @@ -0,0 +1,45 @@ +import SwiftUI + +extension KeyboardShortcut: @retroactive CustomStringConvertible { + public var description: String { + var result = "" + + if modifiers.contains(.command) { + result.append("⌘") + } + if modifiers.contains(.control) { + result.append("⌃") + } + if modifiers.contains(.option) { + result.append("⌥") + } + if modifiers.contains(.shift) { + result.append("⇧") + } + + let keyString: String + switch key { + case .return: keyString = "⏎" + case .escape: keyString = "⎋" + case .delete: keyString = "⌫" + case .space: keyString = "␣" + case .tab: keyString = "⇥" + case .upArrow: keyString = "↑" + case .downArrow: keyString = "↓" + case .leftArrow: keyString = "←" + case .rightArrow: keyString = "→" + default: + keyString = String(key.character) + } + + result.append(keyString) + return result + } +} + +// This is available in macOS 14 so this only applies to early macOS versions. +extension KeyEquivalent: @retroactive Equatable { + public static func == (lhs: KeyEquivalent, rhs: KeyEquivalent) -> Bool { + lhs.character == rhs.character + } +} From 48cba761b9e7f021f0f8bd00439978b4937495e9 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 20 Apr 2025 00:14:26 +0000 Subject: [PATCH 107/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 086e19dd8..b242d7d11 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz", - .hash = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz", + .hash = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index d43bf3d56..96d8431f7 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3": { + "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz", - "hash": "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz", + "hash": "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 1dc56da50..33bdee518 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAEH8MwQaEsARbyV42-bSZGcu1am8xtg2h67wTFC3"; + name = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz"; - hash = "sha256-c+twvkEPiz1DaULYlnGXLxis19Q2h+TgBJxoARMasjU="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz"; + hash = "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index b9bdc50d2..4068380b4 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/4c57d8c11d352a4aeda6928b65d78794c28883a5.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz From 160b26708ce88d78daac96e347a2f8c4f7928145 Mon Sep 17 00:00:00 2001 From: Kat <65649991+00-kat@users.noreply.github.com> Date: Mon, 21 Apr 2025 03:24:05 +0000 Subject: [PATCH 108/642] =?UTF-8?q?Remove=20note=20about=20default=20from?= =?UTF-8?q?=20bell-features=E2=86=92system's=20description.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 d57ed161b..f243a88a0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1866,7 +1866,7 @@ keybind: Keybinds = .{}, /// /// Valid values are: /// -/// * `system` (default) +/// * `system` /// /// Instructs the system to notify the user using built-in system functions. /// This could result in an audiovisual effect, a notification, or something From d4525f2c575b48553499b58d4e680e139dcc36ab Mon Sep 17 00:00:00 2001 From: Kat <65649991+00-kat@users.noreply.github.com> Date: Mon, 21 Apr 2025 03:41:52 +0000 Subject: [PATCH 109/642] Correct remaining instances of `change_title_prompt` to `prompt_surface_title`. --- macos/Sources/App/macOS/AppDelegate.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 9d866d734..e9ef00347 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -398,7 +398,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "increase_font_size:1", menuItem: self.menuIncreaseFontSize) syncMenuShortcut(config, action: "decrease_font_size:1", menuItem: self.menuDecreaseFontSize) syncMenuShortcut(config, action: "reset_font_size", menuItem: self.menuResetFontSize) - syncMenuShortcut(config, action: "change_title_prompt", menuItem: self.menuChangeTitle) + syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle) syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) From 7e00f2fb7f3bd611941f49cc59e8186d3de16292 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 07:13:58 -0700 Subject: [PATCH 110/642] macOS: get proper unshifted codepoint with ctrl pressed Fixes a regression where `C-S-c` stopped working properly in both legacy and Kitty modes (although the Kitty mode side only affected alternates and not the key itself so it probably worked fine in most programs). The issue is that `charactersIgnoringModifiers` changes behavior if `control` is pressed, so it doesn't really ignore all modifiers. We have to use `characters(byApplyingModifiers:)` to get the proper unshifted codepoint when `control` is pressed. --- macos/Sources/Ghostty/NSEvent+Extension.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index 754bb7a3a..058e7aace 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -33,11 +33,13 @@ extension NSEvent { .subtracting([.control, .command])) // Our unshifted codepoint is the codepoint with no modifiers. We - // ignore multi-codepoint values. + // ignore multi-codepoint values. We have to use `byApplyingModifiers` + // instead of `charactersIgnoringModifiers` because the latter changes + // behavior with ctrl pressed and we don't want any of that. key_ev.unshifted_codepoint = 0 if type == .keyDown || type == .keyUp { - if let charactersIgnoringModifiers, - let codepoint = charactersIgnoringModifiers.unicodeScalars.first + if let chars = characters(byApplyingModifiers: []), + let codepoint = chars.unicodeScalars.first { key_ev.unshifted_codepoint = codepoint.value } From 9709d934f043d889ae28d463fee58f282a9ee92f Mon Sep 17 00:00:00 2001 From: Asadullah Shaikh <159872817+pantheraleo-7@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:42:50 +0530 Subject: [PATCH 111/642] remove "r" & "c" from resize overlay on macOS --- macos/Sources/Ghostty/SurfaceView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 7eebd3ef1..3b9c10067 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -329,7 +329,7 @@ extension Ghostty { Spacer() } - Text(verbatim: "\(size.columns)c ⨯ \(size.rows)r") + Text(verbatim: "\(size.columns) ⨯ \(size.rows)") .padding(.init(top: padding, leading: padding, bottom: padding, trailing: padding)) .background( RoundedRectangle(cornerRadius: 4) From be7fb45e9f42303bb7b5720b408199fe4c44d098 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 18 Apr 2025 14:14:50 -0700 Subject: [PATCH 112/642] command palette SwiftUI view --- macos/Ghostty.xcodeproj/project.pbxproj | 12 + .../Command Palette/CommandPalette.swift | 226 ++++++++++++++++++ .../Features/Terminal/TerminalView.swift | 101 +++++--- 3 files changed, 307 insertions(+), 32 deletions(-) create mode 100644 macos/Sources/Features/Command Palette/CommandPalette.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 5d02ba12b..1a2fc7caa 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */; }; A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */; }; + A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297A2DB2E49400B6E02C /* CommandPalette.swift */; }; A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; }; A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; @@ -142,6 +143,7 @@ A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventModifiers+Extension.swift"; sourceTree = ""; }; A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyboardShortcut+Extension.swift"; sourceTree = ""; }; + A53A297A2DB2E49400B6E02C /* CommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette.swift; sourceTree = ""; }; A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = ""; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; @@ -271,6 +273,7 @@ A5CBD05A2CA0C5910017A1AE /* QuickTerminal */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A57D79252C9C8782001D522E /* Secure Input */, + A53A29742DB2E04900B6E02C /* Command Palette */, A534263E2A7DCC5800EBB7A2 /* Settings */, A51BFC1C2B2FB5AB00E92F16 /* About */, A54B0CE72D0CEC9800CBEFF8 /* Colorized Ghostty Icon */, @@ -325,6 +328,14 @@ path = Settings; sourceTree = ""; }; + A53A29742DB2E04900B6E02C /* Command Palette */ = { + isa = PBXGroup; + children = ( + A53A297A2DB2E49400B6E02C /* CommandPalette.swift */, + ); + path = "Command Palette"; + sourceTree = ""; + }; A53D0C912B53B41900305CE6 /* App */ = { isa = PBXGroup; children = ( @@ -699,6 +710,7 @@ A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, + A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift new file mode 100644 index 000000000..cd68eeeb1 --- /dev/null +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -0,0 +1,226 @@ +import SwiftUI + +struct CommandOption: Identifiable, Hashable { + let id = UUID() + let title: String + let shortcut: String? + let action: () -> Void + + static func == (lhs: CommandOption, rhs: CommandOption) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + // Sample data remains the same + static let sampleData: [CommandOption] = [ + .init(title: "assistant: copy code", shortcut: nil, action: {}), + .init(title: "assistant: inline assist", shortcut: "⌃⏎", action: {}), + .init(title: "assistant: insert into editor", shortcut: "⌘<", action: {}), + .init(title: "assistant: new chat", shortcut: nil, action: {}), + .init(title: "assistant: open prompt library", shortcut: nil, action: {}), + .init(title: "assistant: quote selection", shortcut: "⌘>", action: {}), + .init(title: "assistant: show configuration", shortcut: nil, action: {}), + .init(title: "assistant: toggle focus", shortcut: "⌘?", action: {}), + ] +} + +struct CommandPaletteView: View { + @Binding var isPresented: Bool + var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) + var options: [CommandOption] = CommandOption.sampleData + @State private var query = "" + @State private var selectedIndex: UInt = 0 + @State private var hoveredOptionID: UUID? = nil + + // The options that we should show, taking into account any filtering from + // the query. + var filteredOptions: [CommandOption] { + if query.isEmpty { + return options + } else { + return options.filter { $0.title.localizedCaseInsensitiveContains(query) } + } + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + // Prompt Field + CommandPaletteQuery(query: $query) { event in + switch (event) { + case .exit: + isPresented = false + + case .submit: + isPresented = false + + case .move(.up): + if selectedIndex > 0 { + selectedIndex -= 1 + } + + case .move(.down): + if selectedIndex < filteredOptions.count - 1 { + selectedIndex += 1 + } + + case .move(_): + // Unknown, ignore + break + } + } + + Divider() + .padding(.bottom, 4) + + CommandTable( + query: $query, + selectedIndex: $selectedIndex, + hoveredOptionID: $hoveredOptionID) + } + .frame(width: 500) + .background(backgroundColor) + .cornerRadius(12) + .shadow(radius: 20) + .padding() + } +} + +/// The text field for building the query for the command palette. +fileprivate struct CommandPaletteQuery: View { + @Binding var query: String + var onEvent: ((KeyboardEvent) -> Void)? = nil + @FocusState private var isTextFieldFocused: Bool + + enum KeyboardEvent { + case exit + case submit + case move(MoveCommandDirection) + } + + var body: some View { + ZStack { + Group { + Button { onEvent?(.move(.up)) } label: { Color.clear } + .buttonStyle(PlainButtonStyle()) + .keyboardShortcut(.upArrow, modifiers: []) + Button { onEvent?(.move(.down)) } label: { Color.clear } + .buttonStyle(PlainButtonStyle()) + .keyboardShortcut(.downArrow, modifiers: []) + + Button { onEvent?(.move(.up)) } label: { Color.clear } + .buttonStyle(PlainButtonStyle()) + .keyboardShortcut(.init("p"), modifiers: [.control]) + Button { onEvent?(.move(.down)) } label: { Color.clear } + .buttonStyle(PlainButtonStyle()) + .keyboardShortcut(.init("n"), modifiers: [.control]) + } + .frame(width: 0, height: 0) + .accessibilityHidden(true) + + TextField("Execute a command…", text: $query) + .padding() + .font(.system(size: 14)) + .textFieldStyle(PlainTextFieldStyle()) + .focused($isTextFieldFocused) + .onAppear { + isTextFieldFocused = true + } + .onChange(of: isTextFieldFocused) { focused in + if !focused { + onEvent?(.exit) + } + } + .onExitCommand { onEvent?(.exit) } + .onMoveCommand { onEvent?(.move($0)) } + .onSubmit { onEvent?(.submit) } + } + } +} + +fileprivate struct CommandTable: View { + var options: [CommandOption] = CommandOption.sampleData + @Binding var query: String + @Binding var selectedIndex: UInt + @Binding var hoveredOptionID: UUID? + + // The options that we should show, taking into account any filtering from + // the query. + var filteredOptions: [CommandOption] { + if query.isEmpty { + return options + } else { + return options.filter { $0.title.localizedCaseInsensitiveContains(query) } + } + } + + var body: some View { + if filteredOptions.isEmpty { + Text("No matches") + .foregroundStyle(.secondary) + .padding() + } else { + ScrollViewReader { proxy in + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array(filteredOptions.enumerated()), id: \.1.id) { index, option in + CommandRow( + option: option, + isSelected: selectedIndex == index, + hoveredID: $hoveredOptionID + ) + } + } + } + .frame(height: 200) + .onChange(of: selectedIndex) { _ in + guard selectedIndex < filteredOptions.count else { return } + withAnimation { + proxy.scrollTo( + filteredOptions[Int(selectedIndex)].id, + anchor: .center) + } + } + } + } + } +} + +/// A single row in the command palette. +fileprivate struct CommandRow: View { + let option: CommandOption + var isSelected: Bool + @Binding var hoveredID: UUID? + + var body: some View { + Button(action: option.action) { + HStack { + Text(option.title) + Spacer() + if let shortcut = option.shortcut { + Text(shortcut) + .foregroundStyle(.secondary) + .font(.system(size: 12)) + } + } + .padding(.horizontal, 6) + .padding(.vertical, 8) + .background( + isSelected + ? Color.accentColor.opacity(0.2) + : (hoveredID == option.id + ? Color.secondary.opacity(0.2) + : Color.clear) + ) + .cornerRadius(6) + } + .buttonStyle(PlainButtonStyle()) + .onHover { hovering in + hoveredID = hovering ? option.id : nil + } + .padding(.horizontal, 4) + .padding(.vertical, 1) + } +} diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 3d4165e91..fe48b6b73 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -68,6 +68,8 @@ struct TerminalView: View { return URL(fileURLWithPath: surfacePwd) } + @State var showingCommandPalette = false + var body: some View { switch ghostty.readiness { case .loading: @@ -75,42 +77,77 @@ struct TerminalView: View { case .error: ErrorView() case .ready: - VStack(spacing: 0) { - // If we're running in debug mode we show a warning so that users - // know that performance will be degraded. - if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { - DebugBuildWarningView() - } + ZStack { + VStack(spacing: 0) { + // If we're running in debug mode we show a warning so that users + // know that performance will be degraded. + if (Ghostty.info.mode == GHOSTTY_BUILD_MODE_DEBUG || Ghostty.info.mode == GHOSTTY_BUILD_MODE_RELEASE_SAFE) { + DebugBuildWarningView() + } - Ghostty.TerminalSplit(node: $viewModel.surfaceTree) - .environmentObject(ghostty) - .focused($focused) - .onAppear { self.focused = true } - .onChange(of: focusedSurface) { newValue in - self.delegate?.focusedSurfaceDidChange(to: newValue) + HStack { + Spacer() + Button("Command Palette") { + showingCommandPalette.toggle() + } + Spacer() } - .onChange(of: title) { newValue in - self.delegate?.titleDidChange(to: newValue) - } - .onChange(of: pwdURL) { newValue in - self.delegate?.pwdDidChange(to: newValue) - } - .onChange(of: cellSize) { newValue in - guard let size = newValue else { return } - self.delegate?.cellSizeDidChange(to: size) - } - .onChange(of: viewModel.surfaceTree?.hashValue) { _ in - // This is funky, but its the best way I could think of to detect - // ANY CHANGE within the deeply nested surface tree -- detecting a change - // in the hash value. - self.delegate?.surfaceTreeDidChange() - } - .onChange(of: zoomedSplit) { newValue in - self.delegate?.zoomStateDidChange(to: newValue ?? false) + .background(Color(.windowBackgroundColor)) + .frame(maxWidth: .infinity) + + Ghostty.TerminalSplit(node: $viewModel.surfaceTree) + .environmentObject(ghostty) + .focused($focused) + .onAppear { self.focused = true } + .onChange(of: focusedSurface) { newValue in + self.delegate?.focusedSurfaceDidChange(to: newValue) + } + .onChange(of: title) { newValue in + self.delegate?.titleDidChange(to: newValue) + } + .onChange(of: pwdURL) { newValue in + self.delegate?.pwdDidChange(to: newValue) + } + .onChange(of: cellSize) { newValue in + guard let size = newValue else { return } + self.delegate?.cellSizeDidChange(to: size) + } + .onChange(of: viewModel.surfaceTree?.hashValue) { _ in + // This is funky, but its the best way I could think of to detect + // ANY CHANGE within the deeply nested surface tree -- detecting a change + // in the hash value. + self.delegate?.surfaceTreeDidChange() + } + .onChange(of: zoomedSplit) { newValue in + self.delegate?.zoomStateDidChange(to: newValue ?? false) + } + } + // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style + .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) + + if showingCommandPalette { + // The Palette View Itself + GeometryReader { geometry in + VStack { + Spacer().frame(height: geometry.size.height * 0.1) + + CommandPaletteView( + isPresented: $showingCommandPalette, + backgroundColor: ghostty.config.backgroundColor + ) + .transition( + .move(edge: .top) + .combined(with: .opacity) + .animation(.spring(response: 0.4, dampingFraction: 0.8)) + ) // Spring animation + .zIndex(1) // Ensure it's on top + + Spacer() + } + .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) } + } } - // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style - .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) } } } From a34134e643abe2a9ccc1c79b84a5b0fe6e5b2095 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 12:41:22 -0700 Subject: [PATCH 113/642] input: defind Command struct and default commands --- include/ghostty.h | 6 + src/input.zig | 2 + src/input/Binding.zig | 9 - src/input/command.zig | 393 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 401 insertions(+), 9 deletions(-) create mode 100644 src/input/command.zig diff --git a/include/ghostty.h b/include/ghostty.h index c4ef11930..06b812948 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -279,6 +279,12 @@ typedef struct { ghostty_input_mods_e mods; } ghostty_input_trigger_s; +typedef struct { + const char* action; + const char* title; + const char* description; +} ghostty_command_s; + typedef enum { GHOSTTY_BUILD_MODE_DEBUG, GHOSTTY_BUILD_MODE_RELEASE_SAFE, diff --git a/src/input.zig b/src/input.zig index 83be38d3d..caaf80509 100644 --- a/src/input.zig +++ b/src/input.zig @@ -5,6 +5,7 @@ const mouse = @import("input/mouse.zig"); const key = @import("input/key.zig"); const keyboard = @import("input/keyboard.zig"); +pub const command = @import("input/command.zig"); pub const function_keys = @import("input/function_keys.zig"); pub const keycodes = @import("input/keycodes.zig"); pub const kitty = @import("input/kitty.zig"); @@ -12,6 +13,7 @@ pub const kitty = @import("input/kitty.zig"); pub const ctrlOrSuper = key.ctrlOrSuper; pub const Action = key.Action; pub const Binding = @import("input/Binding.zig"); +pub const Command = command.Command; pub const Link = @import("input/Link.zig"); pub const Key = key.Key; pub const KeyboardLayout = keyboard.Layout; diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 244cd29cd..0b9ae1136 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1017,15 +1017,6 @@ pub const Action = union(enum) { } }; -// A key for the C API to execute an action. This must be kept in sync -// with include/ghostty.h. -pub const Key = enum(c_int) { - copy_to_clipboard, - paste_from_clipboard, - new_tab, - new_window, -}; - /// Trigger is the associated key state that can trigger an action. /// This is an extern struct because this is also used in the C API. /// diff --git a/src/input/command.zig b/src/input/command.zig new file mode 100644 index 000000000..51bcbaad6 --- /dev/null +++ b/src/input/command.zig @@ -0,0 +1,393 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const Action = @import("binding.zig").Action; + +/// A command is a named binding action that can be executed from +/// something like a command palette. +/// +/// A command must be associated with a binding; all commands can be +/// mapped to traditional `keybind` configurations. This restriction +/// makes it so that there is nothing special about commands and likewise +/// it makes it trivial and consistent to define custom commands. +/// +/// For apprt implementers: a command palette doesn't have to make use +/// of all the fields here. We try to provide as much information as +/// possible to make it easier to implement a command palette in the way +/// that makes the most sense for the application. +pub const Command = struct { + action: Action, + title: [:0]const u8, + description: [:0]const u8, + + /// ghostty_command_s + pub const C = extern struct { + action: [*:0]const u8, + title: [*:0]const u8, + description: [*:0]const u8, + }; + + /// Convert this command to a C struct. + pub fn comptimeCval(self: Command) C { + assert(@inComptime()); + + return .{ + .action = std.fmt.comptimePrint("{s}", .{self.action}), + .title = self.title, + .description = self.description, + }; + } +}; + +pub const defaults: []const Command = defaults: { + var count: usize = 0; + for (@typeInfo(Action.Key).@"enum".fields) |field| { + const action = @field(Action.Key, field.name); + count += actionCommands(action).len; + } + + var result: [count]Command = undefined; + var i: usize = 0; + for (@typeInfo(Action.Key).@"enum".fields) |field| { + const action = @field(Action.Key, field.name); + const commands = actionCommands(action); + for (commands) |cmd| { + result[i] = cmd; + i += 1; + } + } + + assert(i == count); + const final = result; + break :defaults &final; +}; + +/// Defaults in C-compatible form. +pub const defaultsC: []const Command.C = defaults: { + var result: [defaults.len]Command.C = undefined; + for (defaults, 0..) |cmd, i| result[i] = cmd.comptimeCval(); + const final = result; + break :defaults &final; +}; + +/// Returns the set of commands associated with this action key by +/// default. Not all actions should have commands. As a general guideline, +/// an action should have a command only if it is useful and reasonable +/// to appear in a command palette. +fn actionCommands(action: Action.Key) []const Command { + // This is implemented as a function and switch rather than a + // flat comptime const because we want to ensure we get a compiler + // error when a new binding is added so that the contributor has + // to consider whether that new binding should have commands or not. + const result: []const Command = switch (action) { + // Note: the use of `comptime` prefix on the return values + // ensures that the data returned is all in the binary and + // and not pointing to the stack. + + .reset => comptime &.{.{ + .action = .reset, + .title = "Reset Terminal", + .description = "Reset the terminal to a clean state.", + }}, + + .copy_to_clipboard => comptime &.{.{ + .action = .copy_to_clipboard, + .title = "Copy to Clipboard", + .description = "Copy the selected text to the clipboard.", + }}, + + .copy_url_to_clipboard => comptime &.{.{ + .action = .copy_url_to_clipboard, + .title = "Copy URL to Clipboard", + .description = "Copy the URL under the cursor to the clipboard.", + }}, + + .paste_from_clipboard => comptime &.{.{ + .action = .paste_from_clipboard, + .title = "Paste from Clipboard", + .description = "Paste the contents of the clipboard.", + }}, + + .paste_from_selection => comptime &.{.{ + .action = .paste_from_selection, + .title = "Paste from Selection", + .description = "Paste the contents of the selection clipboard.", + }}, + + .increase_font_size => comptime &.{.{ + .action = .{ .increase_font_size = 1 }, + .title = "Increase Font Size", + .description = "Increase the font size by 1 point.", + }}, + + .decrease_font_size => comptime &.{.{ + .action = .{ .decrease_font_size = 1 }, + .title = "Decrease Font Size", + .description = "Decrease the font size by 1 point.", + }}, + + .reset_font_size => comptime &.{.{ + .action = .reset_font_size, + .title = "Reset Font Size", + .description = "Reset the font size to the default.", + }}, + + .clear_screen => comptime &.{.{ + .action = .clear_screen, + .title = "Clear Screen", + .description = "Clear the screen and scrollback.", + }}, + + .select_all => comptime &.{.{ + .action = .select_all, + .title = "Select All", + .description = "Select all text on the screen.", + }}, + + .scroll_to_top => comptime &.{.{ + .action = .scroll_to_top, + .title = "Scroll to Top", + .description = "Scroll to the top of the screen.", + }}, + + .scroll_to_bottom => comptime &.{.{ + .action = .scroll_to_bottom, + .title = "Scroll to Bottom", + .description = "Scroll to the bottom of the screen.", + }}, + + .scroll_page_up => comptime &.{.{ + .action = .scroll_page_up, + .title = "Scroll Page Up", + .description = "Scroll the screen up by a page.", + }}, + + .scroll_page_down => comptime &.{.{ + .action = .scroll_page_down, + .title = "Scroll Page Down", + .description = "Scroll the screen down by a page.", + }}, + + .write_screen_file => comptime &.{ + .{ + .action = .{ .write_screen_file = .paste }, + .title = "Copy Screen to Temporary File and Paste Path", + .description = "Copy the screen contents to a temporary file and paste the path to the file.", + }, + .{ + .action = .{ .write_screen_file = .open }, + .title = "Copy Screen to Temporary File and Open", + .description = "Copy the screen contents to a temporary file and open it.", + }, + }, + + .write_selection_file => comptime &.{ + .{ + .action = .{ .write_selection_file = .paste }, + .title = "Copy Selection to Temporary File and Paste Path", + .description = "Copy the selection contents to a temporary file and paste the path to the file.", + }, + .{ + .action = .{ .write_selection_file = .open }, + .title = "Copy Selection to Temporary File and Open", + .description = "Copy the selection contents to a temporary file and open it.", + }, + }, + + .new_window => comptime &.{.{ + .action = .new_window, + .title = "New Window", + .description = "Open a new window.", + }}, + + .new_tab => comptime &.{.{ + .action = .new_tab, + .title = "New Tab", + .description = "Open a new tab.", + }}, + + .move_tab => comptime &.{ + .{ + .action = .{ .move_tab = -1 }, + .title = "Move Tab Left", + .description = "Move the current tab to the left.", + }, + .{ + .action = .{ .move_tab = 1 }, + .title = "Move Tab Right", + .description = "Move the current tab to the right.", + }, + }, + + .toggle_tab_overview => comptime &.{.{ + .action = .toggle_tab_overview, + .title = "Toggle Tab Overview", + .description = "Toggle the tab overview.", + }}, + + .prompt_surface_title => comptime &.{.{ + .action = .prompt_surface_title, + .title = "Change Title...", + .description = "Prompt for a new title for the current terminal.", + }}, + + .new_split => comptime &.{ + .{ + .action = .{ .new_split = .left }, + .title = "Split Left", + .description = "Split the terminal to the left.", + }, + .{ + .action = .{ .new_split = .right }, + .title = "Split Right", + .description = "Split the terminal to the right.", + }, + .{ + .action = .{ .new_split = .up }, + .title = "Split Up", + .description = "Split the terminal up.", + }, + .{ + .action = .{ .new_split = .down }, + .title = "Split Down", + .description = "Split the terminal down.", + }, + }, + + .toggle_split_zoom => comptime &.{.{ + .action = .toggle_split_zoom, + .title = "Toggle Split Zoom", + .description = "Toggle the zoom state of the current split.", + }}, + + .equalize_splits => comptime &.{.{ + .action = .equalize_splits, + .title = "Equalize Splits", + .description = "Equalize the size of all splits.", + }}, + + .reset_window_size => comptime &.{.{ + .action = .reset_window_size, + .title = "Reset Window Size", + .description = "Reset the window size to the default.", + }}, + + .inspector => comptime &.{.{ + .action = .{ .inspector = .toggle }, + .title = "Toggle Inspector", + .description = "Toggle the inspector.", + }}, + + .open_config => comptime &.{.{ + .action = .open_config, + .title = "Open Config", + .description = "Open the config file.", + }}, + + .reload_config => comptime &.{.{ + .action = .reload_config, + .title = "Reload Config", + .description = "Reload the config file.", + }}, + + .close_surface => comptime &.{.{ + .action = .close_surface, + .title = "Close Terminal", + .description = "Close the current terminal.", + }}, + + .close_tab => comptime &.{.{ + .action = .close_tab, + .title = "Close Tab", + .description = "Close the current tab.", + }}, + + .close_window => comptime &.{.{ + .action = .close_window, + .title = "Close Window", + .description = "Close the current window.", + }}, + + .close_all_windows => comptime &.{.{ + .action = .close_all_windows, + .title = "Close All Windows", + .description = "Close all windows.", + }}, + + .toggle_maximize => comptime &.{.{ + .action = .toggle_maximize, + .title = "Toggle Maximize", + .description = "Toggle the maximized state of the current window.", + }}, + + .toggle_fullscreen => comptime &.{.{ + .action = .toggle_fullscreen, + .title = "Toggle Fullscreen", + .description = "Toggle the fullscreen state of the current window.", + }}, + + .toggle_window_decorations => comptime &.{.{ + .action = .toggle_window_decorations, + .title = "Toggle Window Decorations", + .description = "Toggle the window decorations.", + }}, + + .toggle_secure_input => comptime &.{.{ + .action = .toggle_secure_input, + .title = "Toggle Secure Input", + .description = "Toggle secure input mode.", + }}, + + .quit => comptime &.{.{ + .action = .quit, + .title = "Quit", + .description = "Quit the application.", + }}, + + // No commands because they're parameterized and there + // aren't obvious values users would use. It is possible that + // these may have commands in the future if there are very + // common values that users tend to use. + .csi, + .esc, + .text, + .cursor_key, + .scroll_page_fractional, + .scroll_page_lines, + .adjust_selection, + .jump_to_prompt, + .write_scrollback_file, + .goto_tab, + .goto_split, + .resize_split, + .crash, + => comptime &.{}, + + // No commands because I'm not sure they make sense in a command + // palette context. + .toggle_quick_terminal, + .toggle_visibility, + .previous_tab, + .next_tab, + .last_tab, + => comptime &.{}, + + // No commands for obvious reasons + .ignore, + .unbind, + => comptime &.{}, + }; + + // All generated commands should have the same action as the + // action passed in. + for (result) |cmd| assert(cmd.action == action); + + return result; +} + +test "command defaults" { + // This just ensures that defaults is analyzed and works. + const testing = std.testing; + try testing.expect(defaults.len > 0); + try testing.expectEqual(defaults.len, defaultsC.len); +} From 8615dfb73de79cb8e31d8217ea1cf8fe2e5bc7bb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 13:22:36 -0700 Subject: [PATCH 114/642] libghostty: add API for getting commands --- include/ghostty.h | 1 + src/apprt/embedded.zig | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index 06b812948..05e15c54f 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -730,6 +730,7 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t, ghostty_color_scheme_e); ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t, ghostty_input_mods_e); +void ghostty_surface_commands(ghostty_surface_t, ghostty_command_s**, size_t*); bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s); bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s); void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index e8da8612c..22ae6e488 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1487,6 +1487,23 @@ pub const CAPI = struct { return @intCast(@as(input.Mods.Backing, @bitCast(result))); } + /// Returns the current possible commands for a surface + /// in the output parameter. The memory is owned by libghostty + /// and doesn't need to be freed. + export fn ghostty_surface_commands( + surface: *Surface, + out: *[*]const input.Command.C, + len: *usize, + ) void { + // In the future we may use this information to filter + // some commands. + _ = surface; + + const commands = input.command.defaultsC; + out.* = commands.ptr; + len.* = commands.len; + } + /// Send this for raw keypresses (i.e. the keyDown event on macOS). /// This will handle the keymap translation and send the appropriate /// key and char events. From 5fab6faf042c06706a7da5606e6d21d9b445fcb3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 19 Apr 2025 13:44:47 -0700 Subject: [PATCH 115/642] macOS: hook up command palette C API to actual command palette --- .../Command Palette/CommandPalette.swift | 3 +- .../Features/Terminal/TerminalView.swift | 32 ++++++++++++++++++- macos/Sources/Ghostty/Ghostty.Input.swift | 2 +- macos/Sources/Helpers/Weak.swift | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index cd68eeeb1..09425f471 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -76,6 +76,7 @@ struct CommandPaletteView: View { .padding(.bottom, 4) CommandTable( + options: options, query: $query, selectedIndex: $selectedIndex, hoveredOptionID: $hoveredOptionID) @@ -197,7 +198,7 @@ fileprivate struct CommandRow: View { var body: some View { Button(action: option.action) { HStack { - Text(option.title) + Text(option.title.lowercased()) Spacer() if let shortcut = option.shortcut { Text(shortcut) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index fe48b6b73..5a86b9ff8 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -44,6 +44,10 @@ struct TerminalView: View { // An optional delegate to receive information about terminal changes. weak var delegate: (any TerminalViewDelegate)? = nil + // The most recently focused surface, equal to focusedSurface when + // it is non-nil. + @State private var lastFocusedSurface: Weak = .init() + // This seems like a crutch after switching from SwiftUI to AppKit lifecycle. @FocusState private var focused: Bool @@ -68,6 +72,25 @@ struct TerminalView: View { return URL(fileURLWithPath: surfacePwd) } + // The commands available to the command palette. + private var commandOptions: [CommandOption] { + guard let surface = lastFocusedSurface.value?.surface else { return [] } + + var ptr: UnsafeMutablePointer? = nil + var count: Int = 0 + ghostty_surface_commands(surface, &ptr, &count) + guard let ptr else { return [] } + + let buffer = UnsafeBufferPointer(start: ptr, count: count) + return Array(buffer).map { c in + let action = String(cString: c.action) + return CommandOption( + title: String(cString: c.title), + shortcut: ghostty.config.keyEquivalent(for: action)?.description + ) {} + } + } + @State var showingCommandPalette = false var body: some View { @@ -100,6 +123,12 @@ struct TerminalView: View { .focused($focused) .onAppear { self.focused = true } .onChange(of: focusedSurface) { newValue in + // We want to keep track of our last focused surface so even if + // we lose focus we keep this set to the last non-nil value. + if newValue != nil { + lastFocusedSurface = .init(newValue) + } + self.delegate?.focusedSurfaceDidChange(to: newValue) } .onChange(of: title) { newValue in @@ -133,7 +162,8 @@ struct TerminalView: View { CommandPaletteView( isPresented: $showingCommandPalette, - backgroundColor: ghostty.config.backgroundColor + backgroundColor: ghostty.config.backgroundColor, + options: commandOptions ) .transition( .move(edge: .top) diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index cb4fdc451..0be579122 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -11,7 +11,7 @@ extension Ghostty { return Self.keyToEquivalent[key] } - /// Return the keyboard shortcut for a trigger. + /// Return the key equivalent for the given trigger. /// /// Returns nil if the trigger doesn't have an equivalent KeyboardShortcut. This is possible /// because Ghostty input triggers are a superset of what can be represented by a macOS diff --git a/macos/Sources/Helpers/Weak.swift b/macos/Sources/Helpers/Weak.swift index d5f784844..0fbb9bd87 100644 --- a/macos/Sources/Helpers/Weak.swift +++ b/macos/Sources/Helpers/Weak.swift @@ -3,7 +3,7 @@ class Weak { weak var value: T? - init(_ value: T) { + init(_ value: T? = nil) { self.value = value } } From 0915a7af46c9dd75791999bdc4c405eb2de014d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 08:34:46 -0700 Subject: [PATCH 116/642] macOS: extract TerminalCommandPalette --- macos/Ghostty.xcodeproj/project.pbxproj | 8 +- .../TerminalCommandPalette.swift | 77 +++++++++++++++++++ .../Terminal/BaseTerminalController.swift | 7 +- .../Features/Terminal/TerminalView.swift | 53 +++---------- 4 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 macos/Sources/Features/Command Palette/TerminalCommandPalette.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 1a2fc7caa..a34c4685f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -34,9 +34,10 @@ A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */; }; A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */; }; A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A535B9D9299C569B0017E2E4 /* ErrorView.swift */; }; + A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297A2DB2E49400B6E02C /* CommandPalette.swift */; }; A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */; }; A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */; }; - A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A297A2DB2E49400B6E02C /* CommandPalette.swift */; }; + A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A29872DB69D2C00B6E02C /* TerminalCommandPalette.swift */; }; A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; }; A53D0C8E2B53B0EA00305CE6 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C932B53B43700305CE6 /* iOSApp.swift */; }; @@ -141,9 +142,10 @@ A5333E212B5A2128008AEFF7 /* SurfaceView_AppKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView_AppKit.swift; sourceTree = ""; }; A53426342A7DA53D00EBB7A2 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; A535B9D9299C569B0017E2E4 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A53A297A2DB2E49400B6E02C /* CommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette.swift; sourceTree = ""; }; A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EventModifiers+Extension.swift"; sourceTree = ""; }; A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KeyboardShortcut+Extension.swift"; sourceTree = ""; }; - A53A297A2DB2E49400B6E02C /* CommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette.swift; sourceTree = ""; }; + A53A29872DB69D2C00B6E02C /* TerminalCommandPalette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalCommandPalette.swift; sourceTree = ""; }; A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Action.swift; sourceTree = ""; }; A53D0C932B53B43700305CE6 /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.App.swift; sourceTree = ""; }; @@ -332,6 +334,7 @@ isa = PBXGroup; children = ( A53A297A2DB2E49400B6E02C /* CommandPalette.swift */, + A53A29872DB69D2C00B6E02C /* TerminalCommandPalette.swift */, ); path = "Command Palette"; sourceTree = ""; @@ -726,6 +729,7 @@ A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, + A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */, A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */, diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift new file mode 100644 index 000000000..7b6676aad --- /dev/null +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -0,0 +1,77 @@ +import SwiftUI +import GhosttyKit + +struct TerminalCommandPaletteView: View { + /// The surface that this command palette represents. + let surfaceView: Ghostty.SurfaceView + + /// Set this to true to show the view, this will be set to false if any actions + /// result in the view disappearing. + @Binding var isPresented: Bool + + /// The configuration so we can lookup keyboard shortcuts. + @ObservedObject var ghosttyConfig: Ghostty.Config + + /// The callback when an action is submitted. + var onAction: ((String) -> Void) + + // The commands available to the command palette. + private var commandOptions: [CommandOption] { + guard let surface = surfaceView.surface else { return [] } + + var ptr: UnsafeMutablePointer? = nil + var count: Int = 0 + ghostty_surface_commands(surface, &ptr, &count) + guard let ptr else { return [] } + + let buffer = UnsafeBufferPointer(start: ptr, count: count) + return Array(buffer).map { c in + let action = String(cString: c.action) + return CommandOption( + title: String(cString: c.title), + shortcut: ghosttyConfig.keyboardShortcut(for: action)?.description + ) { + onAction(action) + } + } + } + + var body: some View { + ZStack { + if isPresented { + GeometryReader { geometry in + VStack { + Spacer().frame(height: geometry.size.height * 0.1) + + CommandPaletteView( + isPresented: $isPresented, + backgroundColor: ghosttyConfig.backgroundColor, + options: commandOptions + ) + .transition( + .move(edge: .top) + .combined(with: .opacity) + .animation(.spring(response: 0.4, dampingFraction: 0.8)) + ) // Spring animation + .zIndex(1) // Ensure it's on top + + Spacer() + } + .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) + } + } + } + .onChange(of: isPresented) { newValue in + // When the command palette disappears we need to send focus back to the + // surface view we were overlaid on top of. There's probably a better way + // to handle the first responder state here but I don't know it. + if !newValue { + // Has to be on queue because onChange happens on a user-interactive + // thread and Xcode is mad about this call on that. + DispatchQueue.main.async { + surfaceView.window?.makeFirstResponder(surfaceView) + } + } + } + } +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 3b4b1a2ef..b9a1def1b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -45,6 +45,9 @@ class BaseTerminalController: NSWindowController, didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } } + /// This can be set to show/hide the command palette. + @Published var commandPaletteIsShowing: Bool = false + /// Whether the terminal surface should focus when the mouse is over it. var focusFollowsMouse: Bool { self.derivedConfig.focusFollowsMouse @@ -209,12 +212,12 @@ class BaseTerminalController: NSWindowController, // We only care if the configuration is a global configuration, not a // surface-specific one. guard notification.object == nil else { return } - + // Get our managed configuration object out guard let config = notification.userInfo?[ Notification.Name.GhosttyConfigChangeKey ] as? Ghostty.Config else { return } - + // Update our derived config self.derivedConfig = DerivedConfig(config) } diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 5a86b9ff8..e3c4f04b7 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -32,6 +32,9 @@ protocol TerminalViewModel: ObservableObject { /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView /// and children. This should be @Published. var surfaceTree: Ghostty.SplitNode? { get set } + + /// The command palette state. + var commandPaletteIsShowing: Bool { get set } } /// The main terminal view. This terminal view supports splits. @@ -72,27 +75,6 @@ struct TerminalView: View { return URL(fileURLWithPath: surfacePwd) } - // The commands available to the command palette. - private var commandOptions: [CommandOption] { - guard let surface = lastFocusedSurface.value?.surface else { return [] } - - var ptr: UnsafeMutablePointer? = nil - var count: Int = 0 - ghostty_surface_commands(surface, &ptr, &count) - guard let ptr else { return [] } - - let buffer = UnsafeBufferPointer(start: ptr, count: count) - return Array(buffer).map { c in - let action = String(cString: c.action) - return CommandOption( - title: String(cString: c.title), - shortcut: ghostty.config.keyEquivalent(for: action)?.description - ) {} - } - } - - @State var showingCommandPalette = false - var body: some View { switch ghostty.readiness { case .loading: @@ -111,7 +93,7 @@ struct TerminalView: View { HStack { Spacer() Button("Command Palette") { - showingCommandPalette.toggle() + viewModel.commandPaletteIsShowing.toggle() } Spacer() } @@ -154,27 +136,12 @@ struct TerminalView: View { // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) - if showingCommandPalette { - // The Palette View Itself - GeometryReader { geometry in - VStack { - Spacer().frame(height: geometry.size.height * 0.1) - - CommandPaletteView( - isPresented: $showingCommandPalette, - backgroundColor: ghostty.config.backgroundColor, - options: commandOptions - ) - .transition( - .move(edge: .top) - .combined(with: .opacity) - .animation(.spring(response: 0.4, dampingFraction: 0.8)) - ) // Spring animation - .zIndex(1) // Ensure it's on top - - Spacer() - } - .frame(width: geometry.size.width, height: geometry.size.height, alignment: .top) + if let surfaceView = lastFocusedSurface.value { + TerminalCommandPaletteView( + surfaceView: surfaceView, + isPresented: $viewModel.commandPaletteIsShowing, + ghosttyConfig: ghostty.config) { action in + print(action) } } } From afd4ec6de267e0deb27099fd8ce9604d9cceb031 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 09:32:39 -0700 Subject: [PATCH 117/642] macOS: command palette "enter" works --- .../Command Palette/CommandPalette.swift | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 09425f471..71fc7ae4d 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -55,6 +55,9 @@ struct CommandPaletteView: View { case .submit: isPresented = false + if selectedIndex < filteredOptions.count { + filteredOptions[Int(selectedIndex)].action() + } case .move(.up): if selectedIndex > 0 { @@ -79,7 +82,10 @@ struct CommandPaletteView: View { options: options, query: $query, selectedIndex: $selectedIndex, - hoveredOptionID: $hoveredOptionID) + hoveredOptionID: $hoveredOptionID) { option in + isPresented = false + option.action() + } } .frame(width: 500) .background(backgroundColor) @@ -146,6 +152,7 @@ fileprivate struct CommandTable: View { @Binding var query: String @Binding var selectedIndex: UInt @Binding var hoveredOptionID: UUID? + var action: (CommandOption) -> Void // The options that we should show, taking into account any filtering from // the query. @@ -171,7 +178,9 @@ fileprivate struct CommandTable: View { option: option, isSelected: selectedIndex == index, hoveredID: $hoveredOptionID - ) + ) { + action(option) + } } } } @@ -194,9 +203,10 @@ fileprivate struct CommandRow: View { let option: CommandOption var isSelected: Bool @Binding var hoveredID: UUID? + var action: () -> Void var body: some View { - Button(action: option.action) { + Button(action: action) { HStack { Text(option.title.lowercased()) Spacer() From 8bd91e71041145377eb685d4e804429aaf35d6f4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 09:40:02 -0700 Subject: [PATCH 118/642] macOS: hook up full action execution --- .../Features/Command Palette/CommandPalette.swift | 1 - .../Features/Terminal/BaseTerminalController.swift | 9 +++++++++ macos/Sources/Features/Terminal/TerminalView.swift | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 71fc7ae4d..33b97585a 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -47,7 +47,6 @@ struct CommandPaletteView: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - // Prompt Field CommandPaletteQuery(query: $query) { event in switch (event) { case .exit: diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b9a1def1b..d4e7dfb45 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -291,6 +291,15 @@ class BaseTerminalController: NSWindowController, func zoomStateDidChange(to: Bool) {} + func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { + guard let surface = surfaceView.surface else { return } + let len = action.utf8CString.count + if (len == 0) { return } + _ = action.withCString { cString in + ghostty_surface_binding_action(surface, cString, UInt(len - 1)) + } + } + // MARK: Fullscreen /// Toggle fullscreen for the given mode. diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index e3c4f04b7..1bc0603a9 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -23,6 +23,9 @@ protocol TerminalViewDelegate: AnyObject { /// This is called when a split is zoomed. func zoomStateDidChange(to: Bool) + + /// Perform an action. At the time of writing this is only triggered by the command palette. + func performAction(_ action: String, on: Ghostty.SurfaceView) } /// The view model is a required implementation for TerminalView callers. This contains @@ -141,7 +144,7 @@ struct TerminalView: View { surfaceView: surfaceView, isPresented: $viewModel.commandPaletteIsShowing, ghosttyConfig: ghostty.config) { action in - print(action) + self.delegate?.performAction(action, on: surfaceView) } } } From 6d2685b5a2353e95f697f4be9c1e603a4c886211 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 09:52:21 -0700 Subject: [PATCH 119/642] add toggle command palette binding --- include/ghostty.h | 1 + macos/Sources/App/macOS/AppDelegate.swift | 2 ++ macos/Sources/App/macOS/MainMenu.xib | 11 ++++++-- .../Terminal/BaseTerminalController.swift | 15 +++++++++++ .../Features/Terminal/TerminalView.swift | 10 -------- macos/Sources/Ghostty/Ghostty.App.swift | 25 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 1 + src/Surface.zig | 6 +++++ src/apprt/action.zig | 6 +++++ src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 1 + src/config/Config.zig | 7 ++++++ src/input/Binding.zig | 9 +++++++ src/input/command.zig | 1 + 14 files changed, 84 insertions(+), 12 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 05e15c54f..3fd582077 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -579,6 +579,7 @@ typedef enum { GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW, GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS, GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL, + GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE, GHOSTTY_ACTION_TOGGLE_VISIBILITY, GHOSTTY_ACTION_MOVE_TAB, GHOSTTY_ACTION_GOTO_TAB, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 2d83c0074..75d0ef7ac 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -59,6 +59,7 @@ class AppDelegate: NSObject, @IBOutlet private var menuChangeTitle: NSMenuItem? @IBOutlet private var menuQuickTerminal: NSMenuItem? @IBOutlet private var menuTerminalInspector: NSMenuItem? + @IBOutlet private var menuCommandPalette: NSMenuItem? @IBOutlet private var menuEqualizeSplits: NSMenuItem? @IBOutlet private var menuMoveSplitDividerUp: NSMenuItem? @@ -402,6 +403,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) + syncMenuShortcut(config, action: "toggle_command_palette", menuItem: self.menuCommandPalette) syncMenuShortcut(config, action: "toggle_secure_input", menuItem: self.menuSecureInput) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 88db6ed01..8f7b16aa9 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -21,6 +21,7 @@ + @@ -249,6 +250,12 @@ + + + + + + diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index d4e7dfb45..d73e85111 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -110,6 +110,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyConfigDidChangeBase(_:)), name: .ghosttyConfigDidChange, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyCommandPaletteDidToggle(_:)), + name: .ghosttyCommandPaletteDidToggle, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -222,6 +227,12 @@ class BaseTerminalController: NSWindowController, self.derivedConfig = DerivedConfig(config) } + @objc private func ghosttyCommandPaletteDidToggle(_ notification: Notification) { + guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree?.contains(view: surfaceView) ?? false else { return } + toggleCommandPalette(nil) + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -631,6 +642,10 @@ class BaseTerminalController: NSWindowController, ghostty.changeFontSize(surface: surface, .reset) } + @IBAction func toggleCommandPalette(_ sender: Any?) { + commandPaletteIsShowing.toggle() + } + @objc func resetTerminal(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } ghostty.resetTerminal(surface: surface) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 1bc0603a9..758ee4b81 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -93,16 +93,6 @@ struct TerminalView: View { DebugBuildWarningView() } - HStack { - Spacer() - Button("Command Palette") { - viewModel.commandPaletteIsShowing.toggle() - } - Spacer() - } - .background(Color(.windowBackgroundColor)) - .frame(maxWidth: .infinity) - Ghostty.TerminalSplit(node: $viewModel.surfaceTree) .environmentObject(ghostty) .focused($focused) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index d7fd0c777..677129960 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -520,6 +520,9 @@ extension Ghostty { case GHOSTTY_ACTION_RENDERER_HEALTH: rendererHealth(app, target: target, v: action.action.renderer_health) + case GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE: + toggleCommandPalette(app, target: target) + case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL: toggleQuickTerminal(app, target: target) @@ -742,6 +745,28 @@ extension Ghostty { } } + private static func toggleCommandPalette( + _ app: ghostty_app_t, + target: ghostty_target_s) { + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle command palette does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + NotificationCenter.default.post( + name: .ghosttyCommandPaletteDidToggle, + object: surfaceView + ) + + + default: + assertionFailure() + } + } + private static func toggleVisibility( _ app: ghostty_app_t, target: ghostty_target_s diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 3afca56aa..e2c770899 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -256,6 +256,7 @@ extension Notification.Name { /// Ring the bell static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing") + static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle") } // NOTE: I am moving all of these to Notification.Name extensions over time. This diff --git a/src/Surface.zig b/src/Surface.zig index b9eb9e14a..c776fed36 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4295,6 +4295,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .toggle, ), + .toggle_command_palette => return try self.rt_app.performAction( + .{ .surface = self }, + .toggle_command_palette, + {}, + ), + .select_all => { const sel = self.io.terminal.screen.selectAll(); if (sel) |s| { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 30cbfb1e1..da0ebf8e6 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -107,6 +107,9 @@ pub const Action = union(Key) { /// Toggle the quick terminal in or out. toggle_quick_terminal, + /// Toggle the command palette. This currently only works on macOS. + toggle_command_palette, + /// Toggle the visibility of all Ghostty terminal windows. toggle_visibility, @@ -244,6 +247,8 @@ pub const Action = union(Key) { /// Closes the currently focused window. close_window, + /// Called when the bell character is seen. The apprt should do whatever + /// it needs to ring the bell. This is usually a sound or visual effect. ring_bell, /// Sync with: ghostty_action_tag_e @@ -259,6 +264,7 @@ pub const Action = union(Key) { toggle_tab_overview, toggle_window_decorations, toggle_quick_terminal, + toggle_command_palette, toggle_visibility, move_tab, goto_tab, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index c5ee802c4..66b994051 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -228,6 +228,7 @@ pub const App = struct { .toggle_tab_overview, .toggle_window_decorations, .toggle_quick_terminal, + .toggle_command_palette, .toggle_visibility, .goto_tab, .move_tab, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index a14383ca3..72c0d7509 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -488,6 +488,7 @@ pub fn performAction( // Unimplemented .close_all_windows, + .toggle_command_palette, .toggle_visibility, .cell_size, .key_sequence, diff --git a/src/config/Config.zig b/src/config/Config.zig index f243a88a0..f71e0972d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4866,6 +4866,13 @@ pub const Keybinds = struct { .{ .jump_to_prompt = 1 }, ); + // Toggle command palette, matches VSCode + try self.set.put( + alloc, + .{ .key = .{ .translated = .p }, .mods = .{ .super = true, .shift = true } }, + .{ .toggle_command_palette = {} }, + ); + // Inspector, matching Chromium try self.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 0b9ae1136..1a2961a53 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -441,6 +441,14 @@ pub const Action = union(enum) { /// This only works on macOS, since this is a system API on macOS. toggle_secure_input: void, + /// Toggle the command palette. The command palette is a UI element + /// that lets you see what actions you can perform, their associated + /// keybindings (if any), a search bar to filter the actions, and + /// the ability to then execute the action. + /// + /// This only works on macOS. + toggle_command_palette, + /// Toggle the "quick" terminal. The quick terminal is a terminal that /// appears on demand from a keybinding, often sliding in from a screen /// edge such as the top. This is useful for quick access to a terminal @@ -790,6 +798,7 @@ pub const Action = union(enum) { .toggle_fullscreen, .toggle_window_decorations, .toggle_secure_input, + .toggle_command_palette, .reset_window_size, .crash, => .surface, diff --git a/src/input/command.zig b/src/input/command.zig index 51bcbaad6..6c6e3a55b 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -365,6 +365,7 @@ fn actionCommands(action: Action.Key) []const Command { // No commands because I'm not sure they make sense in a command // palette context. + .toggle_command_palette, .toggle_quick_terminal, .toggle_visibility, .previous_tab, From f2fa47bca7e3dbe2c73a44bd3c87a82a4410519d Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Mon, 21 Apr 2025 12:12:54 -0500 Subject: [PATCH 120/642] apprt/gtk: fix typo I had a copy-paste error when I used right instead of up. Signed-off-by: Tristan Partin --- src/apprt/gtk/Window.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 129c149e7..1300aed57 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -1058,7 +1058,7 @@ fn gtkActionSplitUp( _: ?*glib.Variant, self: *Window, ) callconv(.C) void { - self.performBindingAction(.{ .new_split = .right }); + self.performBindingAction(.{ .new_split = .up }); } fn gtkActionToggleInspector( From 63b4cb4ead3a06789186db0e3f00c06497e49f14 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 10:16:52 -0700 Subject: [PATCH 121/642] macOS: fix responder chain --- .../TerminalCommandPalette.swift | 18 ++++++++++++++++++ .../Terminal/BaseTerminalController.swift | 1 + .../Features/Terminal/TerminalView.swift | 3 +-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 7b6676aad..29ce28906 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -43,6 +43,9 @@ struct TerminalCommandPaletteView: View { VStack { Spacer().frame(height: geometry.size.height * 0.1) + ResponderChainInjector(responder: surfaceView) + .frame(width: 0, height: 0) + CommandPaletteView( isPresented: $isPresented, backgroundColor: ghosttyConfig.backgroundColor, @@ -75,3 +78,18 @@ struct TerminalCommandPaletteView: View { } } } + +/// This is done to ensure that the given view is in the responder chain. +fileprivate struct ResponderChainInjector: NSViewRepresentable { + let responder: NSResponder + + func makeNSView(context: Context) -> NSView { + let dummy = NSView() + DispatchQueue.main.async { + dummy.nextResponder = responder + } + return dummy + } + + func updateNSView(_ nsView: NSView, context: Context) {} +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index d73e85111..b502e56e0 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -152,6 +152,7 @@ class BaseTerminalController: NSWindowController, // Our focus state requires that this window is key and our currently // focused surface is the surface in this leaf. let focused: Bool = (window?.isKeyWindow ?? false) && + !commandPaletteIsShowing && focusedSurface != nil && leaf.surface == focusedSurface! leaf.surface.focusDidChange(focused) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 758ee4b81..1178c75a5 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -102,9 +102,8 @@ struct TerminalView: View { // we lose focus we keep this set to the last non-nil value. if newValue != nil { lastFocusedSurface = .init(newValue) + self.delegate?.focusedSurfaceDidChange(to: newValue) } - - self.delegate?.focusedSurfaceDidChange(to: newValue) } .onChange(of: title) { newValue in self.delegate?.titleDidChange(to: newValue) From 6dad763e69877b17a09f6ea5634b2d0112fb0b9e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 10:20:25 -0700 Subject: [PATCH 122/642] input: omit commands that are platform-specific --- src/input/command.zig | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/input/command.zig b/src/input/command.zig index 6c6e3a55b..017a14a18 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Action = @import("binding.zig").Action; @@ -219,11 +220,11 @@ fn actionCommands(action: Action.Key) []const Command { }, }, - .toggle_tab_overview => comptime &.{.{ + .toggle_tab_overview => comptime if (builtin.os.tag == .linux) &.{.{ .action = .toggle_tab_overview, .title = "Toggle Tab Overview", .description = "Toggle the tab overview.", - }}, + }} else &.{}, .prompt_surface_title => comptime &.{.{ .action = .prompt_surface_title, @@ -314,11 +315,11 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Close all windows.", }}, - .toggle_maximize => comptime &.{.{ + .toggle_maximize => comptime if (!builtin.os.tag.isDarwin()) &.{.{ .action = .toggle_maximize, .title = "Toggle Maximize", .description = "Toggle the maximized state of the current window.", - }}, + }} else &.{}, .toggle_fullscreen => comptime &.{.{ .action = .toggle_fullscreen, @@ -326,17 +327,17 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle the fullscreen state of the current window.", }}, - .toggle_window_decorations => comptime &.{.{ + .toggle_window_decorations => comptime if (!builtin.os.tag.isDarwin()) &.{.{ .action = .toggle_window_decorations, .title = "Toggle Window Decorations", .description = "Toggle the window decorations.", - }}, + }} else &.{}, - .toggle_secure_input => comptime &.{.{ + .toggle_secure_input => comptime if (builtin.os.tag.isDarwin()) &.{.{ .action = .toggle_secure_input, .title = "Toggle Secure Input", .description = "Toggle secure input mode.", - }}, + }} else &.{}, .quit => comptime &.{.{ .action = .quit, From baad08243883212133644eaaa90daa8c67258822 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 10:26:18 -0700 Subject: [PATCH 123/642] macOS: command palette selection tweaks --- .../Command Palette/CommandPalette.swift | 38 +++++++++---------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 33b97585a..24677debc 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -45,6 +45,14 @@ struct CommandPaletteView: View { } } + var selectedOption: CommandOption? { + if selectedIndex < filteredOptions.count { + filteredOptions[Int(selectedIndex)] + } else { + filteredOptions.last + } + } + var body: some View { VStack(alignment: .leading, spacing: 0) { CommandPaletteQuery(query: $query) { event in @@ -54,9 +62,7 @@ struct CommandPaletteView: View { case .submit: isPresented = false - if selectedIndex < filteredOptions.count { - filteredOptions[Int(selectedIndex)].action() - } + selectedOption?.action() case .move(.up): if selectedIndex > 0 { @@ -78,8 +84,7 @@ struct CommandPaletteView: View { .padding(.bottom, 4) CommandTable( - options: options, - query: $query, + options: filteredOptions, selectedIndex: $selectedIndex, hoveredOptionID: $hoveredOptionID) { option in isPresented = false @@ -148,23 +153,12 @@ fileprivate struct CommandPaletteQuery: View { fileprivate struct CommandTable: View { var options: [CommandOption] = CommandOption.sampleData - @Binding var query: String @Binding var selectedIndex: UInt @Binding var hoveredOptionID: UUID? var action: (CommandOption) -> Void - // The options that we should show, taking into account any filtering from - // the query. - var filteredOptions: [CommandOption] { - if query.isEmpty { - return options - } else { - return options.filter { $0.title.localizedCaseInsensitiveContains(query) } - } - } - var body: some View { - if filteredOptions.isEmpty { + if options.isEmpty { Text("No matches") .foregroundStyle(.secondary) .padding() @@ -172,10 +166,12 @@ fileprivate struct CommandTable: View { ScrollViewReader { proxy in ScrollView(showsIndicators: false) { VStack(alignment: .leading, spacing: 0) { - ForEach(Array(filteredOptions.enumerated()), id: \.1.id) { index, option in + ForEach(Array(options.enumerated()), id: \.1.id) { index, option in CommandRow( option: option, - isSelected: selectedIndex == index, + isSelected: selectedIndex == index || + (selectedIndex >= options.count && + index == options.count - 1), hoveredID: $hoveredOptionID ) { action(option) @@ -185,10 +181,10 @@ fileprivate struct CommandTable: View { } .frame(height: 200) .onChange(of: selectedIndex) { _ in - guard selectedIndex < filteredOptions.count else { return } + guard selectedIndex < options.count else { return } withAnimation { proxy.scrollTo( - filteredOptions[Int(selectedIndex)].id, + options[Int(selectedIndex)].id, anchor: .center) } } From e33eed0216b49ba4ee8a2d4eacdbca9bada67417 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 10:32:07 -0700 Subject: [PATCH 124/642] macOS: command palette visual tweaks --- .../Command Palette/CommandPalette.swift | 26 ++++++++++++++----- .../TerminalCommandPalette.swift | 2 +- .../Helpers/KeyboardShortcut+Extension.swift | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 24677debc..887ea3464 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -91,10 +91,16 @@ struct CommandPaletteView: View { option.action() } } - .frame(width: 500) - .background(backgroundColor) - .cornerRadius(12) - .shadow(radius: 20) + .frame(maxWidth: 500) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(backgroundColor) + .shadow(color: .black.opacity(0.4), radius: 10, x: 0, y: 10) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(Color.black.opacity(0.1), lineWidth: 1) + ) + ) .padding() } } @@ -179,7 +185,7 @@ fileprivate struct CommandTable: View { } } } - .frame(height: 200) + .frame(maxHeight: 200) .onChange(of: selectedIndex) { _ in guard selectedIndex < options.count else { return } withAnimation { @@ -207,8 +213,14 @@ fileprivate struct CommandRow: View { Spacer() if let shortcut = option.shortcut { Text(shortcut) - .foregroundStyle(.secondary) - .font(.system(size: 12)) + .font(.system(.body, design: .monospaced)) + .kerning(1.5) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + RoundedRectangle(cornerRadius: 6) + .fill(Color.gray.opacity(0.2)) + ) } } .padding(.horizontal, 6) diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 29ce28906..fe23d5bf8 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -41,7 +41,7 @@ struct TerminalCommandPaletteView: View { if isPresented { GeometryReader { geometry in VStack { - Spacer().frame(height: geometry.size.height * 0.1) + Spacer().frame(height: geometry.size.height * 0.05) ResponderChainInjector(responder: surfaceView) .frame(width: 0, height: 0) diff --git a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift index b953f5755..9b5855757 100644 --- a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift +++ b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift @@ -29,7 +29,7 @@ extension KeyboardShortcut: @retroactive CustomStringConvertible { case .leftArrow: keyString = "←" case .rightArrow: keyString = "→" default: - keyString = String(key.character) + keyString = String(key.character.uppercased()) } result.append(keyString) From a732bb272d4c0da8d9210314a3d8993aeea5cb50 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 10:50:34 -0700 Subject: [PATCH 125/642] fix CI --- .../Command Palette/CommandPalette.swift | 16 ++-------------- src/input/command.zig | 2 +- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 887ea3464..09b216a39 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -13,24 +13,12 @@ struct CommandOption: Identifiable, Hashable { func hash(into hasher: inout Hasher) { hasher.combine(id) } - - // Sample data remains the same - static let sampleData: [CommandOption] = [ - .init(title: "assistant: copy code", shortcut: nil, action: {}), - .init(title: "assistant: inline assist", shortcut: "⌃⏎", action: {}), - .init(title: "assistant: insert into editor", shortcut: "⌘<", action: {}), - .init(title: "assistant: new chat", shortcut: nil, action: {}), - .init(title: "assistant: open prompt library", shortcut: nil, action: {}), - .init(title: "assistant: quote selection", shortcut: "⌘>", action: {}), - .init(title: "assistant: show configuration", shortcut: nil, action: {}), - .init(title: "assistant: toggle focus", shortcut: "⌘?", action: {}), - ] } struct CommandPaletteView: View { @Binding var isPresented: Bool var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) - var options: [CommandOption] = CommandOption.sampleData + var options: [CommandOption] @State private var query = "" @State private var selectedIndex: UInt = 0 @State private var hoveredOptionID: UUID? = nil @@ -158,7 +146,7 @@ fileprivate struct CommandPaletteQuery: View { } fileprivate struct CommandTable: View { - var options: [CommandOption] = CommandOption.sampleData + var options: [CommandOption] @Binding var selectedIndex: UInt @Binding var hoveredOptionID: UUID? var action: (CommandOption) -> Void diff --git a/src/input/command.zig b/src/input/command.zig index 017a14a18..a36232d48 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -2,7 +2,7 @@ const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; -const Action = @import("binding.zig").Action; +const Action = @import("Binding.zig").Action; /// A command is a named binding action that can be executed from /// something like a command palette. From 1c62ddffc451e555a3700def5c450acb14bc951e Mon Sep 17 00:00:00 2001 From: Tristan Partin Date: Sat, 19 Apr 2025 09:28:56 -0500 Subject: [PATCH 126/642] apprt/gtk: add menu to new tab button to create splits Closes: https://github.com/ghostty-org/ghostty/discussions/6828 Signed-off-by: Tristan Partin --- po/ca_ES.UTF-8.po | 81 ++++++++------ po/com.mitchellh.ghostty.pot | 72 ++++++------ po/de_DE.UTF-8.po | 73 +++++++------ po/es_BO.UTF-8.po | 81 ++++++++------ po/fr_FR.UTF-8.po | 93 +++++++++------- po/id_ID.UTF-8.po | 88 ++++++++------- po/ja_JP.UTF-8.po | 92 +++++++++------- po/mk_MK.UTF-8.po | 93 +++++++++------- po/nb_NO.UTF-8.po | 76 +++++++------ po/nl_NL.UTF-8.po | 87 ++++++++------- po/pl_PL.UTF-8.po | 103 ++++++++++-------- po/pt_BR.UTF-8.po | 88 ++++++++------- po/ru_RU.UTF-8.po | 101 +++++++++-------- po/tr_TR.UTF-8.po | 73 +++++++------ po/uk_UA.UTF-8.po | 78 +++++++------ po/zh_CN.UTF-8.po | 73 +++++++------ src/apprt/gtk/Window.zig | 19 +++- src/apprt/gtk/gresource.zig | 1 + .../gtk/ui/1.0/menu-headerbar-split_menu.blp | 25 +++++ 19 files changed, 800 insertions(+), 597 deletions(-) create mode 100644 src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 5cbb7efd5..1001e8fe7 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-20 08:07+0100\n" "Last-Translator: Francesc Arpi \n" "Language-Team: \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"S'han trobat un o més errors de configuració. Si us plau, revisa els errors a " -"continuació i torna a carregar la configuració o ignora aquests errors." +"S'han trobat un o més errors de configuració. Si us plau, revisa els errors " +"a continuació i torna a carregar la configuració o ignora aquests errors." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +56,30 @@ msgstr "Ignora" msgid "Reload Configuration" msgstr "Carrega la configuració" +#: 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 "Divideix cap amunt" + +#: 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 "Divideix cap avall" + +#: 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 "Divideix a l'esquerra" + +#: 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 "Divideix a la dreta" + #: 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,33 +111,13 @@ msgstr "Divideix" msgid "Change Title…" msgstr "Canvia el títol…" -#: 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 "Divideix cap amunt" - -#: 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 "Divideix cap avall" - -#: 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 "Divideix a l'esquerra" - -#: 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 "Divideix a la dreta" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nova pestanya" @@ -150,7 +154,7 @@ msgid "Terminal Inspector" msgstr "Inspector de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Sobre Ghostty" @@ -205,10 +209,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copiat al porta-retalls" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tanca" @@ -245,25 +245,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiat al porta-retalls" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menú principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Mostra les pestanyes obertes" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Divideix" + +#: 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." +"⚠️ Estàs executant una versió de depuració de Ghostty! El rendiment es veurà " +"afectat." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "S'ha tornat a carregar la configuració" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Desenvolupadors de Ghostty" diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index 4bf47da53..2b2a2509d 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -54,6 +54,30 @@ msgstr "" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -85,33 +109,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:248 msgid "New Tab" msgstr "" @@ -148,7 +152,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "" @@ -197,10 +201,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "" @@ -237,23 +237,31 @@ msgstr "" msgid "The currently running process in this split will be terminated." msgstr "" -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "" -#: src/apprt/gtk/Window.zig:295 +#: 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 "" -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "" diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 1de7a7b96..bb72bbb2c 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-06 14:57+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" @@ -55,6 +55,30 @@ msgstr "" msgid "Reload Configuration" msgstr "Konfiguration neu laden" +#: 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 "Fenster nach oben teilen" + +#: 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 "Fenster nach unten teilen" + +#: 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 "Fenter nach links teilen" + +#: 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 "Fenster nach rechts teilen" + #: 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,33 +110,13 @@ msgstr "Fenster teilen" msgid "Change Title…" msgstr "Titel bearbeiten…" -#: 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 "Fenster nach oben teilen" - -#: 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 "Fenster nach unten teilen" - -#: 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 "Fenter nach links teilen" - -#: 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 "Fenster nach rechts teilen" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Neuer Tab" @@ -149,7 +153,7 @@ msgid "Terminal Inspector" msgstr "Terminalinspektor" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Über Ghostty" @@ -204,10 +208,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "In die Zwischenablage kopiert" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Schließen" @@ -244,25 +244,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "In die Zwischenablage kopiert" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Hauptmenü" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Offene Tabs einblenden" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Fenster teilen" + +#: 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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Konfiguration wurde neu geladen" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty-Entwickler" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index 339ff54c4..436fd3721 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Se encontraron uno o más errores de configuración. Por favor revise los errores a continuación, " -"y recargue su configuración o ignore estos errores." +"Se encontraron uno o más errores de configuración. Por favor revise los " +"errores a continuación, y recargue su configuración o ignore estos errores." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +56,30 @@ msgstr "Ignorar" msgid "Reload Configuration" msgstr "Recargar configuración" +#: 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 "Dividir arriba" + +#: 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 "Dividir abajo" + +#: 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 "Dividir a la izquierda" + +#: 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 "Dividir a la derecha" + #: 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,33 +111,13 @@ msgstr "Dividir" msgid "Change Title…" msgstr "Cambiar título…" -#: 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 "Dividir arriba" - -#: 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 "Dividir abajo" - -#: 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 "Dividir a la izquierda" - -#: 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 "Dividir a la derecha" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nueva pestaña" @@ -150,7 +154,7 @@ 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:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Acerca de Ghostty" @@ -205,10 +209,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspector de la terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copiado al portapapeles" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Cerrar" @@ -245,23 +245,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado al portapapeles" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menú principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Ver pestañas abiertas" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Dividir" + +#: 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." +msgstr "" +"⚠️ Está ejecutando una versión de depuración de Ghostty. El rendimiento no " +"será óptimo." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Configuración recargada" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Desarrolladores de Ghostty" diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index fc5bfd054..414b07d60 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-22 09:31+0100\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" @@ -43,8 +43,9 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire les erreurs ci-dessous," -"et recharger votre configuration ou bien ignorer ces erreurs." +"Une ou plusieurs erreurs de configuration ont été trouvées. Veuillez lire " +"les erreurs ci-dessous,et recharger votre configuration ou bien ignorer ces " +"erreurs." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +57,30 @@ msgstr "Ignorer" msgid "Reload Configuration" msgstr "Recharger la configuration" +#: 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 "Panneau en haut" + +#: 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 "Panneau en bas" + +#: 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 "Panneau à gauche" + +#: 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 "Panneau à droite" + #: 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,33 +112,13 @@ msgstr "Créer panneau" msgid "Change Title…" msgstr "Changer le titre…" -#: 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 "Panneau en haut" - -#: 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 "Panneau en bas" - -#: 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 "Panneau à gauche" - -#: 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 "Panneau à droite" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nouvel onglet" @@ -150,7 +155,7 @@ msgid "Terminal Inspector" msgstr "Inspecteur de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "À propos de Ghostty" @@ -168,8 +173,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essaie de lire depuis le presse-papiers." -"Le contenu actuel du presse-papiers est affiché ci-dessous." +"Une application essaie de lire depuis le presse-papiers.Le contenu actuel du " +"presse-papiers est affiché ci-dessous." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -186,8 +191,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Une application essaie d'écrire dans le presse-papiers." -"Le contenu actuel du presse-papiers est affiché ci-dessous." +"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 msgid "Warning: Potentially Unsafe Paste" @@ -198,17 +203,13 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Coller ce texte dans le terminal pourrait être dangereux, " -"il semblerait que certaines commandes pourraient être exécutées." +"Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " +"certaines commandes pourraient être exécutées." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspecteur" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copié dans le presse-papiers" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Fermer" @@ -245,24 +246,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copié dans le presse-papiers" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menu principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Voir les onglets ouverts" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Créer panneau" + +#: 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." +"⚠️ Vous utilisez une version de débogage de Ghostty ! Les performances seront " +"dégradées." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Recharger la configuration" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Les développeurs de Ghostty" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index c8b89a89e..1a344da88 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-20 15:19+0700\n" "Last-Translator: Satrio Bayu Aji \n" "Language-Team: Indonesian \n" @@ -42,8 +42,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di bawah ini, " -"dan muat ulang konfigurasi anda atau abaikan kesalahan ini." +"Ditemukan satu atau lebih kesalahan konfigurasi. Silakan tinjau kesalahan di " +"bawah ini, dan muat ulang konfigurasi anda atau abaikan kesalahan ini." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -55,6 +55,30 @@ msgstr "Abaikan" msgid "Reload Configuration" msgstr "Muat ulang konfigurasi" +#: 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 "Belah atas" + +#: 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 "Belah bawah" + +#: 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 "Belah kiri" + +#: 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 "Belah kanan" + #: 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,33 +110,13 @@ msgstr "Belah" msgid "Change Title…" msgstr "Ubah judul…" -#: 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 "Belah atas" - -#: 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 "Belah bawah" - -#: 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 "Belah kiri" - -#: 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 "Belah kanan" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Tab baru" @@ -149,7 +153,7 @@ msgid "Terminal Inspector" msgstr "Inspektur terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Tentang Ghostty" @@ -167,8 +171,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip " -"saat ini ditampilkan di bawah ini." +"Aplikasi sedang mencoba membaca dari papan klip. Isi papan klip saat ini " +"ditampilkan di bawah ini." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -185,8 +189,8 @@ msgid "" "An application is attempting to write to the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Aplikasi sedang mencoba menulis ke papan klip. Isi papan klip " -"saat ini ditampilkan di bawah ini." +"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 msgid "Warning: Potentially Unsafe Paste" @@ -204,10 +208,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspektur terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Disalin ke papan klip" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Tutup" @@ -244,23 +244,33 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Disalin ke papan klip" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menu utama" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Lihat tab terbuka" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Belah" + +#: 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." +msgstr "" +"⚠️ Anda sedang menjalankan versi debug dari Ghostty! Performa akan menurun." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Memuat ulang konfigurasi" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Pengembang Ghostty" diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index e06ec2fbc..93689aa69 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-21 00:08+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" @@ -44,8 +44,8 @@ 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 msgid "Ignore" @@ -57,6 +57,30 @@ msgstr "無視" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,33 +112,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:248 msgid "New Tab" msgstr "新しいタブ" @@ -151,7 +155,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "Ghostty について" @@ -169,8 +173,8 @@ 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 @@ -187,8 +191,8 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -199,17 +203,13 @@ 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 "クリップボードにコピーしました" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "閉じる" @@ -246,23 +246,33 @@ msgstr "タブ内のすべてのターミナルセッションが終了します msgid "The currently running process in this split will be terminated." msgstr "分割ウィンドウ内のすべてのプロセスが終了します。" -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "クリップボードにコピーしました" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "メインメニュー" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "開いているすべてのタブを表示" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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 のデバッグビルドを実行しています! パフォーマンスが低下しています。" +msgstr "" +"⚠️ Ghostty のデバッグビルドを実行しています! パフォーマンスが低下しています。" -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "設定を再読み込みしました" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty 開発者" diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 5552cc6e4..99ecd37bc 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-23 14:17+0100\n" "Last-Translator: Andrej Daskalov \n" "Language-Team: Macedonian\n" @@ -41,7 +41,10 @@ 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 msgid "Ignore" @@ -53,6 +56,30 @@ msgstr "Игнорирај" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -84,33 +111,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:248 msgid "New Tab" msgstr "Ново јазиче" @@ -147,7 +154,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "За Ghostty" @@ -164,7 +171,9 @@ msgstr "Авторизирај пристап до привремена мемо 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 @@ -180,7 +189,9 @@ 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 msgid "Warning: Potentially Unsafe Paste" @@ -190,16 +201,14 @@ msgstr "Предупредување: Потенцијално небезбед msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи изгледа како да ќе се извршат одредени команди." +msgstr "" +"Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи " +"изгледа како да ќе се извршат одредени команди." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Инспектор на терминал" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Копирано во привремена меморија" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Затвори" @@ -236,23 +245,33 @@ msgstr "Сите сесии во ова јазиче ќе бидат преки msgid "The currently running process in this split will be terminated." msgstr "Процесот кој моментално се извршува во оваа поделба ќе биде прекинат." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Копирано во привремена меморија" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Главно мени" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Прегледај отворени јазичиња" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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! Перформансите ќе бидат намалени." +msgstr "" +"⚠️ Извршувате дебаг верзија на Ghostty! Перформансите ќе бидат намалени." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Конфигурацијата е одново вчитана" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Развивачи на Ghostty" diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index bd7c8876a..28bf1df0d 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -10,6 +10,7 @@ msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-04-14 16:25+0200\n" "Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" @@ -45,8 +46,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgå feilene under, " -"og enten last konfigurasjonen din på nytt eller ignorer disse feilene." +"Én eller flere konfigurasjonsfeil ble funnet. Vennligst gjennomgå feilene " +"under, og enten last konfigurasjonen din på nytt eller ignorer disse feilene." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -58,6 +59,30 @@ msgstr "Ignorer" msgid "Reload Configuration" msgstr "Last konfigurasjon på nytt" +#: 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 "Splitt opp" + +#: 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 "Splitt ned" + +#: 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 "Splitt venstre" + +#: 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 "Splitt høyre" + #: 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,33 +114,13 @@ msgstr "Splitt" msgid "Change Title…" msgstr "Endre tittel…" -#: 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 "Splitt opp" - -#: 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 "Splitt ned" - -#: 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 "Splitt venstre" - -#: 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 "Splitt høyre" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Ny fane" @@ -152,7 +157,7 @@ msgid "Terminal Inspector" msgstr "Terminalinspektør" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Om Ghostty" @@ -207,10 +212,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Terminalinspektør" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Kopiert til utklippstavlen" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Lukk" @@ -247,23 +248,32 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Kopiert til utklippstavlen" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Hovedmeny" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Se åpne faner" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Splitt" + +#: 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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Konfigurasjonen ble lastet på nytt" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty-utviklere" diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 6ebea478b..69de9ebe2 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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" "Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" @@ -43,8 +43,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande fouten " -"en herlaad je configuratie of negeer deze fouten." +"Er zijn één of meer configuratiefouten gevonden. Bekijk de onderstaande " +"fouten en herlaad je configuratie of negeer deze fouten." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -56,6 +56,30 @@ msgstr "Negeer" msgid "Reload Configuration" msgstr "Herlaad configuratie" +#: 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 "Splits naar boven" + +#: 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 "Splits naar beneden" + +#: 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 "Splits naar links" + +#: 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 "Splits naar rechts" + #: 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,33 +111,13 @@ msgstr "Splitsen" msgid "Change Title…" msgstr "Wijzig titel…" -#: 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 "Splits naar boven" - -#: 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 "Splits naar beneden" - -#: 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 "Splits naar links" - -#: 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 "Splits naar rechts" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nieuw tabblad" @@ -150,7 +154,7 @@ msgid "Terminal Inspector" msgstr "Terminal inspecteur" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Over Ghostty" @@ -198,17 +202,13 @@ msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." msgstr "" -"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat " -"het lijkt op een commando dat uitgevoerd kan worden." +"Het plakken van deze tekst in de terminal is mogelijk gevaarlijk, omdat het " +"lijkt op een commando dat uitgevoerd kan worden." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: terminal inspecteur" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Gekopieerd naar klembord" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Afsluiten" @@ -243,26 +243,37 @@ msgstr "Alle terminalsessies binnen dit tabblad zullen worden beëindigd." #: src/apprt/gtk/CloseDialog.zig:99 msgid "The currently running process in this split will be terminated." -msgstr "Alle processen die nu draaien in deze splitsing zullen worden beëindigd." +msgstr "" +"Alle processen die nu draaien in deze splitsing zullen worden beëindigd." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Gekopieerd naar klembord" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Hoofdmenu" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Open tabbladen bekijken" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Splitsen" + +#: 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." +"⚠️ Je draait een debug versie van Ghostty! Prestaties zullen minder zijn dan " +"normaal." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "De configuratie is herladen" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty ontwikkelaars" diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index 492326c17..e5de8febc 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-03-18 11:48+0100\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-17 12:15+0100\n" "Last-Translator: Bartosz Sokorski \n" "Language-Team: Polish \n" @@ -45,8 +45,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Znaleziono jeden lub więcej błędów konfiguracji. Sprawdź błędy wylistowane poniżej " -"i przeładuj konfigurację lub zignoruj je." +"Znaleziono jeden lub więcej błędów konfiguracji. Sprawdź błędy wylistowane " +"poniżej i przeładuj konfigurację lub zignoruj je." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -58,6 +58,30 @@ msgstr "Zignoruj" msgid "Reload Configuration" msgstr "Przeładuj konfigurację" +#: 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 "Podziel w górę" + +#: 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 "Podziel w dół" + +#: 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 "Podziel w lewo" + +#: 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 "Podziel w prawo" + #: 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,33 +113,13 @@ msgstr "Podział" msgid "Change Title…" msgstr "Zmień tytuł…" -#: 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 "Podziel w górę" - -#: 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 "Podziel w dół" - -#: 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 "Podziel w lewo" - -#: 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 "Podziel w prawo" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Nowa karta" @@ -152,7 +156,7 @@ msgid "Terminal Inspector" msgstr "Inspektor terminala" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:958 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "O Ghostty" @@ -203,27 +207,6 @@ msgstr "" "Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " "spowodować wykonanie komend." -#: src/apprt/gtk/Window.zig:200 -msgid "Main Menu" -msgstr "Menu główne" - -#: src/apprt/gtk/Window.zig:221 -msgid "View Open Tabs" -msgstr "Zobacz otwarte karty" - -#: src/apprt/gtk/Window.zig:295 -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:725 -msgid "Reloaded the configuration" -msgstr "Przeładowano konfigurację" - -#: src/apprt/gtk/Window.zig:939 -msgid "Ghostty Developers" -msgstr "Twórcy Ghostty" - #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Inspektor terminala Ghostty" @@ -264,6 +247,32 @@ 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:1242 +#: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" msgstr "Skopiowano do schowka" + +#: 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 +#, fuzzy +msgid "New Split" +msgstr "Podział" + +#: 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" diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index f9fadce66..dabacaf12 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -8,11 +8,11 @@ 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-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-28 11:04-0300\n" "Last-Translator: Gustavo Peres \n" -"Language-Team: Brazilian Portuguese \n" +"Language-Team: Brazilian Portuguese \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -45,8 +45,8 @@ msgid "" "One or more configuration errors were found. Please review the errors below, " "and either reload your configuration or ignore these errors." msgstr "" -"Um ou mais erros de configuração encontrados. Por favor revise os erros abaixo, " -"e ou recarregue sua configuração, ou ignore esses erros." +"Um ou mais erros de configuração encontrados. Por favor revise os erros " +"abaixo, e ou recarregue sua configuração, ou ignore esses erros." #: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 msgid "Ignore" @@ -58,6 +58,30 @@ msgstr "Ignorar" msgid "Reload Configuration" msgstr "Recarregar configuração" +#: 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 "Dividir para cima" + +#: 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 "Dividir para baixo" + +#: 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 "Dividir à esquerda" + +#: 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 "Dividir à direita" + #: 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,33 +113,13 @@ msgstr "Dividir" msgid "Change Title…" msgstr "Mudar título…" -#: 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 "Dividir para cima" - -#: 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 "Dividir para baixo" - -#: 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 "Dividir à esquerda" - -#: 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 "Dividir à direita" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" msgstr "Aba" #: 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:248 msgid "New Tab" msgstr "Nova aba" @@ -152,7 +156,7 @@ msgid "Terminal Inspector" msgstr "Inspetor de terminal" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:102 -#: src/apprt/gtk/Window.zig:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Sobre o Ghostty" @@ -170,8 +174,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Uma aplicação está tentando ler da área de transferência. O conteúdo " -"atual da área de transferência está sendo exibido abaixo." +"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " +"da área de transferência está sendo exibido abaixo." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -207,10 +211,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Inspetor de terminal" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Copiado para a área de transferência" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Fechar" @@ -247,23 +247,33 @@ msgstr "Todas as sessões de terminal nessa aba serão finalizadas." msgid "The currently running process in this split will be terminated." msgstr "O processo atual rodando nessa divisão será finalizado." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Copiado para a área de transferência" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Menu Principal" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Visualizar abas abertas" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Dividir" + +#: src/apprt/gtk/Window.zig:312 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." -msgstr "⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." +msgstr "" +"⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Configuração recarregada" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Desenvolvedores Ghostty" diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index a3c21a246..fa00d453e 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.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-03-19 08:28-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-24 00:01+0500\n" "Last-Translator: blackzeshi \n" "Language-Team: Russian \n" @@ -44,9 +44,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 msgid "Ignore" @@ -58,6 +58,30 @@ msgstr "Игнорировать" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -89,33 +113,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:248 msgid "New Tab" msgstr "Новая вкладка" @@ -152,7 +156,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "О Ghostty" @@ -169,9 +173,9 @@ msgstr "Разрешить доступ к буферу обмена" 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 @@ -187,9 +191,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 msgid "Warning: Potentially Unsafe Paste" @@ -199,18 +202,14 @@ msgstr "Внимание! Вставляемые данные могут нан msgid "" "Pasting this text into the terminal may be dangerous as it looks like some " "commands may be executed." -msgstr "" -"Вставка этого текста в терминал может быть опасной. Это выглядит " -"как команды, которые могут быть исполнены." +msgstr "" +"Вставка этого текста в терминал может быть опасной. Это выглядит как " +"команды, которые могут быть исполнены." #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: инспектор терминала" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Скопировано в буфер обмена" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрыть" @@ -247,24 +246,34 @@ msgstr "Все сессии терминала в этой вкладке буд msgid "The currently running process in this split will be terminated." msgstr "Процесс, работающий в этой сплит-области, будет остановлен." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопировано в буфер обмена" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Главное меню" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Просмотреть открытые вкладки" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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! Это может влиять на производительность." +msgstr "" +"⚠️ Вы запустили отладочную сборку Ghostty! Это может влиять на " +"производительность." -#: src/apprt/gtk/Window.zig:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Конфигурация была обновлена" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Разработчики Ghostty" diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index cee17a6a1..b0fe028e5 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-24 22:01+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" @@ -57,6 +57,30 @@ msgstr "Yok Say" msgid "Reload Configuration" msgstr "Yapılandırmayı Yeniden Yükle" +#: 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 "Yukarı Doğru Böl" + +#: 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 "Aşağı Doğru Böl" + +#: 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 "Sola Doğru Böl" + +#: 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 "Sağa Doğru Böl" + #: 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,33 +112,13 @@ msgstr "Böl" msgid "Change Title…" msgstr "Başlığı Değiştir…" -#: 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 "Yukarı Doğru Böl" - -#: 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 "Aşağı Doğru Böl" - -#: 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 "Sola Doğru Böl" - -#: 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 "Sağa Doğru Böl" - #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 msgid "Tab" 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:246 +#: src/apprt/gtk/Window.zig:248 msgid "New Tab" msgstr "Yeni Sekme" @@ -151,7 +155,7 @@ 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:960 +#: src/apprt/gtk/Window.zig:1003 msgid "About Ghostty" msgstr "Ghostty Hakkında" @@ -206,10 +210,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Uçbirim Denetçisi" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Panoya kopyalandı" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Kapat" @@ -246,25 +246,34 @@ 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/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Panoya kopyalandı" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Ana Menü" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Açık Sekmeleri Görüntüle" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +msgid "New Split" +msgstr "Böl" + +#: 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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Yapılandırma yeniden yüklendi" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty Geliştiricileri" diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 662117071..e40b44f1e 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-03-16 20:16+0200\n" "Last-Translator: Danylo Zalizchuk \n" "Language-Team: Ukrainian \n" @@ -44,8 +44,9 @@ 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 msgid "Ignore" @@ -57,6 +58,30 @@ msgstr "Ігнорувати" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -88,33 +113,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:248 msgid "New Tab" msgstr "Нова вкладка" @@ -151,7 +156,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "Про Ghostty" @@ -206,10 +211,6 @@ msgstr "" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty: Інспектор терміналу" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "Скопійовано в буфер обміну" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "Закрити" @@ -247,24 +248,33 @@ msgid "The currently running process in this split will be terminated." msgstr "" "Поточний процес, що виконується в цій розділеній панелі, буде завершено." -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "Скопійовано в буфер обміну" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "Головне меню" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "Переглянути відкриті вкладки" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "Конфігурацію перезавантажено" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Розробники Ghostty" diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index cdb4c3873..239cc2ac2 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-03-19 08:54-0700\n" +"POT-Creation-Date: 2025-04-21 13:01-0500\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -55,6 +55,30 @@ msgstr "忽略" 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.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 msgid "Copy" @@ -86,33 +110,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:248 msgid "New Tab" msgstr "新建标签页" @@ -149,7 +153,7 @@ 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/Window.zig:1003 msgid "About Ghostty" msgstr "关于 Ghostty" @@ -198,10 +202,6 @@ msgstr "将以下内容粘贴至终端内将可能执行有害命令。" msgid "Ghostty: Terminal Inspector" msgstr "Ghostty 终端调试器" -#: src/apprt/gtk/Surface.zig:1243 -msgid "Copied to clipboard" -msgstr "已复制至剪贴板" - #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "关闭" @@ -238,23 +238,32 @@ msgstr "标签页内所有运行中的进程将被终止。" msgid "The currently running process in this split will be terminated." msgstr "分屏内正在运行中的进程将被终止。" -#: src/apprt/gtk/Window.zig:200 +#: src/apprt/gtk/Surface.zig:1243 +msgid "Copied to clipboard" +msgstr "已复制至剪贴板" + +#: src/apprt/gtk/Window.zig:201 msgid "Main Menu" msgstr "主菜单" -#: src/apprt/gtk/Window.zig:221 +#: src/apprt/gtk/Window.zig:222 msgid "View Open Tabs" msgstr "浏览标签页" -#: src/apprt/gtk/Window.zig:295 +#: src/apprt/gtk/Window.zig:249 +#, fuzzy +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:725 +#: src/apprt/gtk/Window.zig:744 msgid "Reloaded the configuration" msgstr "已重新加载配置" -#: src/apprt/gtk/Window.zig:941 +#: src/apprt/gtk/Window.zig:984 msgid "Ghostty Developers" msgstr "Ghostty 开发团队" diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 129c149e7..fded6078a 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -25,6 +25,7 @@ const input = @import("../../input.zig"); const CoreSurface = @import("../../Surface.zig"); const App = @import("App.zig"); +const Builder = @import("Builder.zig"); const Color = configpkg.Config.Color; const Surface = @import("Surface.zig"); const Menu = @import("menu.zig").Menu; @@ -242,12 +243,19 @@ pub fn init(self: *Window, app: *App) !void { } { - const btn = gtk.Button.newFromIconName("tab-new-symbolic"); + const btn = adw.SplitButton.new(); + btn.setIconName("tab-new-symbolic"); btn.as(gtk.Widget).setTooltipText(i18n._("New Tab")); - _ = gtk.Button.signals.clicked.connect( + btn.setDropdownTooltip(i18n._("New Split")); + + var builder = Builder.init("menu-headerbar-split_menu", 1, 0, .blp); + defer builder.deinit(); + btn.setMenuModel(builder.getObject(gio.MenuModel, "menu")); + + _ = adw.SplitButton.signals.clicked.connect( btn, *Window, - gtkTabNewClick, + adwNewTabClick, self, .{}, ); @@ -824,6 +832,11 @@ fn gtkTabNewClick(_: *gtk.Button, self: *Window) callconv(.c) void { self.performBindingAction(.{ .new_tab = {} }); } +/// Create a new surface (tab or split). +fn adwNewTabClick(_: *adw.SplitButton, self: *Window) callconv(.c) void { + self.performBindingAction(.{ .new_tab = {} }); +} + /// Create a new tab from the AdwTabOverview. We can't copy gtkTabNewClick /// because we need to return an AdwTabPage from this function. fn gtkNewTabFromOverview(_: *adw.TabOverview, self: *Window) callconv(.c) *adw.TabPage { diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 64067c199..7ced9fc45 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -75,6 +75,7 @@ pub const VersionedBlueprint = struct { pub const blueprint_files = [_]VersionedBlueprint{ .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" }, .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, + .{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" }, .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" }, .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" }, diff --git a/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp b/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp new file mode 100644 index 000000000..90de02845 --- /dev/null +++ b/src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp @@ -0,0 +1,25 @@ +using Gtk 4.0; + +menu menu { + section { + item { + label: _("Split Up"); + action: "win.split-up"; + } + + item { + label: _("Split Down"); + action: "win.split-down"; + } + + item { + label: _("Split Left"); + action: "win.split-left"; + } + + item { + label: _("Split Right"); + action: "win.split-right"; + } + } +} From e5e9d43d52aabc7826a8efa2e77bf4cc94441d86 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 13:55:35 -0700 Subject: [PATCH 127/642] renderer/opengl: reduce flickering/stretching on resize Fixes #2446 Two separate issues: 1. Ensure that our screen size matches the viewport size when drawFrame is called. By the time drawFrame is called, GTK will have updated the OpenGL context, but our deferred screen size may still be incorrect since we wait for the pty to update the screen size. 2. Do not clear our cells buffer when the screen size changes, instead changing to a mechanism that only clears the buffers when we have over 50% wasted space. Co-authored-by: Andrew de los Reyes --- src/renderer/OpenGL.zig | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index ba9e5d81f..a3a2d8f7e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1779,6 +1779,15 @@ pub fn rebuildCells( } } + // Free up memory, generally in case where surface has shrunk. + // If more than half of the capacity is unused, remove all unused capacity. + if (self.cells.items.len * 2 < self.cells.capacity) { + self.cells.shrinkAndFree(self.alloc, self.cells.items.len); + } + if (self.cells_bg.items.len * 2 < self.cells_bg.capacity) { + self.cells_bg.shrinkAndFree(self.alloc, self.cells_bg.items.len); + } + // Some debug mode safety checks if (std.debug.runtime_safety) { for (self.cells_bg.items) |cell| assert(cell.mode == .bg); @@ -2196,12 +2205,6 @@ pub fn setScreenSize( if (single_threaded_draw) self.draw_mutex.lock(); defer if (single_threaded_draw) self.draw_mutex.unlock(); - // Reset our buffer sizes so that we free memory when the screen shrinks. - // This could be made more clever by only doing this when the screen - // shrinks but the performance cost really isn't that much. - self.cells.clearAndFree(self.alloc); - self.cells_bg.clearAndFree(self.alloc); - // Store our screen size self.size = size; @@ -2338,6 +2341,23 @@ pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { if (comptime is_darwin) _ = ogl.CGLLockContext(cgl_ctx); defer _ = if (comptime is_darwin) ogl.CGLUnlockContext(cgl_ctx); + // If our viewport size doesn't match the saved screen size then + // we need to update it. We rely on this over setScreenSize because + // we can pull it directly from the OpenGL context instead of relying + // on the eventual message. + { + var viewport: [4]gl.c.GLint = undefined; + gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport); + const screen: renderer.ScreenSize = .{ + .width = @intCast(viewport[2]), + .height = @intCast(viewport[3]), + }; + if (!screen.equals(self.size.screen)) { + self.size.screen = screen; + self.deferred_screen_size = .{ .size = self.size }; + } + } + // Draw our terminal cells try self.drawCellProgram(gl_state); From 28404e946b196f0d6e62ad3e602d2426b8fa0b24 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 21 Apr 2025 17:13:00 -0700 Subject: [PATCH 128/642] order commands alphabetically and preserve capitalization --- .../Features/Command Palette/CommandPalette.swift | 2 +- src/input/command.zig | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 09b216a39..943cc7846 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -197,7 +197,7 @@ fileprivate struct CommandRow: View { var body: some View { Button(action: action) { HStack { - Text(option.title.lowercased()) + Text(option.title) Spacer() if let shortcut = option.shortcut { Text(shortcut) diff --git a/src/input/command.zig b/src/input/command.zig index a36232d48..dcfcd0b39 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -38,9 +38,19 @@ pub const Command = struct { .description = self.description, }; } + + /// Implements a comparison function for std.mem.sortUnstable + /// and similar functions. The sorting is defined by Ghostty + /// to be what we prefer. If a caller wants some other sorting, + /// they should do it themselves. + pub fn lessThan(_: void, lhs: Command, rhs: Command) bool { + return std.ascii.orderIgnoreCase(lhs.title, rhs.title) == .lt; + } }; pub const defaults: []const Command = defaults: { + @setEvalBranchQuota(100_000); + var count: usize = 0; for (@typeInfo(Action.Key).@"enum".fields) |field| { const action = @field(Action.Key, field.name); @@ -58,6 +68,8 @@ pub const defaults: []const Command = defaults: { } } + std.mem.sortUnstable(Command, &result, {}, Command.lessThan); + assert(i == count); const final = result; break :defaults &final; From 3e5fe5de9a02e2abf8f7425a7ce70c082a014a13 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 08:33:24 -0700 Subject: [PATCH 129/642] move command filtering into apprt --- include/ghostty.h | 1 + .../TerminalCommandPalette.swift | 12 +++++++++++- src/input/command.zig | 19 ++++++++++--------- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 3fd582077..3db280c93 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -280,6 +280,7 @@ typedef struct { } ghostty_input_trigger_s; typedef struct { + const char* action_key; const char* action; const char* title; const char* description; diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index fe23d5bf8..e0c8435af 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -25,7 +25,17 @@ struct TerminalCommandPaletteView: View { guard let ptr else { return [] } let buffer = UnsafeBufferPointer(start: ptr, count: count) - return Array(buffer).map { c in + return Array(buffer).filter { c in + let key = String(cString: c.action_key) + switch (key) { + case "toggle_tab_overview", + "toggle_maximize", + "toggle_window_decorations": + return false + default: + return true + } + }.map { c in let action = String(cString: c.action) return CommandOption( title: String(cString: c.title), diff --git a/src/input/command.zig b/src/input/command.zig index dcfcd0b39..c757736c7 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -1,5 +1,4 @@ const std = @import("std"); -const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Action = @import("Binding.zig").Action; @@ -23,6 +22,7 @@ pub const Command = struct { /// ghostty_command_s pub const C = extern struct { + action_key: [*:0]const u8, action: [*:0]const u8, title: [*:0]const u8, description: [*:0]const u8, @@ -33,6 +33,7 @@ pub const Command = struct { assert(@inComptime()); return .{ + .action_key = @tagName(self.action), .action = std.fmt.comptimePrint("{s}", .{self.action}), .title = self.title, .description = self.description, @@ -232,11 +233,11 @@ fn actionCommands(action: Action.Key) []const Command { }, }, - .toggle_tab_overview => comptime if (builtin.os.tag == .linux) &.{.{ + .toggle_tab_overview => comptime &.{.{ .action = .toggle_tab_overview, .title = "Toggle Tab Overview", .description = "Toggle the tab overview.", - }} else &.{}, + }}, .prompt_surface_title => comptime &.{.{ .action = .prompt_surface_title, @@ -327,11 +328,11 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Close all windows.", }}, - .toggle_maximize => comptime if (!builtin.os.tag.isDarwin()) &.{.{ + .toggle_maximize => comptime &.{.{ .action = .toggle_maximize, .title = "Toggle Maximize", .description = "Toggle the maximized state of the current window.", - }} else &.{}, + }}, .toggle_fullscreen => comptime &.{.{ .action = .toggle_fullscreen, @@ -339,17 +340,17 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle the fullscreen state of the current window.", }}, - .toggle_window_decorations => comptime if (!builtin.os.tag.isDarwin()) &.{.{ + .toggle_window_decorations => comptime &.{.{ .action = .toggle_window_decorations, .title = "Toggle Window Decorations", .description = "Toggle the window decorations.", - }} else &.{}, + }}, - .toggle_secure_input => comptime if (builtin.os.tag.isDarwin()) &.{.{ + .toggle_secure_input => comptime &.{.{ .action = .toggle_secure_input, .title = "Toggle Secure Input", .description = "Toggle secure input mode.", - }} else &.{}, + }}, .quit => comptime &.{.{ .action = .quit, From 5427b0b507a3b35b29c50f9fb85316b40791a805 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 08:36:12 -0700 Subject: [PATCH 130/642] macOS: add description as hover tooltip --- macos/Sources/Features/Command Palette/CommandPalette.swift | 2 ++ .../Features/Command Palette/TerminalCommandPalette.swift | 1 + 2 files changed, 3 insertions(+) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 943cc7846..cad93aa22 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -3,6 +3,7 @@ import SwiftUI struct CommandOption: Identifiable, Hashable { let id = UUID() let title: String + let description: String? let shortcut: String? let action: () -> Void @@ -222,6 +223,7 @@ fileprivate struct CommandRow: View { ) .cornerRadius(6) } + .help(option.description ?? "") .buttonStyle(PlainButtonStyle()) .onHover { hovering in hoveredID = hovering ? option.id : nil diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index e0c8435af..2e895d4d9 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -39,6 +39,7 @@ struct TerminalCommandPaletteView: View { let action = String(cString: c.action) return CommandOption( title: String(cString: c.title), + description: String(cString: c.description), shortcut: ghosttyConfig.keyboardShortcut(for: action)?.description ) { onAction(action) From ba67c506f28dd9d74aa53b19a592e9fb7db50f9c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 08:54:54 -0700 Subject: [PATCH 131/642] ci: extract translation check to script so we can run standalone --- .github/scripts/check-translations.sh | 14 ++++++++++++++ .github/workflows/test.yml | 14 +------------- 2 files changed, 15 insertions(+), 13 deletions(-) create mode 100755 .github/scripts/check-translations.sh diff --git a/.github/scripts/check-translations.sh b/.github/scripts/check-translations.sh new file mode 100755 index 000000000..18d5cd67b --- /dev/null +++ b/.github/scripts/check-translations.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +old_pot=$(mktemp) +cp po/com.mitchellh.ghostty.pot "$old_pot" +zig build update-translations + +# Compare previous POT to current POT +msgcmp "$old_pot" po/com.mitchellh.ghostty.pot --use-untranslated + +# Compare all other POs to current POT +for f in po/*.po; do + # Ignore untranslated entries + msgcmp --use-untranslated "$f" po/com.mitchellh.ghostty.pot; +done diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a4557703a..ae0861f6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -757,19 +757,7 @@ jobs: skipPush: true useDaemon: false # sometimes fails on short jobs - name: check translations - run: | - old_pot=$(mktemp) - cp po/com.mitchellh.ghostty.pot "$old_pot" - nix develop -c zig build update-translations - - # Compare previous POT to current POT - msgcmp "$old_pot" po/com.mitchellh.ghostty.pot --use-untranslated - - # Compare all other POs to current POT - for f in po/*.po; do - # Ignore untranslated entries - msgcmp --use-untranslated "$f" po/com.mitchellh.ghostty.pot; - done + run: nix develop -c .github/scripts/check-translations.sh blueprint-compiler: if: github.repository == 'ghostty-org/ghostty' From f36729de393d8a1ca1f857d50a3256c61bb11b46 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 08:57:22 -0700 Subject: [PATCH 132/642] mark new strings as untranslated --- po/ca_ES.UTF-8.po | 61 ++++++++++++++++++------------------ po/com.mitchellh.ghostty.pot | 52 +++++++++++++++--------------- po/de_DE.UTF-8.po | 57 +++++++++++++++++---------------- po/es_BO.UTF-8.po | 61 ++++++++++++++++++------------------ po/fr_FR.UTF-8.po | 61 ++++++++++++++++++------------------ po/id_ID.UTF-8.po | 59 +++++++++++++++++----------------- po/ja_JP.UTF-8.po | 59 +++++++++++++++++----------------- po/mk_MK.UTF-8.po | 59 +++++++++++++++++----------------- po/nb_NO.UTF-8.po | 57 +++++++++++++++++---------------- po/nl_NL.UTF-8.po | 61 ++++++++++++++++++------------------ po/pl_PL.UTF-8.po | 57 +++++++++++++++++---------------- po/pt_BR.UTF-8.po | 59 +++++++++++++++++----------------- po/ru_RU.UTF-8.po | 61 ++++++++++++++++++------------------ po/tr_TR.UTF-8.po | 61 ++++++++++++++++++------------------ po/uk_UA.UTF-8.po | 59 +++++++++++++++++----------------- po/zh_CN.UTF-8.po | 57 +++++++++++++++++---------------- 16 files changed, 463 insertions(+), 478 deletions(-) diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 1001e8fe7..712f0d5af 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-20 08:07+0100\n" "Last-Translator: Francesc Arpi \n" "Language-Team: \n" @@ -205,9 +205,32 @@ msgstr "" "Enganxar aquest text al terminal pot ser perillós, ja que sembla que es " "podrien executar algunes ordres." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspector de terminal" +#: 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" @@ -249,30 +272,6 @@ msgstr "El procés actualment en execució en aquesta divisió es tancarà." msgid "Copied to clipboard" msgstr "Copiat al porta-retalls" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Divideix" - -#: 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/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 2b2a2509d..3892d14d8 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-04-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -197,8 +197,29 @@ msgid "" "commands may be executed." msgstr "" -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" +#: 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 "" + +#: src/apprt/gtk/Window.zig:744 +msgid "Reloaded the configuration" +msgstr "" + +#: src/apprt/gtk/Window.zig:984 +msgid "Ghostty Developers" msgstr "" #: src/apprt/gtk/CloseDialog.zig:47 @@ -241,27 +262,6 @@ msgstr "" msgid "Copied to clipboard" 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 "" - -#: src/apprt/gtk/Window.zig:744 -msgid "Reloaded the configuration" -msgstr "" - -#: src/apprt/gtk/Window.zig:984 -msgid "Ghostty Developers" +#: 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 bb72bbb2c..44f3bae39 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-06 14:57+0100\n" "Last-Translator: Robin \n" "Language-Team: German \n" @@ -204,10 +204,33 @@ 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/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" +#: 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" @@ -248,30 +271,6 @@ msgstr "Der aktuell laufende Prozess in diesem geteilten Fenster wird beendet." msgid "Copied to clipboard" msgstr "In die Zwischenablage kopiert" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Fenster teilen" - -#: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" 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" diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index 436fd3721..f3a62748a 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-28 17:46+0200\n" "Last-Translator: Miguel Peredo \n" "Language-Team: Spanish \n" @@ -205,9 +205,32 @@ msgstr "" "Pegar este texto en la terminal puede ser peligroso ya que parece que " "algunos comandos podrían ejecutarse." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspector de la terminal" +#: 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" @@ -249,30 +272,6 @@ msgstr "El proceso actualmente en ejecución en esta división será terminado." msgid "Copied to clipboard" msgstr "Copiado al portapapeles" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Dividir" - -#: 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/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 414b07d60..4db72a23e 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-22 09:31+0100\n" "Last-Translator: Kirwiisp \n" "Language-Team: French \n" @@ -206,9 +206,32 @@ msgstr "" "Coller ce texte dans le terminal pourrait être dangereux, il semblerait que " "certaines commandes pourraient être exécutées." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspecteur" +#: 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" @@ -250,30 +273,6 @@ msgstr "Le processus en cours dans ce panneau va être arrêté." msgid "Copied to clipboard" msgstr "Copié dans le presse-papiers" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Créer panneau" - -#: 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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Inspecteur" diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index 1a344da88..d5204d420 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-20 15:19+0700\n" "Last-Translator: Satrio Bayu Aji \n" "Language-Team: Indonesian \n" @@ -204,9 +204,31 @@ msgstr "" "Menempelkan teks ini ke terminal mungkin berbahaya karena sepertinya " "beberapa perintah mungkin dijalankan." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspektur terminal" +#: 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" @@ -248,29 +270,6 @@ msgstr "Proses yang sedang berjalan dalam belahan ini akan diakhiri." msgid "Copied to clipboard" msgstr "Disalin ke papan klip" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Belah" - -#: 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/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 93689aa69..e6e015f8a 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-21 00:08+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" @@ -206,9 +206,31 @@ msgstr "" "このテキストには実行可能なコマンドが含まれており、ターミナルに貼り付けるのは" "危険な可能性があります。" -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: 端末インスペクター" +#: 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" @@ -250,29 +272,6 @@ msgstr "分割ウィンドウ内のすべてのプロセスが終了します。 msgid "Copied to clipboard" 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 -#, fuzzy -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/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 99ecd37bc..39bb72b91 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-23 14:17+0100\n" "Last-Translator: Andrej Daskalov \n" "Language-Team: Macedonian\n" @@ -205,9 +205,31 @@ msgstr "" "Вметнувањето на овој текст во терминалот може да биде опасно, бидејќи " "изгледа како да ќе се извршат одредени команди." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Инспектор на терминал" +#: 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" @@ -249,29 +271,6 @@ msgstr "Процесот кој моментално се извршува во msgid "Copied to clipboard" 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 -#, fuzzy -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/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 28bf1df0d..ad76eea3d 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-04-14 16:25+0200\n" "Last-Translator: cryptocode \n" "Language-Team: Norwegian Bokmal \n" @@ -208,9 +208,30 @@ 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/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Terminalinspektør" +#: 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 "" + +#: 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" @@ -252,28 +273,6 @@ msgstr "Den kjørende prosessen for denne splitten vil bli avsluttet." msgid "Copied to clipboard" msgstr "Kopiert til utklippstavlen" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Splitt" - -#: 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/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 69de9ebe2..466116352 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-24 15:00+0100\n" "Last-Translator: Nico Geesink \n" "Language-Team: Dutch \n" @@ -205,9 +205,32 @@ 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/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: terminal inspecteur" +#: 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" @@ -250,30 +273,6 @@ msgstr "" msgid "Copied to clipboard" msgstr "Gekopieerd naar klembord" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Splitsen" - -#: 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/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 e5de8febc..22d2cd975 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-17 12:15+0100\n" "Last-Translator: Bartosz Sokorski \n" "Language-Team: Polish \n" @@ -207,9 +207,30 @@ msgstr "" "Wklejenie tego tekstu do terminala może być niebezpieczne, ponieważ może " "spowodować wykonanie komend." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Inspektor terminala Ghostty" +#: 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" @@ -251,28 +272,6 @@ msgstr "Wszyskie trwające procesy w obecnym podziale zostaną zakończone." msgid "Copied to clipboard" msgstr "Skopiowano do schowka" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Podział" - -#: 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/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 dabacaf12..f6d2f26a2 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-28 11:04-0300\n" "Last-Translator: Gustavo Peres \n" "Language-Team: Brazilian Portuguese \n" "Language-Team: Russian \n" @@ -206,9 +206,32 @@ msgstr "" "Вставка этого текста в терминал может быть опасной. Это выглядит как " "команды, которые могут быть исполнены." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: инспектор терминала" +#: 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" @@ -250,30 +273,6 @@ msgstr "Процесс, работающий в этой сплит-област msgid "Copied to clipboard" 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 -#, fuzzy -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/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 b0fe028e5..ac1bfdfc7 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-24 22:01+0300\n" "Last-Translator: Emir SARI \n" "Language-Team: Turkish\n" @@ -206,9 +206,32 @@ msgstr "" "Bu metni uçbirime yapıştırmak tehlikeli olabilir; çünkü bir komut " "yürütülebilecekmiş gibi duruyor." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Uçbirim Denetçisi" +#: 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 "" + +#: 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" @@ -250,30 +273,6 @@ msgstr "Bu bölmedeki şu anda çalışan süreç sonlandırılacaktır." msgid "Copied to clipboard" msgstr "Panoya kopyalandı" -#: 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 -#, fuzzy -msgid "New Split" -msgstr "Böl" - -#: 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/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 e40b44f1e..5a264b537 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-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-03-16 20:16+0200\n" "Last-Translator: Danylo Zalizchuk \n" "Language-Team: Ukrainian \n" @@ -207,9 +207,31 @@ msgstr "" "Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає " "так, ніби деякі команди можуть бути виконані." -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Інспектор терміналу" +#: 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" @@ -252,29 +274,6 @@ msgstr "" msgid "Copied to clipboard" 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 -#, fuzzy -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/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 239cc2ac2..80c3766aa 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-04-21 13:01-0500\n" +"POT-Creation-Date: 2025-04-22 08:57-0700\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -198,9 +198,30 @@ msgid "" "commands may be executed." msgstr "将以下内容粘贴至终端内将可能执行有害命令。" -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty 终端调试器" +#: 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" @@ -242,28 +263,6 @@ msgstr "分屏内正在运行中的进程将被终止。" msgid "Copied to clipboard" 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 -#, fuzzy -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/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty 终端调试器" From ebc169dbaf817d09cf7da6a816fb8506466e830c Mon Sep 17 00:00:00 2001 From: Leorize Date: Wed, 12 Mar 2025 03:19:16 -0500 Subject: [PATCH 133/642] Fix flatpak packaging to a working state This should make testing Flatpak builds a lot easier. To build, enter `flatpak/` directory and run: flatpak-builder --repo=repo builddir com.mitchellh.ghostty.yml alternatively, using org.flatpak.Builder flatpak: flatpak run -p org.flatpak.Builder \ --repo=repo \ builddir \ com.mitchellh.ghostty.yml The resulting flatpak can be installed using flatpak install ./repo com.mitchellh.ghostty Credit of AppStream metadata goes to @yorickpeterse. --- .gitignore | 2 + com.mitchellh.ghostty.yml | 59 ----- dist/linux/com.mitchellh.ghostty.metainfo.xml | 28 +++ flatpak/build-support/check-zig-cache.sh | 108 +++++++++ flatpak/com.mitchellh.ghostty.yml | 113 ++++++++++ flatpak/exceptions.json | 5 + flatpak/zig-packages.json | 206 ++++++++++++++++++ src/build/GhosttyResources.zig | 6 + 8 files changed, 468 insertions(+), 59 deletions(-) delete mode 100644 com.mitchellh.ghostty.yml create mode 100644 dist/linux/com.mitchellh.ghostty.metainfo.xml create mode 100755 flatpak/build-support/check-zig-cache.sh create mode 100644 flatpak/com.mitchellh.ghostty.yml create mode 100644 flatpak/exceptions.json create mode 100644 flatpak/zig-packages.json diff --git a/.gitignore b/.gitignore index f39b0c780..95eb1d5c3 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ zig-out/ example/*.wasm test/ghostty test/cases/**/*.actual.png +flatpak/builddir/ +flatpak/repo/ glad.zip /Box_test.ppm diff --git a/com.mitchellh.ghostty.yml b/com.mitchellh.ghostty.yml deleted file mode 100644 index aa7785b27..000000000 --- a/com.mitchellh.ghostty.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Note: the flatpak build is likely broken right now and is not actively -# maintained. We may completely remove this file in the future. For now, -# we want to keep _trying_ but its something with known issues. -app-id: com.mitchellh.ghostty -runtime: org.gnome.Platform -runtime-version: "43" -sdk: org.gnome.Sdk -default-branch: tip -command: ghostty -build-options: - append-path: /app/tmp/zig - strip: false - no-debuginfo: true -# Note: we have to use cleanup-commands because flatpak-builder doesn't -# run "cleanup" on its own: https://github.com/flatpak/flatpak-builder/issues/14 -cleanup-commands: - - "rm -rf /app/tmp" -finish-args: - # 3D rendering - - --device=dri - # Windowing - - --share=ipc - - --socket=x11 - - --socket=wayland - # Files (we are a terminal so we need all of them) - - --filesystem=host - # So we can escape the sandbox - - --talk-name=org.freedesktop.Flatpak -modules: - # Note: this should be kept in sync with our flake.nix. Over time this - # should stabilize to being a release version and not a nightly. - - name: zig - buildsystem: simple - build-commands: - - mkdir -p /app/tmp/zig - - cp -r ./* /app/tmp/zig - sources: - - type: archive - url: https://ziglang.org/builds/zig-linux-x86_64-0.12.0-dev.141+ddf5859c2.tar.xz - sha256: eaf519b1ec3cb0f3c9bcbc47ead5f50610f9c106279a35b9feb09bed8afc628b - only-arches: - - x86_64 - - type: archive - url: https://ziglang.org/builds/zig-linux-aarch64-0.12.0-dev.141+ddf5859c2.tar.xz - sha256: 4f918ae185a5dc281b5d30be92cd4c36ebd77b8665729c5e2c47a8eeccd243e8 - only-arches: - - aarch64 - - - name: ghostty - buildsystem: simple - build-commands: - - MACH_SDK_PATH="$(pwd)/vendor/mach-sdk" zig build -Doptimize=ReleaseSafe -Dcpu=baseline -Dflatpak=true -Dapp-runtime=gtk --prefix /app - sources: - - type: dir - path: . - skip: - - .flatpak-builder - - zig-cache - - zig-out diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml b/dist/linux/com.mitchellh.ghostty.metainfo.xml new file mode 100644 index 000000000..e504805da --- /dev/null +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml @@ -0,0 +1,28 @@ + + + com.mitchellh.ghostty + com.mitchellh.ghostty.desktop + Ghostty + https://ghostty.org/ + Ghostty is a fast, feature-rich, and cross-platform terminal emulator + MIT + MIT + + + Mitchell Hashimoto + + +

+ Ghostty is a terminal emulator that differentiates itself by being fast, + feature-rich, and native. While there are many excellent terminal + emulators available, they all force you to choose between speed, + features, or native UIs. Ghostty provides all three. +

+
+ + + + https://ghostty.org/docs/install/release-notes/1-0-1 + + +
diff --git a/flatpak/build-support/check-zig-cache.sh b/flatpak/build-support/check-zig-cache.sh new file mode 100755 index 000000000..bea718640 --- /dev/null +++ b/flatpak/build-support/check-zig-cache.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +# +# This script checks if the flatpak/zig-packages.json file is up-to-date. +# If the `--update` flag is passed, it will update all necessary +# files to be up to date. +# +# The files owned by this are: +# +# - flatpak/zig-packages.json +# +# All of these are auto-generated and should not be edited manually. + +# Nothing in this script should fail. +set -eu +set -o pipefail + +WORK_DIR=$(mktemp -d) + +if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then + echo "could not create temp dir" + exit 1 +fi + +function cleanup { + rm -rf "$WORK_DIR" +} + +trap cleanup EXIT + +help() { + echo "" + echo "To fix, please (manually) re-run the script from the repository root," + echo "commit, and submit a PR with the update:" + echo "" + echo " ./flatpak/build-support/check-zig-cache.sh --update" + echo " git add flatpak/zig-packages.json" + echo " git commit -m \"flatpak: update zig-packages.json\"" + echo "" +} + +# Turn Nix's base64 hashes into regular hexadecimal form +decode_hash() { + input=$1 + input=${input#sha256-} + echo "$input" | base64 -d | od -vAn -t x1 | tr -d ' \n' +} + +ROOT="$(realpath "$(dirname "$0")/../../")" +ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json" +BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json" + +if [ ! -f "${BUILD_ZIG_ZON_JSON}" ]; then + echo -e "\nERROR: build.zig.zon2json-lock missing." + help + exit 1 +fi + +if [ -f "${ZIG_PACKAGES_JSON}" ]; then + OLD_HASH=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}') +fi + +while read -r url sha256 dest; do + src_type=archive + sha256=$(decode_hash "$sha256") + git_commit= + if [[ "$url" =~ ^git\+* ]]; then + src_type=git + sha256= + url=${url#git+} + git_commit=${url##*#} + url=${url%%/\?ref*} + url=${url%%#*} + fi + + jq \ + -nec \ + --arg type "$src_type" \ + --arg url "$url" \ + --arg git_commit "$git_commit" \ + --arg dest "$dest" \ + --arg sha256 "$sha256" \ + '{ + type: $type, + url: $url, + commit: $git_commit, + dest: $dest, + sha256: $sha256, + } | with_entries(select(.value != ""))' +done < <(jq -rc 'to_entries[] | [.value.url, .value.hash, "vendor/p/\(.key)"] | @tsv' "$BUILD_ZIG_ZON_JSON") | + jq -s '.' >"$WORK_DIR/zig-packages.json" + +NEW_HASH=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}') + +if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then + echo -e "\nOK: flatpak/zig-packages.json unchanged." + exit 0 +elif [ "${1:-}" != "--update" ]; then + echo -e "\nERROR: flatpak/zig-packages.json needs to be updated." + echo "" + echo " * Old hash: ${OLD_HASH}" + echo " * New hash: ${NEW_HASH}" + help + exit 1 +else + mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON" + echo -e "\nOK: flatpak/zig-packages.json updated." + exit 0 +fi diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml new file mode 100644 index 000000000..074b0e5aa --- /dev/null +++ b/flatpak/com.mitchellh.ghostty.yml @@ -0,0 +1,113 @@ +app-id: com.mitchellh.ghostty +runtime: org.gnome.Platform +runtime-version: "47" +sdk: org.gnome.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.ziglang +default-branch: tip +command: ghostty +finish-args: + # 3D rendering + - --device=dri + # Windowing + - --share=ipc + - --socket=fallback-x11 + - --socket=wayland + # Allow user to specify additional config files in home by default + - --filesystem=home:ro + # So we can escape the sandbox + - --talk-name=org.freedesktop.Flatpak +cleanup: + - /include + - /lib/girepository-1.0 + - /lib/pkgconfig + - /share/gir-1.0 + - /share/pkgconfig + - /share/vala + - '*.la' + - '*.a' + - '*.so' + +modules: + - name: bzip2-redirect + buildsystem: simple + build-commands: + - install -dm755 /app/lib + - echo "INPUT(libbz2.so)" > /app/lib/libbzip2.so + + - name: blueprint-compiler + buildsystem: meson + cleanup: + - '*' + sources: + - type: git + url: https://gitlab.gnome.org/jwestman/blueprint-compiler.git + tag: v0.16.0 + commit: 04ef0944db56ab01307a29aaa7303df6067cb3c0 + x-checker-data: + type: git + tag-pattern: ^v([\d.]+)$ + + - name: gtk4-layer-shell + buildsystem: meson + sources: + - type: git + url: https://github.com/wmww/gtk4-layer-shell.git + tag: v1.1.0 + commit: 93550245220cdc514be4701b517acd374a86acc2 + x-checker-data: + type: git + tag-pattern: ^v([\d.]+)$ + + - name: pandoc + buildsystem: simple + cleanup: + - '*' + build-commands: + - install -Dm755 bin/pandoc /app/bin/pandoc + sources: + - type: archive + sha256: d04c95c138202f87d6b00ac19aa3dd874c681f60a9feb3b55c74f764d6d1a17d + url: https://github.com/jgm/pandoc/releases/download/3.6.3/pandoc-3.6.3-linux-amd64.tar.gz + only-arches: [x86_64] + x-checker-data: + type: json + url: https://api.github.com/repos/jgm/pandoc/releases/latest + url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-amd64.tar.gz") + | .browser_download_url + version-query: .tag_name + - type: archive + sha256: 4e774cb1bdb6e56bc55b8eb79200bd9aa6a39905a04ecda7267f5149116f0881 + url: https://github.com/jgm/pandoc/releases/download/3.6.3/pandoc-3.6.3-linux-arm64.tar.gz + only-arches: [aarch64] + x-checker-data: + type: json + url: https://api.github.com/repos/jgm/pandoc/releases/latest + url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-arm64.tar.gz") + | .browser_download_url + version-query: .tag_name + + - name: ghostty + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/ziglang + build-commands: + - zig build + -Doptimize=ReleaseFast + -Dcpu=baseline + -Dflatpak=true + -fno-sys=oniguruma + --prefix /app + --search-prefix /app + --system $PWD/vendor/p + sources: + - type: dir + path: .. + skip: + - flatpak/.flatpak-builder + - flatpak/builddir + - flatpak/repo + - zig-cache + - zig-out + + - zig-packages.json diff --git a/flatpak/exceptions.json b/flatpak/exceptions.json new file mode 100644 index 000000000..ad86ccd14 --- /dev/null +++ b/flatpak/exceptions.json @@ -0,0 +1,5 @@ +{ + "com.mitchellh.ghostty": [ + "finish-args-flatpak-spawn-access" + ] +} diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json new file mode 100644 index 000000000..712f0adab --- /dev/null +++ b/flatpak/zig-packages.json @@ -0,0 +1,206 @@ +[ + { + "type": "archive", + "url": "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz", + "dest": "vendor/p/N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ", + "sha256": "6cca98943d1a990766cef61077c09aff5938063fe17a1efe1228e5412b6d6ad9" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz", + "dest": "vendor/p/N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v", + "sha256": "3ba2dd92158718acec5caaf1a716043b5aa055c27b081d914af3ccb40dce8a55" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz", + "dest": "vendor/p/N-V-__8AAKLKpwC4H27Ps_0iL3bPkQb-z6ZVSrB-x_3EEkub", + "sha256": "427201f5d5151670d05c1f5b45bef5dda1f2e7dd971ef54f0feaaa7ffd2ab90c" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/gettext-0.24.tar.gz", + "dest": "vendor/p/N-V-__8AADcZkgn4cMhTUpIz6mShCKyqqB-NBtf_S2bHaTC-", + "sha256": "c918503d593d70daf4844d175a13d816afacb667c06fba1ec9dcd5002c1518b7" + }, + { + "type": "archive", + "url": "https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz", + "dest": "vendor/p/N-V-__8AAMrJSwAUGb9-vTzkNR-5LXS81MR__ZRVfF3tWgG6", + "sha256": "3373755d402531e6c1a395f53f2fbd6318ca5e067a79a72a59109b526c0b290a" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz", + "dest": "vendor/p/N-V-__8AABzkUgISeKGgXAzgtutgJsZc0-kkeqBBscJgMkvy", + "sha256": "14a2edbb509cb3e51a9a53e3f5e435dbf5971604b4b833e63e6076e8c0a997b5" + }, + { + "type": "archive", + "url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst", + "dest": "vendor/p/gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR", + "sha256": "85672997459ddd7c9d4fe458efe548a315cf842cde95ed48a7be984a1f8a98b2" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz", + "dest": "vendor/p/N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr", + "sha256": "98284281260a5eef5b4f63a55f16c4bf6a788a1020a6db037ecb0f71fa336988" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz", + "dest": "vendor/p/N-V-__8AAG02ugUcWec-Ndp-i7JTsJ0dgF8nnJRUInkGLG7G", + "sha256": "f16351bafe214725fe2c1d5b59f0d93e49905a4b247899fb90d70cff953a2b9b" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz", + "dest": "vendor/p/N-V-__8AAGmZhABbsPJLfbqrh6JTHsXhY6qCaLAQyx25e0XE", + "sha256": "87d4f8893ef4e08f224973608ffebf94268a81380ba79c12e8841968c80aa212" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", + "dest": "vendor/p/N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3", + "sha256": "a05fd01e04cf11ab781e28387c621d2e420f1e6044c8e27a25e603ea99ef7860" + }, + { + "type": "archive", + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", + "dest": "vendor/p/N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6", + "sha256": "9ce907df531041dd8f3dd8d5a51c41c4d4167d1f44c60ea7445fca1e04b3ddc3" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz", + "dest": "vendor/p/N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD", + "sha256": "fecc95b46cf05e8e3fc8a414750e0ba5aad00d89e9fdf175e94ff041caf1a03a" + }, + { + "type": "archive", + "url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz", + "dest": "vendor/p/libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz", + "sha256": "a0a66a03d77bf631e7a7f1eca89590137dc57e7e447b91b85679507a942e638a" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz", + "dest": "vendor/p/N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK", + "sha256": "6c28059e2e3eeb42b5b4b16489e3916a6346c1095a74fee3bc65cdc5d89a6215" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz", + "dest": "vendor/p/N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c", + "sha256": "001aa1202e78448f4c0bf1a48c76e556876b36f16d92ce3207eccfd61d99f2a0" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz", + "dest": "vendor/p/N-V-__8AADYiAAB_80AWnH1AxXC0tql9thT-R-DYO1gBqTLc", + "sha256": "55e83b16d091082502bf149bf457f31f42092c5982650e3ffbae7b48871bf11a" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz", + "dest": "vendor/p/N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs", + "sha256": "5c58ba214acd8e6bca3426dc08b022c46a8dd997b29a1b3e28badf71c20df441" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz", + "dest": "vendor/p/N-V-__8AAPlZGwBEa-gxrcypGBZ2R8Bse4JYSfo_ul8i2jlG", + "sha256": "2ac6497cc8d61a8d31093e47addb8c9b2c45b16b0705bb334a835b6423c318df" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz", + "dest": "vendor/p/N-V-__8AANb6pwD7O1WG6L5nvD_rNMvnSc9Cpg1ijSlTYywv", + "sha256": "b52b6fcfc45e7fa69b1f06a1362c155473444e2cc09995556b156c53ba6657e3" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz", + "dest": "vendor/p/N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH", + "sha256": "ffc668a310e77607d393f3c18b32715f223da1eac4c4d6e0579a11df8e6b59cf" + }, + { + "type": "git", + "url": "https://github.com/rockorager/libvaxis", + "commit": "1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23", + "dest": "vendor/p/vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz", + "dest": "vendor/p/N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t", + "sha256": "ea4191d68e437677e51f3aacde27829810144e931d397a327dc6035e2c39c50d" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz", + "dest": "vendor/p/N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S", + "sha256": "5cedcadde81b75e60f23e5e83b5dd2b8eb4efb9f8f79bd7a347d148aeb0530f8" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", + "dest": "vendor/p/N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs", + "sha256": "9e4cd20abe96e6c4c6ede9c3057108860126e7be2e2c3e35515476c250be1c13" + }, + { + "type": "archive", + "url": "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz", + "dest": "vendor/p/z2d-0.6.0-j5P_HvLdCABu-dXpCeRM7Uk4m16vULg1980lMNCQj4_C", + "sha256": "3c429549467ab5e45b0f28453d64a2b8149ee34a60af4915462bc863c0a7f074" + }, + { + "type": "archive", + "url": "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz", + "dest": "vendor/p/zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9", + "sha256": "de7ba535077fe2b678a5a7972585f002588d37244db08397feadf3d4907c0bb2" + }, + { + "type": "git", + "url": "https://codeberg.org/atman/zg", + "commit": "4a002763419a34d61dcbb1f415821b83b9bf8ddc", + "dest": "vendor/p/zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz", + "dest": "vendor/p/N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ", + "sha256": "7f235e0956c2f5401a28963a261019953d00e3bf4cfc029830f2161196c3583d" + }, + { + "type": "archive", + "url": "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz", + "dest": "vendor/p/zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt", + "sha256": "ce7d6d47ac614a60e56b8509dedf869e2e0d8b747c75e48aded11eec31b3357c" + }, + { + "type": "archive", + "url": "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz", + "dest": "vendor/p/wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy", + "sha256": "13bec6675e403d86db3b55b39ae262f1e1bdfe24056dcd82824341c6308b5219" + }, + { + "type": "git", + "url": "https://github.com/TUSF/zigimg", + "commit": "31268548fe3276c0e95f318a6c0d2ab10565b58d", + "dest": "vendor/p/zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz", + "dest": "vendor/p/ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf", + "sha256": "72c7bdf3e16df105235fe3fcf32c987dac49389190f4ced89b0ee31710f3f3d9" + }, + { + "type": "archive", + "url": "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz", + "dest": "vendor/p/N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o", + "sha256": "17e88863f3600672ab49182f217281b6fc4d3c762bde361935e436a95214d05c" + } +] diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 9d463bf7d..3d6b99a34 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -202,6 +202,12 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { "share/applications/com.mitchellh.ghostty.desktop", ).step); + // AppStream metainfo so that application has rich metadata within app stores + try steps.append(&b.addInstallFile( + b.path("dist/linux/com.mitchellh.ghostty.metainfo.xml"), + "share/metainfo/com.mitchellh.ghostty.metainfo.xml", + ).step); + // Right click menu action for Plasma desktop try steps.append(&b.addInstallFile( b.path("dist/linux/ghostty_dolphin.desktop"), From 3232cfe13891c23538a04c75ddd05dec3ee67d84 Mon Sep 17 00:00:00 2001 From: Leorize Date: Wed, 12 Mar 2025 09:33:40 -0500 Subject: [PATCH 134/642] flatpak: keep debug info for ghostty itself Flatpak will strip them out on its own into an extension package, useful for debugging --- flatpak/com.mitchellh.ghostty.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml index 074b0e5aa..1cf45d0df 100644 --- a/flatpak/com.mitchellh.ghostty.yml +++ b/flatpak/com.mitchellh.ghostty.yml @@ -96,6 +96,7 @@ modules: -Doptimize=ReleaseFast -Dcpu=baseline -Dflatpak=true + -Dstrip=false -fno-sys=oniguruma --prefix /app --search-prefix /app From 9de1aadbabb30e9e2fb18052d6201c4b5c678141 Mon Sep 17 00:00:00 2001 From: Leorize Date: Wed, 12 Mar 2025 11:31:43 -0500 Subject: [PATCH 135/642] flatpak: add development variant This variant is built in Debug mode and is given a different desktop file so it could be installed side-by-side with regular Ghostty. --- flatpak/com.mitchellh.ghostty.Devel.yml | 63 +++++++++++++++++++++++ flatpak/com.mitchellh.ghostty.yml | 58 +--------------------- flatpak/dependencies.yml | 66 +++++++++++++++++++++++++ 3 files changed, 130 insertions(+), 57 deletions(-) create mode 100644 flatpak/com.mitchellh.ghostty.Devel.yml create mode 100644 flatpak/dependencies.yml diff --git a/flatpak/com.mitchellh.ghostty.Devel.yml b/flatpak/com.mitchellh.ghostty.Devel.yml new file mode 100644 index 000000000..b5e4919db --- /dev/null +++ b/flatpak/com.mitchellh.ghostty.Devel.yml @@ -0,0 +1,63 @@ +app-id: com.mitchellh.ghostty.Devel +runtime: org.gnome.Platform +runtime-version: "47" +sdk: org.gnome.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.ziglang +default-branch: tip +command: ghostty +# Integrate the rename into zig build, maybe? +rename-desktop-file: com.mitchellh.ghostty.desktop +rename-appdata-file: com.mitchellh.ghostty.metainfo.xml +rename-icon: com.mitchellh.ghostty +desktop-file-name-suffix: " (Devel)" +finish-args: + # 3D rendering + - --device=dri + # Windowing + - --share=ipc + - --socket=fallback-x11 + - --socket=wayland + # Allow user to specify additional config files in home by default + - --filesystem=home:ro + # So we can escape the sandbox + - --talk-name=org.freedesktop.Flatpak +cleanup: + - /include + - /lib/girepository-1.0 + - /lib/pkgconfig + - /share/gir-1.0 + - /share/pkgconfig + - /share/vala + - '*.la' + - '*.a' + - '*.so' + +modules: + - dependencies.yml + + - name: ghostty + buildsystem: simple + build-options: + append-path: /usr/lib/sdk/ziglang + build-commands: + - zig build + -Doptimize=Debug + -Dcpu=baseline + -Dflatpak=true + -Dstrip=false + -fno-sys=oniguruma + --prefix /app + --search-prefix /app + --system $PWD/vendor/p + sources: + - type: dir + path: .. + skip: + - flatpak/.flatpak-builder + - flatpak/builddir + - flatpak/repo + - zig-cache + - zig-out + + - zig-packages.json diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml index 1cf45d0df..49782c269 100644 --- a/flatpak/com.mitchellh.ghostty.yml +++ b/flatpak/com.mitchellh.ghostty.yml @@ -29,63 +29,7 @@ cleanup: - '*.so' modules: - - name: bzip2-redirect - buildsystem: simple - build-commands: - - install -dm755 /app/lib - - echo "INPUT(libbz2.so)" > /app/lib/libbzip2.so - - - name: blueprint-compiler - buildsystem: meson - cleanup: - - '*' - sources: - - type: git - url: https://gitlab.gnome.org/jwestman/blueprint-compiler.git - tag: v0.16.0 - commit: 04ef0944db56ab01307a29aaa7303df6067cb3c0 - x-checker-data: - type: git - tag-pattern: ^v([\d.]+)$ - - - name: gtk4-layer-shell - buildsystem: meson - sources: - - type: git - url: https://github.com/wmww/gtk4-layer-shell.git - tag: v1.1.0 - commit: 93550245220cdc514be4701b517acd374a86acc2 - x-checker-data: - type: git - tag-pattern: ^v([\d.]+)$ - - - name: pandoc - buildsystem: simple - cleanup: - - '*' - build-commands: - - install -Dm755 bin/pandoc /app/bin/pandoc - sources: - - type: archive - sha256: d04c95c138202f87d6b00ac19aa3dd874c681f60a9feb3b55c74f764d6d1a17d - url: https://github.com/jgm/pandoc/releases/download/3.6.3/pandoc-3.6.3-linux-amd64.tar.gz - only-arches: [x86_64] - x-checker-data: - type: json - url: https://api.github.com/repos/jgm/pandoc/releases/latest - url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-amd64.tar.gz") - | .browser_download_url - version-query: .tag_name - - type: archive - sha256: 4e774cb1bdb6e56bc55b8eb79200bd9aa6a39905a04ecda7267f5149116f0881 - url: https://github.com/jgm/pandoc/releases/download/3.6.3/pandoc-3.6.3-linux-arm64.tar.gz - only-arches: [aarch64] - x-checker-data: - type: json - url: https://api.github.com/repos/jgm/pandoc/releases/latest - url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-arm64.tar.gz") - | .browser_download_url - version-query: .tag_name + - dependencies.yml - name: ghostty buildsystem: simple diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml new file mode 100644 index 000000000..e6e895d93 --- /dev/null +++ b/flatpak/dependencies.yml @@ -0,0 +1,66 @@ +name: dependencies-meta +buildsystem: simple +build-commands: + - true +modules: + - name: bzip2-redirect + buildsystem: simple + build-commands: + - install -Dm644 libbzip2.so /app/lib/libbzip2.so + sources: + - type: inline + contents: INPUT(libbz2.so) + dest-filename: libbzip2.so + + - name: blueprint-compiler + buildsystem: meson + cleanup: + - '*' + sources: + - type: git + url: https://gitlab.gnome.org/jwestman/blueprint-compiler.git + tag: v0.16.0 + commit: 04ef0944db56ab01307a29aaa7303df6067cb3c0 + x-checker-data: + type: git + tag-pattern: ^v([\d.]+)$ + + - name: gtk4-layer-shell + buildsystem: meson + sources: + - type: git + url: https://github.com/wmww/gtk4-layer-shell.git + tag: v1.1.0 + commit: 93550245220cdc514be4701b517acd374a86acc2 + x-checker-data: + type: git + tag-pattern: ^v([\d.]+)$ + + - name: pandoc + buildsystem: simple + cleanup: + - '*' + build-commands: + - install -Dm755 bin/pandoc /app/bin/pandoc + sources: + - type: archive + sha256: d04c95c138202f87d6b00ac19aa3dd874c681f60a9feb3b55c74f764d6d1a17d + url: https://github.com/jgm/pandoc/releases/download/3.6.3/pandoc-3.6.3-linux-amd64.tar.gz + only-arches: [x86_64] + x-checker-data: + type: json + url: https://api.github.com/repos/jgm/pandoc/releases/latest + url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-amd64.tar.gz") + | .browser_download_url + version-query: .tag_name + - type: archive + sha256: 4e774cb1bdb6e56bc55b8eb79200bd9aa6a39905a04ecda7267f5149116f0881 + url: https://github.com/jgm/pandoc/releases/download/3.6.3/pandoc-3.6.3-linux-arm64.tar.gz + only-arches: [aarch64] + x-checker-data: + type: json + url: https://api.github.com/repos/jgm/pandoc/releases/latest + url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-arm64.tar.gz") + | .browser_download_url + version-query: .tag_name + From 0473b0c3f49ceeffef1a40ed32646bb7a36edac4 Mon Sep 17 00:00:00 2001 From: Leorize Date: Wed, 12 Mar 2025 18:47:00 -0500 Subject: [PATCH 136/642] metainfo: update with extra data * Added URLs to more resources * Fixed developer ID * Added device compatibility information --- dist/linux/com.mitchellh.ghostty.metainfo.xml | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/dist/linux/com.mitchellh.ghostty.metainfo.xml b/dist/linux/com.mitchellh.ghostty.metainfo.xml index e504805da..0424d3a09 100644 --- a/dist/linux/com.mitchellh.ghostty.metainfo.xml +++ b/dist/linux/com.mitchellh.ghostty.metainfo.xml @@ -3,14 +3,21 @@ com.mitchellh.ghostty com.mitchellh.ghostty.desktop Ghostty - https://ghostty.org/ + https://ghostty.org + https://ghostty.org/docs + https://github.com/ghostty-org/ghostty/discussions + https://ghostty.org/docs/help + https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md + https://github.com/ghostty-org/ghostty/blob/main/po/README_TRANSLATORS.md + https://github.com/ghostty-org/ghostty Ghostty is a fast, feature-rich, and cross-platform terminal emulator MIT MIT - + Mitchell Hashimoto + m@mitchellh.com

Ghostty is a terminal emulator that differentiates itself by being fast, @@ -19,8 +26,32 @@ features, or native UIs. Ghostty provides all three.

+ + keyboard + pointing + + + + 360 + + com.mitchellh.ghostty + + + https://ghostty.org/docs/install/release-notes/1-0-1 From 7c1e68293e3538522ee48b7083d64187a2a494cc Mon Sep 17 00:00:00 2001 From: Leorize Date: Wed, 12 Mar 2025 20:37:33 -0500 Subject: [PATCH 137/642] flatpak: use archive for gtk4-layer-shell --- flatpak/dependencies.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml index e6e895d93..429526c7c 100644 --- a/flatpak/dependencies.yml +++ b/flatpak/dependencies.yml @@ -28,13 +28,12 @@ modules: - name: gtk4-layer-shell buildsystem: meson sources: - - type: git - url: https://github.com/wmww/gtk4-layer-shell.git - tag: v1.1.0 - commit: 93550245220cdc514be4701b517acd374a86acc2 - x-checker-data: - type: git - tag-pattern: ^v([\d.]+)$ + # no x-checker-data since this should be synchronized with Nix + # + # TODO: Automate this with check-zig-cache.sh + - type: archive + url: https://github.com/wmww/gtk4-layer-shell/archive/refs/tags/v1.1.0.tar.gz + sha256: 98284281260a5eef5b4f63a55f16c4bf6a788a1020a6db037ecb0f71fa336988 - name: pandoc buildsystem: simple From 3e81006eaa10cbbd522cdb476087441dee6442af Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 10:37:27 -0700 Subject: [PATCH 138/642] prettier --- flatpak/com.mitchellh.ghostty.Devel.yml | 6 +++--- flatpak/com.mitchellh.ghostty.yml | 6 +++--- flatpak/dependencies.yml | 11 ++++++----- flatpak/exceptions.json | 4 +--- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/flatpak/com.mitchellh.ghostty.Devel.yml b/flatpak/com.mitchellh.ghostty.Devel.yml index b5e4919db..a939fda9a 100644 --- a/flatpak/com.mitchellh.ghostty.Devel.yml +++ b/flatpak/com.mitchellh.ghostty.Devel.yml @@ -29,9 +29,9 @@ cleanup: - /share/gir-1.0 - /share/pkgconfig - /share/vala - - '*.la' - - '*.a' - - '*.so' + - "*.la" + - "*.a" + - "*.so" modules: - dependencies.yml diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml index 49782c269..6f387481c 100644 --- a/flatpak/com.mitchellh.ghostty.yml +++ b/flatpak/com.mitchellh.ghostty.yml @@ -24,9 +24,9 @@ cleanup: - /share/gir-1.0 - /share/pkgconfig - /share/vala - - '*.la' - - '*.a' - - '*.so' + - "*.la" + - "*.a" + - "*.so" modules: - dependencies.yml diff --git a/flatpak/dependencies.yml b/flatpak/dependencies.yml index 429526c7c..efb5851e9 100644 --- a/flatpak/dependencies.yml +++ b/flatpak/dependencies.yml @@ -15,7 +15,7 @@ modules: - name: blueprint-compiler buildsystem: meson cleanup: - - '*' + - "*" sources: - type: git url: https://gitlab.gnome.org/jwestman/blueprint-compiler.git @@ -38,7 +38,7 @@ modules: - name: pandoc buildsystem: simple cleanup: - - '*' + - "*" build-commands: - install -Dm755 bin/pandoc /app/bin/pandoc sources: @@ -49,7 +49,8 @@ modules: x-checker-data: type: json url: https://api.github.com/repos/jgm/pandoc/releases/latest - url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-amd64.tar.gz") + url-query: + .assets[] | select(.name=="pandoc-" + $version + "-linux-amd64.tar.gz") | .browser_download_url version-query: .tag_name - type: archive @@ -59,7 +60,7 @@ modules: x-checker-data: type: json url: https://api.github.com/repos/jgm/pandoc/releases/latest - url-query: .assets[] | select(.name=="pandoc-" + $version + "-linux-arm64.tar.gz") + url-query: + .assets[] | select(.name=="pandoc-" + $version + "-linux-arm64.tar.gz") | .browser_download_url version-query: .tag_name - diff --git a/flatpak/exceptions.json b/flatpak/exceptions.json index ad86ccd14..176e8c320 100644 --- a/flatpak/exceptions.json +++ b/flatpak/exceptions.json @@ -1,5 +1,3 @@ { - "com.mitchellh.ghostty": [ - "finish-args-flatpak-spawn-access" - ] + "com.mitchellh.ghostty": ["finish-args-flatpak-spawn-access"] } From d7256c71c428b98580c7d3f1ee3206ad8e22790a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 10:35:48 -0700 Subject: [PATCH 139/642] ci: flatpak --- .github/workflows/test.yml | 57 ++++++++++++++++++++++++++++++++++++-- nix/devShell.nix | 4 +++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ae0861f6d..f04249248 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,6 +21,8 @@ jobs: - build-macos-matrix - build-windows - build-windows-cross + - flatpak-check-zig-cache + - flatpak - test - test-gtk - test-sentry-linux @@ -734,7 +736,7 @@ jobs: translations: if: github.repository == 'ghostty-org/ghostty' - runs-on: namespace-profile-ghostty-sm + runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache @@ -761,7 +763,7 @@ jobs: blueprint-compiler: if: github.repository == 'ghostty-org/ghostty' - runs-on: namespace-profile-ghostty-sm + runs-on: namespace-profile-ghostty-xsm timeout-minutes: 60 env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache @@ -851,3 +853,54 @@ jobs: file: dist/src/build/docker/debian/Dockerfile build-args: | DISTRO_VERSION=12 + + flatpak-check-zig-cache: + if: github.repository == 'ghostty-org/ghostty' + runs-on: namespace-profile-ghostty-xsm + env: + ZIG_LOCAL_CACHE_DIR: /zig/local-cache + ZIG_GLOBAL_CACHE_DIR: /zig/global-cache + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Setup Cache + uses: namespacelabs/nscloud-cache-action@v1.2.0 + with: + path: | + /nix + /zig + - name: Setup Nix + uses: cachix/install-nix-action@v30 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v15 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + useDaemon: false # sometimes fails on short jobs + - name: Check Flatpak Zig Dependencies + run: nix develop -c ./flatpak/build-support/check-zig-cache.sh + + flatpak: + if: github.repository == 'ghostty-org/ghostty' + name: "Flatpak" + container: + image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47 + options: --privileged + strategy: + matrix: + variant: + - arch: x86_64 + runner: namespace-profile-ghostty-md + - arch: aarch64 + runner: namespace-profile-ghostty-md-arm64 + runs-on: ${{ matrix.variant.runner }} + steps: + - uses: actions/checkout@v4 + - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 + with: + bundle: com.mitchellh.ghostty + manifest-path: flatpak/com.mitchellh.ghostty.yml + cache-key: flatpak-builder-${{ github.sha }} + arch: ${{ matrix.variant.arch }} + verbose: true diff --git a/nix/devShell.nix b/nix/devShell.nix index 5b69f882b..498102ef4 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -3,6 +3,8 @@ lib, stdenv, bashInteractive, + appstream, + flatpak-builder, gdb, #, glxinfo # unused ncurses, @@ -128,6 +130,8 @@ in # build only has the qemu-system files. qemu + appstream + flatpak-builder gdb snapcraft valgrind From 946cf5a37543c2ca2bb1d019c6ce384c56e9d460 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 10:56:34 -0700 Subject: [PATCH 140/642] update flatpak build hash --- .github/workflows/test.yml | 1 + flatpak/zig-packages.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f04249248..0f5e65925 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -888,6 +888,7 @@ jobs: image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-47 options: --privileged strategy: + fail-fast: false matrix: variant: - arch: x86_64 diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 712f0adab..efe022a45 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8650079de477e80a5983646e3e4d24cda1dbaefa.tar.gz", - "dest": "vendor/p/N-V-__8AADk6LwSAbK3OMyGiadf6aeyztHNV4-zKaLy6IZa6", - "sha256": "9ce907df531041dd8f3dd8d5a51c41c4d4167d1f44c60ea7445fca1e04b3ddc3" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz", + "dest": "vendor/p/N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC", + "sha256": "495bd223c829d4034053020ca7fbfefd9522399e263f2e27407971cd3848d9fb" }, { "type": "archive", From e6c2105a2b156d036c701885bf0d6fa1759e61ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 22 Apr 2025 12:00:58 -0700 Subject: [PATCH 141/642] ci: fix up flatpak deps --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f5e65925..e294d0565 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -896,6 +896,7 @@ jobs: - arch: aarch64 runner: namespace-profile-ghostty-md-arm64 runs-on: ${{ matrix.variant.runner }} + needs: [flatpak-check-zig-cache, test] steps: - uses: actions/checkout@v4 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 From b4b2b103289ef446bdbf2128154a312e7ef2e788 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Tue, 22 Apr 2025 16:12:09 -0400 Subject: [PATCH 142/642] macOS: command pallete scroll improvements Removes the withAnimation closure which caused flashing when scrolling up or down with arrow keys. Also removes the center anchor to behave more like other command palletes (e.g., Zed, Raycast). --- .../Sources/Features/Command Palette/CommandPalette.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index cad93aa22..471281128 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -177,11 +177,8 @@ fileprivate struct CommandTable: View { .frame(maxHeight: 200) .onChange(of: selectedIndex) { _ in guard selectedIndex < options.count else { return } - withAnimation { - proxy.scrollTo( - options[Int(selectedIndex)].id, - anchor: .center) - } + proxy.scrollTo( + options[Int(selectedIndex)].id) } } } From 6bd9e35cd66f83cbdf3d27437e3677c3b28f356b Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 19 Mar 2025 19:21:36 -0500 Subject: [PATCH 143/642] snap: build from source tarball --- .github/workflows/test.yml | 14 ++++++++++---- snap/snapcraft.yaml | 3 +-- src/build/GhosttyDist.zig | 11 +++++++++++ 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e294d0565..be28d50fb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -353,15 +353,19 @@ jobs: os: [namespace-profile-ghostty-snap, namespace-profile-ghostty-snap-arm64] runs-on: ${{ matrix.os }} - needs: test + needs: [test, build-dist] env: ZIG_LOCAL_CACHE_DIR: /zig/local-cache ZIG_GLOBAL_CACHE_DIR: /zig/global-cache steps: - - uses: actions/checkout@v4 + - name: Download Source Tarball Artifacts + uses: actions/download-artifact@v4 with: - fetch-depth: 0 - fetch-tags: true + name: source-tarball + - name: Extract tarball + run: | + mkdir dist + tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache uses: namespacelabs/nscloud-cache-action@v1.2.0 with: @@ -371,6 +375,8 @@ jobs: - run: sudo apt install -y udev - run: sudo systemctl start systemd-udevd - uses: snapcore/action-build@v1 + with: + path: dist build-windows: runs-on: windows-2022 diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 43d15f813..8f1a7180a 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -70,7 +70,6 @@ parts: plugin: nil build-attributes: [enable-patchelf] build-packages: - - blueprint-compiler - libgtk-4-dev - libadwaita-1-dev # TODO: Add when the Snap is updated to Ubuntu 24.10+ @@ -80,7 +79,7 @@ parts: - patchelf - gettext override-build: | - craftctl set version=$(git describe --abbrev=8) + craftctl set version=$(cat VERSION) $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast cp -rp zig-out/* $CRAFT_PART_INSTALL/ sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index 5af8b7480..3d7ba3b8d 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -36,6 +36,17 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { "--format=tgz", }); + // embed the Ghostty version in the tarball + { + const version = b.addWriteFiles().add("VERSION", b.fmt("{}", .{cfg.version})); + // --add-file uses the most recent --prefix to determine the path + // in the archive to copy the file (the directory only). + git_archive.addArg(b.fmt("--prefix=ghostty-{}/", .{ + cfg.version, + })); + git_archive.addPrefixedFileArg("--add-file=", version); + } + // Add all of our resources into the tarball. for (resources.items) |resource| { // Our dist path basename may not match our generated file basename, From 75cc4b29fdfeead0e19416fcc1cc71b40f3e135d Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 19 Mar 2025 16:50:38 -0500 Subject: [PATCH 144/642] gtk: require blueprint-compiler 0.16 for building Changes: 1. Require `blueprint-compiler` 0.16.0 (or newer) for building from a git checkout. With #6822 distributions that can't meet that requirement can use generated source tarballs to build. 2. Remove all `.ui` files as they are unnecessary. 3. Simplify the `Builder` interface since raw `.ui` files are no longer used. 4. Removed build-time check of raw `.ui` files. --- src/apprt/gtk/Builder.zig | 107 +++-------- src/apprt/gtk/ClipboardConfirmationWindow.zig | 12 +- src/apprt/gtk/ConfigErrorsDialog.zig | 4 +- src/apprt/gtk/Surface.zig | 2 +- src/apprt/gtk/Window.zig | 2 +- src/apprt/gtk/blueprint_compiler.zig | 177 ++++++++++++++---- src/apprt/gtk/builder_check.zig | 32 ---- src/apprt/gtk/gresource.zig | 49 ++--- src/apprt/gtk/menu.zig | 2 +- src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui | 77 -------- src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui | 77 -------- src/apprt/gtk/ui/1.2/ccw-paste.ui | 77 -------- src/apprt/gtk/ui/1.2/config-errors-dialog.ui | 36 ---- src/apprt/gtk/ui/README.md | 28 ++- src/build/SharedDeps.zig | 28 --- 15 files changed, 200 insertions(+), 510 deletions(-) delete mode 100644 src/apprt/gtk/builder_check.zig delete mode 100644 src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui delete mode 100644 src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui delete mode 100644 src/apprt/gtk/ui/1.2/ccw-paste.ui delete mode 100644 src/apprt/gtk/ui/1.2/config-errors-dialog.ui diff --git a/src/apprt/gtk/Builder.zig b/src/apprt/gtk/Builder.zig index 028629200..dbd765ba3 100644 --- a/src/apprt/gtk/Builder.zig +++ b/src/apprt/gtk/Builder.zig @@ -18,88 +18,37 @@ pub fn init( /// The minor version of the minimum Adwaita version that is required to use /// this resource. comptime minor: u16, - /// `blp` signifies that the resource is a Blueprint that has been compiled - /// to GTK Builder XML at compile time. `ui` signifies that the resource is - /// a GTK Builder XML file that is included in the Ghostty source (perhaps - /// because the Blueprint compiler on some target platforms cannot compile a - /// Blueprint that generates the necessary resources). - comptime kind: enum { blp, ui }, ) Builder { const resource_path = comptime resource_path: { const gresource = @import("gresource.zig"); - switch (kind) { - .blp => { - // Check to make sure that our file is listed as a - // `blueprint_file` in `gresource.zig`. If it isn't Ghostty - // could crash at runtime when we try and load a nonexistent - // GResource. - for (gresource.blueprint_files) |file| { - if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue; - // Use @embedFile to make sure that the `.blp` file exists - // at compile time. Zig _should_ discard the data so that - // it doesn't end up in the final executable. At runtime we - // will load the data from a GResource. - const blp_filename = std.fmt.comptimePrint( - "ui/{d}.{d}/{s}.blp", - .{ - file.major, - file.minor, - file.name, - }, - ); - _ = @embedFile(blp_filename); - break :resource_path std.fmt.comptimePrint( - "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui", - .{ - file.major, - file.minor, - file.name, - }, - ); - } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig"); - }, - .ui => { - // Check to make sure that our file is listed as a `ui_file` in - // `gresource.zig`. If it isn't Ghostty could crash at runtime - // when we try and load a nonexistent GResource. - for (gresource.ui_files) |file| { - if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue; - // Use @embedFile to make sure that the `.ui` file exists - // at compile time. Zig _should_ discard the data so that - // it doesn't end up in the final executable. At runtime we - // will load the data from a GResource. - const ui_filename = std.fmt.comptimePrint( - "ui/{d}.{d}/{s}.ui", - .{ - file.major, - file.minor, - file.name, - }, - ); - _ = @embedFile(ui_filename); - // Also use @embedFile to make sure that a matching `.blp` - // file exists at compile time. Zig _should_ discard the - // data so that it doesn't end up in the final executable. - const blp_filename = std.fmt.comptimePrint( - "ui/{d}.{d}/{s}.blp", - .{ - file.major, - file.minor, - file.name, - }, - ); - _ = @embedFile(blp_filename); - break :resource_path std.fmt.comptimePrint( - "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui", - .{ - file.major, - file.minor, - file.name, - }, - ); - } else @compileError("missing ui file '" ++ name ++ "' in gresource.zig"); - }, - } + // Check to make sure that our file is listed as a + // `blueprint_file` in `gresource.zig`. If it isn't Ghostty + // could crash at runtime when we try and load a nonexistent + // GResource. + for (gresource.blueprint_files) |file| { + if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue; + // Use @embedFile to make sure that the `.blp` file exists + // at compile time. Zig _should_ discard the data so that + // it doesn't end up in the final executable. At runtime we + // will load the data from a GResource. + const blp_filename = std.fmt.comptimePrint( + "ui/{d}.{d}/{s}.blp", + .{ + file.major, + file.minor, + file.name, + }, + ); + _ = @embedFile(blp_filename); + break :resource_path std.fmt.comptimePrint( + "/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui", + .{ + file.major, + file.minor, + file.name, + }, + ); + } else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig"); }; return .{ diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index a28b7ddd4..583a58a2c 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -71,14 +71,14 @@ fn init( ) !void { var builder = switch (DialogType) { adw.AlertDialog => switch (request) { - .osc_52_read => Builder.init("ccw-osc-52-read", 1, 5, .blp), - .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5, .blp), - .paste => Builder.init("ccw-paste", 1, 5, .blp), + .osc_52_read => Builder.init("ccw-osc-52-read", 1, 5), + .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5), + .paste => Builder.init("ccw-paste", 1, 5), }, adw.MessageDialog => switch (request) { - .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2, .ui), - .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2, .ui), - .paste => Builder.init("ccw-paste", 1, 2, .ui), + .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2), + .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2), + .paste => Builder.init("ccw-paste", 1, 2), }, else => unreachable, }; diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig index c10f8e679..7a245a1a0 100644 --- a/src/apprt/gtk/ConfigErrorsDialog.zig +++ b/src/apprt/gtk/ConfigErrorsDialog.zig @@ -30,8 +30,8 @@ pub fn maybePresent(app: *App, window: ?*Window) void { if (app.config._diagnostics.empty()) return; var builder = switch (DialogType) { - adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5, .blp), - adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2, .ui), + adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5), + adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2), else => unreachable, }; defer builder.deinit(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e99fe29ce..4ad2eeb13 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1061,7 +1061,7 @@ pub fn promptTitle(self: *Surface) !void { if (!adw_version.atLeast(1, 5, 0)) return; const window = self.container.window() orelse return; - var builder = Builder.init("prompt-title-dialog", 1, 5, .blp); + var builder = Builder.init("prompt-title-dialog", 1, 5); defer builder.deinit(); const entry = builder.getObject(gtk.Entry, "title_entry").?; diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 20ac3d955..e130cd1be 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -248,7 +248,7 @@ pub fn init(self: *Window, app: *App) !void { btn.as(gtk.Widget).setTooltipText(i18n._("New Tab")); btn.setDropdownTooltip(i18n._("New Split")); - var builder = Builder.init("menu-headerbar-split_menu", 1, 0, .blp); + var builder = Builder.init("menu-headerbar-split_menu", 1, 0); defer builder.deinit(); btn.setMenuModel(builder.getObject(gio.MenuModel, "menu")); diff --git a/src/apprt/gtk/blueprint_compiler.zig b/src/apprt/gtk/blueprint_compiler.zig index 7a0442e92..9bc515655 100644 --- a/src/apprt/gtk/blueprint_compiler.zig +++ b/src/apprt/gtk/blueprint_compiler.zig @@ -4,62 +4,157 @@ pub const c = @cImport({ @cInclude("adwaita.h"); }); +const adwaita_version = std.SemanticVersion{ + .major = c.ADW_MAJOR_VERSION, + .minor = c.ADW_MINOR_VERSION, + .patch = c.ADW_MICRO_VERSION, +}; +const required_blueprint_version = std.SemanticVersion{ + .major = 0, + .minor = 16, + .patch = 0, +}; + pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const alloc = gpa.allocator(); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_allocator.deinit(); + const alloc = debug_allocator.allocator(); var it = try std.process.argsWithAllocator(alloc); defer it.deinit(); _ = it.next(); - const major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10); - const minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10); + const required_adwaita_version = std.SemanticVersion{ + .major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10), + .minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10), + .patch = 0, + }; const output = it.next() orelse return error.NoOutput; const input = it.next() orelse return error.NoInput; - if (c.ADW_MAJOR_VERSION < major or (c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor)) { - // If the Adwaita version is too old, generate an "empty" file. - const file = try std.fs.createFileAbsolute(output, .{ - .truncate = true, - }); - try file.writeAll( - \\ - \\ - ); - defer file.close(); - - return; + if (adwaita_version.order(required_adwaita_version) == .lt) { + std.debug.print( + \\`libadwaita` is too old. + \\ + \\Ghostty requires a version {} or newer of `libadwaita` to + \\compile this blueprint. Please install it, ensure that it is + \\available on your PATH, and then retry building Ghostty. + , .{required_adwaita_version}); + std.posix.exit(1); } - var compiler = std.process.Child.init( - &.{ - "blueprint-compiler", - "compile", - "--output", - output, - input, - }, - alloc, - ); + { + var stdout: std.ArrayListUnmanaged(u8) = .empty; + defer stdout.deinit(alloc); + var stderr: std.ArrayListUnmanaged(u8) = .empty; + defer stderr.deinit(alloc); - const term = compiler.spawnAndWait() catch |err| switch (err) { - error.FileNotFound => { - std.log.err( - \\`blueprint-compiler` not found. + var blueprint_compiler = std.process.Child.init( + &.{ + "blueprint-compiler", + "--version", + }, + alloc, + ); + blueprint_compiler.stdout_behavior = .Pipe; + blueprint_compiler.stderr_behavior = .Pipe; + try blueprint_compiler.spawn(); + try blueprint_compiler.collectOutput( + alloc, + &stdout, + &stderr, + std.math.maxInt(u16), + ); + const term = blueprint_compiler.wait() catch |err| switch (err) { + error.FileNotFound => { + std.debug.print( + \\`blueprint-compiler` not found. + \\ + \\Ghostty requires version {} or newer of + \\`blueprint-compiler` as a build-time dependency starting + \\from version 1.2. Please install it, ensure that it is + \\available on your PATH, and then retry building Ghostty. + \\ + , .{required_blueprint_version}); + std.posix.exit(1); + }, + else => return err, + }; + switch (term) { + .Exited => |rc| { + if (rc != 0) std.process.exit(1); + }, + else => std.process.exit(1), + } + + const version = try std.SemanticVersion.parse(std.mem.trim(u8, stdout.items, &std.ascii.whitespace)); + if (version.order(required_blueprint_version) == .lt) { + std.debug.print( + \\`blueprint-compiler` is the wrong version. \\ - \\Ghostty requires `blueprint-compiler` as a build-time dependency starting from version 1.2. - \\Please install it, ensure that it is available on your PATH, and then retry building Ghostty. - , .{}); + \\Ghostty requires version {} or newer of + \\`blueprint-compiler` as a build-time dependency starting + \\from version 1.2. Please install it, ensure that it is + \\available on your PATH, and then retry building Ghostty. + \\ + , .{required_blueprint_version}); std.posix.exit(1); - }, - else => return err, - }; + } + } - switch (term) { - .Exited => |rc| { - if (rc != 0) std.process.exit(1); - }, - else => std.process.exit(1), + { + var stdout: std.ArrayListUnmanaged(u8) = .empty; + defer stdout.deinit(alloc); + var stderr: std.ArrayListUnmanaged(u8) = .empty; + defer stderr.deinit(alloc); + + var blueprint_compiler = std.process.Child.init( + &.{ + "blueprint-compiler", + "compile", + "--output", + output, + input, + }, + alloc, + ); + blueprint_compiler.stdout_behavior = .Pipe; + blueprint_compiler.stderr_behavior = .Pipe; + try blueprint_compiler.spawn(); + try blueprint_compiler.collectOutput( + alloc, + &stdout, + &stderr, + std.math.maxInt(u16), + ); + const term = blueprint_compiler.wait() catch |err| switch (err) { + error.FileNotFound => { + std.debug.print( + \\`blueprint-compiler` not found. + \\ + \\Ghostty requires version {} or newer of + \\`blueprint-compiler` as a build-time dependency starting + \\from version 1.2. Please install it, ensure that it is + \\available on your PATH, and then retry building Ghostty. + \\ + , .{required_blueprint_version}); + std.posix.exit(1); + }, + else => return err, + }; + + switch (term) { + .Exited => |rc| { + if (rc != 0) { + std.debug.print("{s}", .{stderr.items}); + std.process.exit(1); + } + }, + else => { + std.debug.print("{s}", .{stderr.items}); + std.process.exit(1); + }, + } } } diff --git a/src/apprt/gtk/builder_check.zig b/src/apprt/gtk/builder_check.zig deleted file mode 100644 index 015c6310d..000000000 --- a/src/apprt/gtk/builder_check.zig +++ /dev/null @@ -1,32 +0,0 @@ -const std = @import("std"); -const build_options = @import("build_options"); - -const gtk = @import("gtk"); -const adw = @import("adw"); - -pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const alloc = gpa.allocator(); - - const filename = filename: { - var it = try std.process.argsWithAllocator(alloc); - defer it.deinit(); - - _ = it.next() orelse return error.NoFilename; - break :filename try alloc.dupeZ(u8, it.next() orelse return error.NoFilename); - }; - defer alloc.free(filename); - - const data = try std.fs.cwd().readFileAllocOptions(alloc, filename, std.math.maxInt(u16), null, 1, 0); - defer alloc.free(data); - - if (gtk.initCheck() == 0) { - std.debug.print("{s}: skipping builder check because we can't connect to display!\n", .{filename}); - return; - } - - adw.init(); - - const builder = gtk.Builder.newFromString(data.ptr, @intCast(data.len)); - defer builder.unref(); -} diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index 7ced9fc45..a1db8ac62 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -53,19 +53,6 @@ const icons = [_]struct { }, }; -pub const VersionedBuilderXML = struct { - major: u16, - minor: u16, - name: []const u8, -}; - -pub const ui_files = [_]VersionedBuilderXML{ - .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, - .{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" }, - .{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" }, - .{ .major = 1, .minor = 2, .name = "ccw-paste" }, -}; - pub const VersionedBlueprint = struct { major: u16, minor: u16, @@ -81,16 +68,21 @@ pub const blueprint_files = [_]VersionedBlueprint{ .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-write" }, .{ .major = 1, .minor = 5, .name = "ccw-paste" }, + .{ .major = 1, .minor = 2, .name = "config-errors-dialog" }, + .{ .major = 1, .minor = 2, .name = "ccw-osc-52-read" }, + .{ .major = 1, .minor = 2, .name = "ccw-osc-52-write" }, + .{ .major = 1, .minor = 2, .name = "ccw-paste" }, }; pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; - const alloc = gpa.allocator(); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_allocator.deinit(); + const alloc = debug_allocator.allocator(); - var extra_ui_files = std.ArrayList([]const u8).init(alloc); + var extra_ui_files: std.ArrayListUnmanaged([]const u8) = .empty; defer { for (extra_ui_files.items) |item| alloc.free(item); - extra_ui_files.deinit(); + extra_ui_files.deinit(alloc); } var it = try std.process.argsWithAllocator(alloc); @@ -98,7 +90,7 @@ pub fn main() !void { while (it.next()) |argument| { if (std.mem.eql(u8, std.fs.path.extension(argument), ".ui")) { - try extra_ui_files.append(try alloc.dupe(u8, argument)); + try extra_ui_files.append(alloc, try alloc.dupe(u8, argument)); } } @@ -132,16 +124,11 @@ pub fn main() !void { \\ \\ ); - for (ui_files) |ui_file| { - try writer.print( - " src/apprt/gtk/ui/{0d}.{1d}/{2s}.ui\n", - .{ ui_file.major, ui_file.minor, ui_file.name }, - ); - } for (extra_ui_files.items) |ui_file| { - const stem = std.fs.path.stem(ui_file); for (blueprint_files) |file| { - if (!std.mem.eql(u8, file.name, stem)) continue; + const expected = try std.fmt.allocPrint(alloc, "/{d}.{d}/{s}.ui", .{ file.major, file.minor, file.name }); + defer alloc.free(expected); + if (!std.mem.endsWith(u8, ui_file, expected)) continue; try writer.print( " {s}\n", .{ file.major, file.minor, file.name, ui_file }, @@ -157,7 +144,7 @@ pub fn main() !void { } pub const dependencies = deps: { - const total = css_files.len + icons.len + ui_files.len + blueprint_files.len; + const total = css_files.len + icons.len + blueprint_files.len; var deps: [total][]const u8 = undefined; var index: usize = 0; for (css_files) |css_file| { @@ -168,14 +155,6 @@ pub const dependencies = deps: { deps[index] = std.fmt.comptimePrint("images/icons/icon_{s}.png", .{icon.source}); index += 1; } - for (ui_files) |ui_file| { - deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.ui", .{ - ui_file.major, - ui_file.minor, - ui_file.name, - }); - index += 1; - } for (blueprint_files) |blueprint_file| { deps[index] = std.fmt.comptimePrint("src/apprt/gtk/ui/{d}.{d}/{s}.blp", .{ blueprint_file.major, diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig index d0a93b80d..f63a0eb5f 100644 --- a/src/apprt/gtk/menu.zig +++ b/src/apprt/gtk/menu.zig @@ -41,7 +41,7 @@ pub fn Menu( else => unreachable, }; - var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0, .blp); + var builder = Builder.init("menu-" ++ object_type ++ "-" ++ menu_name, 1, 0); defer builder.deinit(); const menu_model = builder.getObject(gio.MenuModel, "menu").?; diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui b/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui deleted file mode 100644 index 82512e3a2..000000000 --- a/src/apprt/gtk/ui/1.2/ccw-osc-52-read.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - Authorize Clipboard Access - An application is attempting to read from the clipboard. The current clipboard contents are shown below. - - Deny - Allow - - cancel - cancel - - - - - - 500 - 250 - - - false - false - true - 8 - 8 - 8 - 8 - - - - - - - - false - 2 - 1 - 12 - 12 - - - view-reveal-symbolic - - - - - - - false - 2 - 1 - 12 - 12 - - - - view-conceal-symbolic - - - - - - - - \ No newline at end of file diff --git a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui b/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui deleted file mode 100644 index 195fb1de1..000000000 --- a/src/apprt/gtk/ui/1.2/ccw-osc-52-write.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - Authorize Clipboard Access - An application is attempting to write to the clipboard. The current clipboard contents are shown below. - - Deny - Allow - - cancel - cancel - - - - - - 500 - 250 - - - false - false - true - 8 - 8 - 8 - 8 - - - - - - - - false - 2 - 1 - 12 - 12 - - - view-reveal-symbolic - - - - - - - false - 2 - 1 - 12 - 12 - - - - view-conceal-symbolic - - - - - - - - \ No newline at end of file diff --git a/src/apprt/gtk/ui/1.2/ccw-paste.ui b/src/apprt/gtk/ui/1.2/ccw-paste.ui deleted file mode 100644 index 342c767e6..000000000 --- a/src/apprt/gtk/ui/1.2/ccw-paste.ui +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - Warning: Potentially Unsafe Paste - Pasting this text into the terminal may be dangerous as it looks like some commands may be executed. - - Cancel - Paste - - cancel - cancel - - - - - - 500 - 250 - - - false - false - true - 8 - 8 - 8 - 8 - - - - - - - - false - 2 - 1 - 12 - 12 - - - view-reveal-symbolic - - - - - - - false - 2 - 1 - 12 - 12 - - - - view-conceal-symbolic - - - - - - - - \ No newline at end of file diff --git a/src/apprt/gtk/ui/1.2/config-errors-dialog.ui b/src/apprt/gtk/ui/1.2/config-errors-dialog.ui deleted file mode 100644 index 1d7517f7a..000000000 --- a/src/apprt/gtk/ui/1.2/config-errors-dialog.ui +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - Configuration Errors - One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors. - - Ignore - Reload Configuration - - - - 500 - 100 - - - false - false - 8 - 8 - 8 - 8 - - - - - - - - - diff --git a/src/apprt/gtk/ui/README.md b/src/apprt/gtk/ui/README.md index 08f3f367c..b9dc732b6 100644 --- a/src/apprt/gtk/ui/README.md +++ b/src/apprt/gtk/ui/README.md @@ -1,21 +1,15 @@ # GTK UI files -This directory is for storing GTK resource definitions. With one exception, the -files should be be in the Blueprint markup language. +This directory is for storing GTK blueprints. GTK blueprints are compiled into +GTK resource builder `.ui` files by `blueprint-compiler` at build time and then +converted into an embeddable resource by `glib-compile-resources`. -Resource files should be stored in directories that represent the minimum -Adwaita version needed to use that resource. Resource files should also be -formatted using `blueprint-compiler format` as well to ensure consistency. +Blueprint files should be stored in directories that represent the minimum +Adwaita version needed to use that resource. Blueprint files should also be +formatted using `blueprint-compiler format` as well to ensure consistency +(formatting will be checked in CI). -The one exception to files being in Blueprint markup language is when Adwaita -features are used that the `blueprint-compiler` on a supported platform does not -compile. For example, Debian 12 includes Adwaita 1.2 and `blueprint-compiler` -0.6.0. Adwaita 1.2 includes support for `MessageDialog` but `blueprint-compiler` -0.6.0 does not. In cases like that the Blueprint markup should be compiled on a -platform that provides a new enough `blueprint-compiler` and the resulting `.ui` -file should be committed to the Ghostty source code. Care should be taken that -the `.blp` file and the `.ui` file remain in sync. - -In all other cases only the `.blp` should be committed to the Ghostty source -code. The build process will use `blueprint-compiler` to generate the `.ui` -files necessary at runtime. +`blueprint-compiler` version 0.16.0 or newer is required to compile Blueprint +files. If your system does not have `blueprint-compiler` or does not have a +new enough version you can use the generated source tarballs, which contain +precompiled versions of the blueprints. diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 4f9373adb..4b97298f7 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -662,34 +662,6 @@ fn addGTK( } { - // For our actual build, we validate our GTK builder files if we can. - { - const gtk_builder_check = b.addExecutable(.{ - .name = "gtk_builder_check", - .root_source_file = b.path("src/apprt/gtk/builder_check.zig"), - .target = b.graph.host, - }); - gtk_builder_check.root_module.addOptions("build_options", self.options); - if (gobject_) |gobject| { - gtk_builder_check.root_module.addImport( - "gtk", - gobject.module("gtk4"), - ); - gtk_builder_check.root_module.addImport( - "adw", - gobject.module("adw1"), - ); - } - - for (gresource.dependencies) |pathname| { - const extension = std.fs.path.extension(pathname); - if (!std.mem.eql(u8, extension, ".ui")) continue; - const check = b.addRunArtifact(gtk_builder_check); - check.addFileArg(b.path(pathname)); - step.step.dependOn(&check.step); - } - } - // Get our gresource c/h files and add them to our build. const dist = gtkDistResources(b); step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} }); From 9bfe4544bfdc1f59786f857c56e9d51c4acfa427 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 23 Apr 2025 10:30:50 -0700 Subject: [PATCH 145/642] macOS: Command palette has no selection by default, selection wraps #7173 (1) The command palette no longer has any selection by default. If and when we introduce most recently used commands, defaulting to that would make sense. A selection only appears when the arrow keys are used or the user starts typing. (2) The selection with arrow keys now wraps, so if you press "down" on the last option, it will wrap to the first option, and if you press "up" on the first option, it will wrap to the last option. This matches both VSCode and Zed. --- .../Command Palette/CommandPalette.swift | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 471281128..73d192e76 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -21,8 +21,8 @@ struct CommandPaletteView: View { var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) var options: [CommandOption] @State private var query = "" - @State private var selectedIndex: UInt = 0 - @State private var hoveredOptionID: UUID? = nil + @State private var selectedIndex: UInt? + @State private var hoveredOptionID: UUID? // The options that we should show, taking into account any filtering from // the query. @@ -35,7 +35,8 @@ struct CommandPaletteView: View { } var selectedOption: CommandOption? { - if selectedIndex < filteredOptions.count { + guard let selectedIndex else { return nil } + return if selectedIndex < filteredOptions.count { filteredOptions[Int(selectedIndex)] } else { filteredOptions.last @@ -54,20 +55,38 @@ struct CommandPaletteView: View { selectedOption?.action() case .move(.up): - if selectedIndex > 0 { - selectedIndex -= 1 - } + if filteredOptions.isEmpty { break } + let current = selectedIndex ?? UInt(filteredOptions.count) + selectedIndex = (current == 0) + ? UInt(filteredOptions.count - 1) + : current - 1 case .move(.down): - if selectedIndex < filteredOptions.count - 1 { - selectedIndex += 1 - } + if filteredOptions.isEmpty { break } + let current = selectedIndex ?? UInt.max + selectedIndex = (current >= UInt(filteredOptions.count - 1)) + ? 0 + : current + 1 case .move(_): // Unknown, ignore break } } + .onChange(of: query) { newValue in + // If the user types a query then we want to make sure the first + // value is selected. If the user clears the query and we were selecting + // the first, we unset any selection. + if !newValue.isEmpty { + if selectedIndex == nil { + selectedIndex = 0 + } + } else { + if let selectedIndex, selectedIndex == 0 { + self.selectedIndex = nil + } + } + } Divider() .padding(.bottom, 4) @@ -148,7 +167,7 @@ fileprivate struct CommandPaletteQuery: View { fileprivate struct CommandTable: View { var options: [CommandOption] - @Binding var selectedIndex: UInt + @Binding var selectedIndex: UInt? @Binding var hoveredOptionID: UUID? var action: (CommandOption) -> Void @@ -164,9 +183,15 @@ fileprivate struct CommandTable: View { ForEach(Array(options.enumerated()), id: \.1.id) { index, option in CommandRow( option: option, - isSelected: selectedIndex == index || - (selectedIndex >= options.count && - index == options.count - 1), + isSelected: { + if let selected = selectedIndex { + return selected == index || + (selected >= options.count && + index == options.count - 1) + } else { + return false + } + }(), hoveredID: $hoveredOptionID ) { action(option) @@ -176,7 +201,8 @@ fileprivate struct CommandTable: View { } .frame(maxHeight: 200) .onChange(of: selectedIndex) { _ in - guard selectedIndex < options.count else { return } + guard let selectedIndex, + selectedIndex < options.count else { return } proxy.scrollTo( options[Int(selectedIndex)].id) } From f58fba54a080c891d4ed03027b5b105e705c2cf3 Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Wed, 23 Apr 2025 16:05:51 -0400 Subject: [PATCH 146/642] macOS: add descriptions for PgUp, PgDown, End, and Home keys These keys were not being assigned a description in the KeyboardShortcut extension. This caused problems for the macOS command pallete. --- macos/Sources/Helpers/KeyboardShortcut+Extension.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift index 9b5855757..ebef82c46 100644 --- a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift +++ b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift @@ -28,6 +28,10 @@ extension KeyboardShortcut: @retroactive CustomStringConvertible { case .downArrow: keyString = "↓" case .leftArrow: keyString = "←" case .rightArrow: keyString = "→" + case .pageUp: keyString = "PgUp" + case .pageDown: keyString = "PgDown" + case .end: keyString = "End" + case .home: keyString = "Home" default: keyString = String(key.character.uppercased()) } From 3827ce9e4c658599cec294f25d4288560419111a Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Thu, 24 Apr 2025 12:34:02 +0800 Subject: [PATCH 147/642] feat: beautify command palette --- .../Command Palette/CommandPalette.swift | 79 ++++++++++++------- .../TerminalCommandPalette.swift | 3 +- .../Helpers/KeyboardShortcut+Extension.swift | 30 ++++--- 3 files changed, 67 insertions(+), 45 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 73d192e76..9a3ed3965 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -4,7 +4,7 @@ struct CommandOption: Identifiable, Hashable { let id = UUID() let title: String let description: String? - let shortcut: String? + let symbols: [String]? let action: () -> Void static func == (lhs: CommandOption, rhs: CommandOption) -> Bool { @@ -18,7 +18,6 @@ struct CommandOption: Identifiable, Hashable { struct CommandPaletteView: View { @Binding var isPresented: Bool - var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) var options: [CommandOption] @State private var query = "" @State private var selectedIndex: UInt? @@ -89,7 +88,6 @@ struct CommandPaletteView: View { } Divider() - .padding(.bottom, 4) CommandTable( options: filteredOptions, @@ -100,15 +98,13 @@ struct CommandPaletteView: View { } } .frame(maxWidth: 500) - .background( - RoundedRectangle(cornerRadius: 12) - .fill(backgroundColor) - .shadow(color: .black.opacity(0.4), radius: 10, x: 0, y: 10) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color.black.opacity(0.1), lineWidth: 1) - ) + .background(BackgroundVisualEffectView()) + .clipShape(RoundedRectangle(cornerRadius: 10)) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color(nsColor: .tertiaryLabelColor).opacity(0.75)) ) + .shadow(radius: 32, x: 0, y: 12) .padding() } } @@ -147,8 +143,9 @@ fileprivate struct CommandPaletteQuery: View { TextField("Execute a command…", text: $query) .padding() - .font(.system(size: 14)) - .textFieldStyle(PlainTextFieldStyle()) + .font(.system(size: 20, weight: .light)) + .frame(height: 48) + .textFieldStyle(.plain) .focused($isTextFieldFocused) .onAppear { isTextFieldFocused = true @@ -178,7 +175,7 @@ fileprivate struct CommandTable: View { .padding() } else { ScrollViewReader { proxy in - ScrollView(showsIndicators: false) { + ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(Array(options.enumerated()), id: \.1.id) { index, option in CommandRow( @@ -198,6 +195,7 @@ fileprivate struct CommandTable: View { } } } + .padding(10) } .frame(maxHeight: 200) .onChange(of: selectedIndex) { _ in @@ -223,20 +221,12 @@ fileprivate struct CommandRow: View { HStack { Text(option.title) Spacer() - if let shortcut = option.shortcut { - Text(shortcut) - .font(.system(.body, design: .monospaced)) - .kerning(1.5) - .padding(.horizontal, 6) - .padding(.vertical, 2) - .background( - RoundedRectangle(cornerRadius: 6) - .fill(Color.gray.opacity(0.2)) - ) + if let symbols = option.symbols { + ShortcutSymbolsView(symbols: symbols) + .foregroundStyle(.secondary) } } - .padding(.horizontal, 6) - .padding(.vertical, 8) + .padding(8) .background( isSelected ? Color.accentColor.opacity(0.2) @@ -244,14 +234,43 @@ fileprivate struct CommandRow: View { ? Color.secondary.opacity(0.2) : Color.clear) ) - .cornerRadius(6) + .cornerRadius(5) } .help(option.description ?? "") - .buttonStyle(PlainButtonStyle()) + .buttonStyle(.plain) .onHover { hovering in hoveredID = hovering ? option.id : nil } - .padding(.horizontal, 4) - .padding(.vertical, 1) + } +} + +/// A view that creates a semi-transparent blurry background. +fileprivate struct BackgroundVisualEffectView: NSViewRepresentable { + func makeNSView(context: Context) -> NSVisualEffectView { + let view = NSVisualEffectView() + + view.blendingMode = .withinWindow + view.state = .active + view.material = .sidebar + + return view + } + + func updateNSView(_ nsView: NSVisualEffectView, context: Context) { + // + } +} + +/// A row of Text representing a shortcut. +fileprivate struct ShortcutSymbolsView: View { + let symbols: [String] + + var body: some View { + HStack(spacing: 1) { + ForEach(symbols, id: \.self) { symbol in + Text(symbol) + .frame(minWidth: 13) + } + } } } diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 2e895d4d9..f2c0f1989 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -40,7 +40,7 @@ struct TerminalCommandPaletteView: View { return CommandOption( title: String(cString: c.title), description: String(cString: c.description), - shortcut: ghosttyConfig.keyboardShortcut(for: action)?.description + symbols: ghosttyConfig.keyboardShortcut(for: action)?.keyList ) { onAction(action) } @@ -59,7 +59,6 @@ struct TerminalCommandPaletteView: View { CommandPaletteView( isPresented: $isPresented, - backgroundColor: ghosttyConfig.backgroundColor, options: commandOptions ) .transition( diff --git a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift index ebef82c46..7891f12d7 100644 --- a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift +++ b/macos/Sources/Helpers/KeyboardShortcut+Extension.swift @@ -1,12 +1,9 @@ import SwiftUI extension KeyboardShortcut: @retroactive CustomStringConvertible { - public var description: String { - var result = "" + public var keyList: [String] { + var result: [String] = [] - if modifiers.contains(.command) { - result.append("⌘") - } if modifiers.contains(.control) { result.append("⌃") } @@ -16,6 +13,9 @@ extension KeyboardShortcut: @retroactive CustomStringConvertible { if modifiers.contains(.shift) { result.append("⇧") } + if modifiers.contains(.command) { + result.append("⌘") + } let keyString: String switch key { @@ -24,14 +24,14 @@ extension KeyboardShortcut: @retroactive CustomStringConvertible { case .delete: keyString = "⌫" case .space: keyString = "␣" case .tab: keyString = "⇥" - case .upArrow: keyString = "↑" - case .downArrow: keyString = "↓" - case .leftArrow: keyString = "←" - case .rightArrow: keyString = "→" - case .pageUp: keyString = "PgUp" - case .pageDown: keyString = "PgDown" - case .end: keyString = "End" - case .home: keyString = "Home" + case .upArrow: keyString = "▲" + case .downArrow: keyString = "▼" + case .leftArrow: keyString = "◀" + case .rightArrow: keyString = "▶" + case .pageUp: keyString = "↑" + case .pageDown: keyString = "↓" + case .home: keyString = "⤒" + case .end: keyString = "⤓" default: keyString = String(key.character.uppercased()) } @@ -39,6 +39,10 @@ extension KeyboardShortcut: @retroactive CustomStringConvertible { result.append(keyString) return result } + + public var description: String { + return self.keyList.joined() + } } // This is available in macOS 14 so this only applies to early macOS versions. From 1acb1715c392fd6753e5f2c51ef6da9f05151142 Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Fri, 25 Apr 2025 10:42:22 +0800 Subject: [PATCH 148/642] replace NSVisualEffectView with ultraThinMaterial plus a background color --- .../Command Palette/CommandPalette.swift | 36 +++++++++---------- .../TerminalCommandPalette.swift | 1 + 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 9a3ed3965..53070c5db 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -18,6 +18,7 @@ struct CommandOption: Identifiable, Hashable { struct CommandPaletteView: View { @Binding var isPresented: Bool + var backgroundColor: Color = Color(nsColor: .windowBackgroundColor) var options: [CommandOption] @State private var query = "" @State private var selectedIndex: UInt? @@ -43,6 +44,12 @@ struct CommandPaletteView: View { } var body: some View { + @State var scheme: ColorScheme = if OSColor(backgroundColor).isLightColor { + .light + } else { + .dark + } + VStack(alignment: .leading, spacing: 0) { CommandPaletteQuery(query: $query) { event in switch (event) { @@ -98,7 +105,16 @@ struct CommandPaletteView: View { } } .frame(maxWidth: 500) - .background(BackgroundVisualEffectView()) + .background( + ZStack { + Rectangle() + .fill(.ultraThinMaterial) + Rectangle() + .fill(backgroundColor) + .blendMode(.color) + } + .compositingGroup() + ) .clipShape(RoundedRectangle(cornerRadius: 10)) .overlay( RoundedRectangle(cornerRadius: 10) @@ -106,6 +122,7 @@ struct CommandPaletteView: View { ) .shadow(radius: 32, x: 0, y: 12) .padding() + .environment(\.colorScheme, scheme) } } @@ -244,23 +261,6 @@ fileprivate struct CommandRow: View { } } -/// A view that creates a semi-transparent blurry background. -fileprivate struct BackgroundVisualEffectView: NSViewRepresentable { - func makeNSView(context: Context) -> NSVisualEffectView { - let view = NSVisualEffectView() - - view.blendingMode = .withinWindow - view.state = .active - view.material = .sidebar - - return view - } - - func updateNSView(_ nsView: NSVisualEffectView, context: Context) { - // - } -} - /// A row of Text representing a shortcut. fileprivate struct ShortcutSymbolsView: View { let symbols: [String] diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index f2c0f1989..affbc4ddc 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -59,6 +59,7 @@ struct TerminalCommandPaletteView: View { CommandPaletteView( isPresented: $isPresented, + backgroundColor: ghosttyConfig.backgroundColor, options: commandOptions ) .transition( From 334093a9ea1ea91d6ae3c562cb182969fe5576fa Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Fri, 25 Apr 2025 15:59:44 +0800 Subject: [PATCH 149/642] feat: implement toggleMaximize for macOS --- macos/Sources/App/macOS/AppDelegate.swift | 6 ++++++ .../Command Palette/TerminalCommandPalette.swift | 1 - macos/Sources/Ghostty/Ghostty.App.swift | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 682099e92..8849ddb75 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -807,6 +807,12 @@ class AppDelegate: NSObject, setSecureInput(.toggle) } + @IBAction func toggleMaximize(_ sender: Any) { + if NSApp.isActive, let keyWindow = NSApp.keyWindow { + keyWindow.zoom(self) + } + } + @IBAction func toggleQuickTerminal(_ sender: Any) { if quickController == nil { quickController = QuickTerminalController( diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 2e895d4d9..7820e45fa 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -29,7 +29,6 @@ struct TerminalCommandPaletteView: View { let key = String(cString: c.action_key) switch (key) { case "toggle_tab_overview", - "toggle_maximize", "toggle_window_decorations": return false default: diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 677129960..8f2624df4 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -523,6 +523,9 @@ extension Ghostty { case GHOSTTY_ACTION_TOGGLE_COMMAND_PALETTE: toggleCommandPalette(app, target: target) + case GHOSTTY_ACTION_TOGGLE_MAXIMIZE: + toggleMaximize(app, target: target) + case GHOSTTY_ACTION_TOGGLE_QUICK_TERMINAL: toggleQuickTerminal(app, target: target) @@ -767,6 +770,14 @@ extension Ghostty { } } + private static func toggleMaximize( + _ app: ghostty_app_t, + target: ghostty_target_s + ) { + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } + appDelegate.toggleMaximize(self) + } + private static func toggleVisibility( _ app: ghostty_app_t, target: ghostty_target_s From 77f5fc34f119aa8bcfd31636056b14e46f4a84ba Mon Sep 17 00:00:00 2001 From: Friedrich Stoltzfus Date: Fri, 25 Apr 2025 14:10:35 -0400 Subject: [PATCH 150/642] Add config color palette C bindings C bindings to expose the color palette to Swift for macOS. --- include/ghostty.h | 5 +++++ src/config/Config.zig | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index 3db280c93..f7504eb7e 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -357,6 +357,11 @@ typedef struct { size_t len; } ghostty_config_color_list_s; +// config.Palette +typedef struct { + ghostty_config_color_s colors[256]; +} ghostty_config_palette_s; + // apprt.Target.Key typedef enum { GHOSTTY_TARGET_APP, diff --git a/src/config/Config.zig b/src/config/Config.zig index f71e0972d..196409762 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3930,6 +3930,24 @@ pub const Palette = struct { /// The actual value that is updated as we parse. value: terminal.color.Palette = terminal.color.default, + /// ghostty_config_palette_s + pub const C = extern struct { + colors: [265]Color.C, + }; + + pub fn cval(self: Self) Palette.C { + var result: Palette.C = undefined; + for (self.value, 0..) |color, i| { + result.colors[i] = Color.C{ + .r = color.r, + .g = color.g, + .b = color.b, + }; + } + + return result; + } + pub fn parseCLI( self: *Self, input: ?[]const u8, From 2b4d89e11f3504eaa637737944d04bcc818b2fca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 25 Apr 2025 11:42:04 -0700 Subject: [PATCH 151/642] macOS: scheme doesn't need to be state --- macos/Sources/Features/Command Palette/CommandPalette.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 53070c5db..3e5a3a36f 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -44,7 +44,7 @@ struct CommandPaletteView: View { } var body: some View { - @State var scheme: ColorScheme = if OSColor(backgroundColor).isLightColor { + let scheme: ColorScheme = if OSColor(backgroundColor).isLightColor { .light } else { .dark From 12f48419b689d803dad8de17305821e1e22e6a7e Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:14:50 +0000 Subject: [PATCH 152/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b242d7d11..c424cf541 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz", - .hash = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz", + .hash = "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 96d8431f7..cd3275fb1 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC": { + "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz", - "hash": "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz", + "hash": "sha256-Vy5muiJ3hJXcOvmFHLhqc+Dvdh74GG6+u/L+EsavDb0=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 33bdee518..b3a98af24 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC"; + name = "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz"; - hash = "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz"; + hash = "sha256-Vy5muiJ3hJXcOvmFHLhqc+Dvdh74GG6+u/L+EsavDb0="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 4068380b4..61c8f5e72 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz From 1ec3e331dedaa4db92d5556e1eec0a31fac17bd5 Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Sun, 27 Apr 2025 08:48:06 +0800 Subject: [PATCH 153/642] Refactor toggleMaximize to use notifications --- macos/Sources/App/macOS/AppDelegate.swift | 6 ------ .../Terminal/BaseTerminalController.swift | 10 ++++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 19 +++++++++++++++++-- macos/Sources/Ghostty/Package.swift | 3 +++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 8849ddb75..682099e92 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -807,12 +807,6 @@ class AppDelegate: NSObject, setSecureInput(.toggle) } - @IBAction func toggleMaximize(_ sender: Any) { - if NSApp.isActive, let keyWindow = NSApp.keyWindow { - keyWindow.zoom(self) - } - } - @IBAction func toggleQuickTerminal(_ sender: Any) { if quickController == nil { quickController = QuickTerminalController( diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b502e56e0..59cef503d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -115,6 +115,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyCommandPaletteDidToggle(_:)), name: .ghosttyCommandPaletteDidToggle, object: nil) + center.addObserver( + self, + selector: #selector(toggleMaximize), + name: .ghosttyToggleMaximize, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -548,6 +553,11 @@ class BaseTerminalController: NSWindowController, window.performClose(sender) } + @IBAction func toggleMaximize(_ sender: Any) { + guard let window = window else { return } + window.zoom(self) + } + @IBAction func splitRight(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 8f2624df4..e72acc05e 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -774,8 +774,23 @@ extension Ghostty { _ app: ghostty_app_t, target: ghostty_target_s ) { - guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return } - appDelegate.toggleMaximize(self) + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle maximize does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + NotificationCenter.default.post( + name: .ghosttyToggleMaximize, + object: surfaceView + ) + + + default: + assertionFailure() + } } private static func toggleVisibility( diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e2c770899..ee0073aa1 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -257,6 +257,9 @@ extension Notification.Name { /// Ring the bell static let ghosttyBellDidRing = Notification.Name("com.mitchellh.ghostty.ghosttyBellDidRing") static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle") + + /// Toggle maximize of current window + static let ghosttyToggleMaximize = Notification.Name("com.mitchellh.ghostty.toggleMaximize") } // NOTE: I am moving all of these to Notification.Name extensions over time. This From 0c8339d2da2b1b1fdcfa40a94158115b9e984158 Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Sat, 26 Apr 2025 23:39:02 -0500 Subject: [PATCH 154/642] update z2d to 0.6.1 --- build.zig.zon | 4 +- build.zig.zon.json | 4 +- build.zig.zon.nix | 680 +++++++++++++++++++++++---------------------- build.zig.zon.txt | 2 +- 4 files changed, 350 insertions(+), 340 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index b242d7d11..a8f763e41 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz", - .hash = "z2d-0.6.0-j5P_HvLdCABu-dXpCeRM7Uk4m16vULg1980lMNCQj4_C", + .url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", + .hash = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 96d8431f7..ea6279340 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -126,8 +126,8 @@ }, "z2d-0.6.0-j5P_HvLdCABu-dXpCeRM7Uk4m16vULg1980lMNCQj4_C": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz", - "hash": "sha256-PEKVSUZ6teRbDyhFPWSiuBSe40pgr0kVRivIY8Cn8HQ=" + "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", + "hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=" }, "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 33bdee518..0f9f21aaf 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -7,350 +7,360 @@ runCommandLocal, zig_0_14, name ? "zig-packages", -}: let - unpackZigArtifact = { - name, - artifact, - }: - runCommandLocal name +}: +let + unpackZigArtifact = { - nativeBuildInputs = [zig_0_14]; - } - '' - hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" - mv "$TMPDIR/p/$hash" "$out" - chmod 755 "$out" - ''; + name, + artifact, + }: + runCommandLocal name + { + nativeBuildInputs = [ zig_0_14 ]; + } + '' + hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" + mv "$TMPDIR/p/$hash" "$out" + chmod 755 "$out" + ''; - fetchZig = { - name, - url, - hash, - }: let - artifact = fetchurl {inherit url hash;}; - in - unpackZigArtifact {inherit name artifact;}; + fetchZig = + { + name, + url, + hash, + }: + let + artifact = fetchurl { inherit url hash; }; + in + unpackZigArtifact { inherit name artifact; }; - fetchGitZig = { - name, - url, - hash, - }: let - parts = lib.splitString "#" url; - url_base = builtins.elemAt parts 0; - url_without_query = builtins.elemAt (lib.splitString "?" url_base) 0; - rev_base = builtins.elemAt parts 1; - rev = - if builtins.match "^[a-fA-F0-9]{40}$" rev_base != null - then rev_base - else "refs/heads/${rev_base}"; - in + fetchGitZig = + { + name, + url, + hash, + }: + let + parts = lib.splitString "#" url; + url_base = builtins.elemAt parts 0; + url_without_query = builtins.elemAt (lib.splitString "?" url_base) 0; + rev_base = builtins.elemAt parts 1; + rev = + if builtins.match "^[a-fA-F0-9]{40}$" rev_base != null then + rev_base + else + "refs/heads/${rev_base}"; + in fetchgit { inherit name rev hash; url = url_without_query; deepClone = false; + fetchSubmodules = false; }; - fetchZigArtifact = { - name, - url, - hash, - }: let - parts = lib.splitString "://" url; - proto = builtins.elemAt parts 0; - path = builtins.elemAt parts 1; - fetcher = { - "git+http" = fetchGitZig { - inherit name hash; - url = "http://${path}"; + fetchZigArtifact = + { + name, + url, + hash, + }: + let + parts = lib.splitString "://" url; + proto = builtins.elemAt parts 0; + path = builtins.elemAt parts 1; + fetcher = { + "git+http" = fetchGitZig { + inherit name hash; + url = "http://${path}"; + }; + "git+https" = fetchGitZig { + inherit name hash; + url = "https://${path}"; + }; + http = fetchZig { + inherit name hash; + url = "http://${path}"; + }; + https = fetchZig { + inherit name hash; + url = "https://${path}"; + }; }; - "git+https" = fetchGitZig { - inherit name hash; - url = "https://${path}"; - }; - http = fetchZig { - inherit name hash; - url = "http://${path}"; - }; - https = fetchZig { - inherit name hash; - url = "https://${path}"; - }; - }; - in + in fetcher.${proto}; in - linkFarm name [ - { - name = "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ"; - path = fetchZigArtifact { - name = "breakpad"; - url = "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz"; - hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="; - }; - } - { - name = "N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v"; - path = fetchZigArtifact { - name = "fontconfig"; - url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz"; - hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="; - }; - } - { - name = "N-V-__8AAKLKpwC4H27Ps_0iL3bPkQb-z6ZVSrB-x_3EEkub"; - path = fetchZigArtifact { - name = "freetype"; - url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz"; - hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw="; - }; - } - { - name = "N-V-__8AADcZkgn4cMhTUpIz6mShCKyqqB-NBtf_S2bHaTC-"; - path = fetchZigArtifact { - name = "gettext"; - url = "https://deps.files.ghostty.org/gettext-0.24.tar.gz"; - hash = "sha256-yRhQPVk9cNr0hE0XWhPYFq+stmfAb7oeydzVACwVGLc="; - }; - } - { - name = "N-V-__8AAMrJSwAUGb9-vTzkNR-5LXS81MR__ZRVfF3tWgG6"; - path = fetchZigArtifact { - name = "glfw"; - url = "https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz"; - hash = "sha256-M3N1XUAlMebBo5X1Py+9YxjKXgZ6eacqWRCbUmwLKQo="; - }; - } - { - name = "N-V-__8AABzkUgISeKGgXAzgtutgJsZc0-kkeqBBscJgMkvy"; - path = fetchZigArtifact { - name = "glslang"; - url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz"; - hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="; - }; - } - { - name = "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR"; - path = fetchZigArtifact { - name = "gobject"; - url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst"; - hash = "sha256-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI="; - }; - } - { - name = "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr"; - path = fetchZigArtifact { - name = "gtk4_layer_shell"; - url = "https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz"; - hash = "sha256-mChCgSYKXu9bT2OlXxbEv2p4ihAgptsDfssPcfozaYg="; - }; - } - { - name = "N-V-__8AAG02ugUcWec-Ndp-i7JTsJ0dgF8nnJRUInkGLG7G"; - path = fetchZigArtifact { - name = "harfbuzz"; - url = "https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz"; - hash = "sha256-8WNRuv4hRyX+LB1bWfDZPkmQWkskeJn7kNcM/5U6K5s="; - }; - } - { - name = "N-V-__8AAGmZhABbsPJLfbqrh6JTHsXhY6qCaLAQyx25e0XE"; - path = fetchZigArtifact { - name = "highway"; - url = "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz"; - hash = "sha256-h9T4iT704I8iSXNgj/6/lCaKgTgLp5wS6IQZaMgKohI="; - }; - } - { - name = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3"; - path = fetchZigArtifact { - name = "imgui"; - url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz"; - hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="; - }; - } - { - name = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC"; - path = fetchZigArtifact { - name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz"; - hash = "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs="; - }; - } - { - name = "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD"; - path = fetchZigArtifact { - name = "libpng"; - url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz"; - hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="; - }; - } - { - name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz"; - path = fetchZigArtifact { - name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz"; - hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="; - }; - } - { - name = "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK"; - path = fetchZigArtifact { - name = "libxml2"; - url = "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz"; - hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="; - }; - } - { - name = "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c"; - path = fetchZigArtifact { - name = "oniguruma"; - url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz"; - hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA="; - }; - } - { - name = "N-V-__8AADYiAAB_80AWnH1AxXC0tql9thT-R-DYO1gBqTLc"; - path = fetchZigArtifact { - name = "pixels"; - url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz"; - hash = "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro="; - }; - } - { - name = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs"; - path = fetchZigArtifact { - name = "plasma_wayland_protocols"; - url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz"; - hash = "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE="; - }; - } - { - name = "N-V-__8AAPlZGwBEa-gxrcypGBZ2R8Bse4JYSfo_ul8i2jlG"; - path = fetchZigArtifact { - name = "sentry"; - url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz"; - hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8="; - }; - } - { - name = "N-V-__8AANb6pwD7O1WG6L5nvD_rNMvnSc9Cpg1ijSlTYywv"; - path = fetchZigArtifact { - name = "spirv_cross"; - url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz"; - hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="; - }; - } - { - name = "N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH"; - path = fetchZigArtifact { - name = "utfcpp"; - url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz"; - hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="; - }; - } - { - name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; - path = fetchZigArtifact { - name = "vaxis"; - url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; - hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; - }; - } - { - name = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t"; - path = fetchZigArtifact { - name = "wayland"; - url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz"; - hash = "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0="; - }; - } - { - name = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S"; - path = fetchZigArtifact { - name = "wayland_protocols"; - url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz"; - hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="; - }; - } - { - name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; - path = fetchZigArtifact { - name = "wuffs"; - url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz"; - hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="; - }; - } - { - name = "z2d-0.6.0-j5P_HvLdCABu-dXpCeRM7Uk4m16vULg1980lMNCQj4_C"; - path = fetchZigArtifact { - name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz"; - hash = "sha256-PEKVSUZ6teRbDyhFPWSiuBSe40pgr0kVRivIY8Cn8HQ="; - }; - } - { - name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; - path = fetchZigArtifact { - name = "zf"; - url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; - hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; - }; - } - { - name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"; - path = fetchZigArtifact { - name = "zg"; - url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc"; - hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="; - }; - } - { - name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ"; - path = fetchZigArtifact { - name = "zig_js"; - url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz"; - hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="; - }; - } - { - name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt"; - path = fetchZigArtifact { - name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; - hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; - }; - } - { - name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy"; - path = fetchZigArtifact { - name = "zig_wayland"; - url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz"; - hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="; - }; - } - { - name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; - path = fetchZigArtifact { - name = "zigimg"; - url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; - hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; - }; - } - { - name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; - path = fetchZigArtifact { - name = "ziglyph"; - url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; - hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; - }; - } - { - name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; - path = fetchZigArtifact { - name = "zlib"; - url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz"; - hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw="; - }; - } - ] +linkFarm name [ + { + name = "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ"; + path = fetchZigArtifact { + name = "breakpad"; + url = "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz"; + hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="; + }; + } + { + name = "N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v"; + path = fetchZigArtifact { + name = "fontconfig"; + url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz"; + hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="; + }; + } + { + name = "N-V-__8AAKLKpwC4H27Ps_0iL3bPkQb-z6ZVSrB-x_3EEkub"; + path = fetchZigArtifact { + name = "freetype"; + url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz"; + hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw="; + }; + } + { + name = "N-V-__8AADcZkgn4cMhTUpIz6mShCKyqqB-NBtf_S2bHaTC-"; + path = fetchZigArtifact { + name = "gettext"; + url = "https://deps.files.ghostty.org/gettext-0.24.tar.gz"; + hash = "sha256-yRhQPVk9cNr0hE0XWhPYFq+stmfAb7oeydzVACwVGLc="; + }; + } + { + name = "N-V-__8AAMrJSwAUGb9-vTzkNR-5LXS81MR__ZRVfF3tWgG6"; + path = fetchZigArtifact { + name = "glfw"; + url = "https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz"; + hash = "sha256-M3N1XUAlMebBo5X1Py+9YxjKXgZ6eacqWRCbUmwLKQo="; + }; + } + { + name = "N-V-__8AABzkUgISeKGgXAzgtutgJsZc0-kkeqBBscJgMkvy"; + path = fetchZigArtifact { + name = "glslang"; + url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz"; + hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="; + }; + } + { + name = "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR"; + path = fetchZigArtifact { + name = "gobject"; + url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst"; + hash = "sha256-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI="; + }; + } + { + name = "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr"; + path = fetchZigArtifact { + name = "gtk4_layer_shell"; + url = "https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz"; + hash = "sha256-mChCgSYKXu9bT2OlXxbEv2p4ihAgptsDfssPcfozaYg="; + }; + } + { + name = "N-V-__8AAG02ugUcWec-Ndp-i7JTsJ0dgF8nnJRUInkGLG7G"; + path = fetchZigArtifact { + name = "harfbuzz"; + url = "https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz"; + hash = "sha256-8WNRuv4hRyX+LB1bWfDZPkmQWkskeJn7kNcM/5U6K5s="; + }; + } + { + name = "N-V-__8AAGmZhABbsPJLfbqrh6JTHsXhY6qCaLAQyx25e0XE"; + path = fetchZigArtifact { + name = "highway"; + url = "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz"; + hash = "sha256-h9T4iT704I8iSXNgj/6/lCaKgTgLp5wS6IQZaMgKohI="; + }; + } + { + name = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3"; + path = fetchZigArtifact { + name = "imgui"; + url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz"; + hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="; + }; + } + { + name = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC"; + path = fetchZigArtifact { + name = "iterm2_themes"; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz"; + hash = "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs="; + }; + } + { + name = "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD"; + path = fetchZigArtifact { + name = "libpng"; + url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz"; + hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="; + }; + } + { + name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz"; + path = fetchZigArtifact { + name = "libxev"; + url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz"; + hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="; + }; + } + { + name = "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK"; + path = fetchZigArtifact { + name = "libxml2"; + url = "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz"; + hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="; + }; + } + { + name = "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c"; + path = fetchZigArtifact { + name = "oniguruma"; + url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz"; + hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA="; + }; + } + { + name = "N-V-__8AADYiAAB_80AWnH1AxXC0tql9thT-R-DYO1gBqTLc"; + path = fetchZigArtifact { + name = "pixels"; + url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz"; + hash = "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro="; + }; + } + { + name = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs"; + path = fetchZigArtifact { + name = "plasma_wayland_protocols"; + url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz"; + hash = "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE="; + }; + } + { + name = "N-V-__8AAPlZGwBEa-gxrcypGBZ2R8Bse4JYSfo_ul8i2jlG"; + path = fetchZigArtifact { + name = "sentry"; + url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz"; + hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8="; + }; + } + { + name = "N-V-__8AANb6pwD7O1WG6L5nvD_rNMvnSc9Cpg1ijSlTYywv"; + path = fetchZigArtifact { + name = "spirv_cross"; + url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz"; + hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="; + }; + } + { + name = "N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH"; + path = fetchZigArtifact { + name = "utfcpp"; + url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz"; + hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="; + }; + } + { + name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; + path = fetchZigArtifact { + name = "vaxis"; + url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; + hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; + }; + } + { + name = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t"; + path = fetchZigArtifact { + name = "wayland"; + url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz"; + hash = "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0="; + }; + } + { + name = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S"; + path = fetchZigArtifact { + name = "wayland_protocols"; + url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz"; + hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="; + }; + } + { + name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; + path = fetchZigArtifact { + name = "wuffs"; + url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz"; + hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="; + }; + } + { + name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW"; + path = fetchZigArtifact { + name = "z2d"; + url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz"; + hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="; + }; + } + { + name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; + path = fetchZigArtifact { + name = "zf"; + url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; + hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; + }; + } + { + name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"; + path = fetchZigArtifact { + name = "zg"; + url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc"; + hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="; + }; + } + { + name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ"; + path = fetchZigArtifact { + name = "zig_js"; + url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz"; + hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="; + }; + } + { + name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt"; + path = fetchZigArtifact { + name = "zig_objc"; + url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; + hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; + }; + } + { + name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy"; + path = fetchZigArtifact { + name = "zig_wayland"; + url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz"; + hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="; + }; + } + { + name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; + path = fetchZigArtifact { + name = "zigimg"; + url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; + hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; + }; + } + { + name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; + path = fetchZigArtifact { + name = "ziglyph"; + url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; + hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; + }; + } + { + name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; + path = fetchZigArtifact { + name = "zlib"; + url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz"; + hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw="; + }; + } +] diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 4068380b4..4833cd5ac 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -31,4 +31,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22 https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz +https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz From bbbe81efc5b4059db54bd41a30b5b27f308695cf Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Sat, 26 Apr 2025 23:51:04 -0500 Subject: [PATCH 155/642] z2d context no longer has err return --- src/font/sprite/Box.zig | 8 ++++---- src/font/sprite/canvas.zig | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index f387ab240..68acdabe5 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -2239,7 +2239,7 @@ fn draw_branch_node( @min(float_width - cx, float_height - cy), ); - var ctx = canvas.getContext() catch return; + var ctx = canvas.getContext(); defer ctx.deinit(); ctx.setSource(.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, @@ -2290,7 +2290,7 @@ fn draw_circle( }; const r: f64 = 0.5 * @min(float_width, float_height); - var ctx = canvas.getContext() catch return; + var ctx = canvas.getContext(); defer ctx.deinit(); ctx.setSource(.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, @@ -2680,7 +2680,7 @@ fn draw_arc( // Fraction away from the center to place the middle control points, const s: f64 = 0.25; - var ctx = try canvas.getContext(); + var ctx = canvas.getContext(); defer ctx.deinit(); ctx.setSource(.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, @@ -2974,7 +2974,7 @@ fn draw_separated_block_quadrant(self: Box, canvas: *font.sprite.Canvas, comptim } } - var ctx = try canvas.getContext(); + var ctx = canvas.getContext(); defer ctx.deinit(); ctx.setSource(.{ .opaque_pattern = .{ .pixel = .{ .alpha8 = .{ .a = @intFromEnum(Shade.on) } }, diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index 072e5bd46..ed00aef12 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -149,8 +149,8 @@ pub const Canvas = struct { } /// Acquires a z2d drawing context, caller MUST deinit context. - pub fn getContext(self: *Canvas) Allocator.Error!z2d.Context { - return try z2d.Context.init(self.alloc, &self.sfc); + pub fn getContext(self: *Canvas) z2d.Context { + return z2d.Context.init(self.alloc, &self.sfc); } /// Draw and fill a single pixel From b1561112d02c62e34cd5b976baa10d5a7099fb67 Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:03:00 -0500 Subject: [PATCH 156/642] run the nix cache script --- build.zig.zon.json | 2 +- build.zig.zon.nix | 680 ++++++++++++++++++++++----------------------- 2 files changed, 336 insertions(+), 346 deletions(-) diff --git a/build.zig.zon.json b/build.zig.zon.json index ea6279340..3c811ebe7 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -124,7 +124,7 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.6.0-j5P_HvLdCABu-dXpCeRM7Uk4m16vULg1980lMNCQj4_C": { + "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW": { "name": "z2d", "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", "hash": "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U=" diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 0f9f21aaf..65bb73263 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -7,360 +7,350 @@ runCommandLocal, zig_0_14, name ? "zig-packages", -}: -let - unpackZigArtifact = - { - name, - artifact, - }: +}: let + unpackZigArtifact = { + name, + artifact, + }: runCommandLocal name - { - nativeBuildInputs = [ zig_0_14 ]; - } - '' - hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" - mv "$TMPDIR/p/$hash" "$out" - chmod 755 "$out" - ''; - - fetchZig = { - name, - url, - hash, - }: - let - artifact = fetchurl { inherit url hash; }; - in - unpackZigArtifact { inherit name artifact; }; + nativeBuildInputs = [zig_0_14]; + } + '' + hash="$(zig fetch --global-cache-dir "$TMPDIR" ${artifact})" + mv "$TMPDIR/p/$hash" "$out" + chmod 755 "$out" + ''; - fetchGitZig = - { - name, - url, - hash, - }: - let - parts = lib.splitString "#" url; - url_base = builtins.elemAt parts 0; - url_without_query = builtins.elemAt (lib.splitString "?" url_base) 0; - rev_base = builtins.elemAt parts 1; - rev = - if builtins.match "^[a-fA-F0-9]{40}$" rev_base != null then - rev_base - else - "refs/heads/${rev_base}"; - in + fetchZig = { + name, + url, + hash, + }: let + artifact = fetchurl {inherit url hash;}; + in + unpackZigArtifact {inherit name artifact;}; + + fetchGitZig = { + name, + url, + hash, + }: let + parts = lib.splitString "#" url; + url_base = builtins.elemAt parts 0; + url_without_query = builtins.elemAt (lib.splitString "?" url_base) 0; + rev_base = builtins.elemAt parts 1; + rev = + if builtins.match "^[a-fA-F0-9]{40}$" rev_base != null + then rev_base + else "refs/heads/${rev_base}"; + in fetchgit { inherit name rev hash; url = url_without_query; deepClone = false; - fetchSubmodules = false; }; - fetchZigArtifact = - { - name, - url, - hash, - }: - let - parts = lib.splitString "://" url; - proto = builtins.elemAt parts 0; - path = builtins.elemAt parts 1; - fetcher = { - "git+http" = fetchGitZig { - inherit name hash; - url = "http://${path}"; - }; - "git+https" = fetchGitZig { - inherit name hash; - url = "https://${path}"; - }; - http = fetchZig { - inherit name hash; - url = "http://${path}"; - }; - https = fetchZig { - inherit name hash; - url = "https://${path}"; - }; + fetchZigArtifact = { + name, + url, + hash, + }: let + parts = lib.splitString "://" url; + proto = builtins.elemAt parts 0; + path = builtins.elemAt parts 1; + fetcher = { + "git+http" = fetchGitZig { + inherit name hash; + url = "http://${path}"; }; - in + "git+https" = fetchGitZig { + inherit name hash; + url = "https://${path}"; + }; + http = fetchZig { + inherit name hash; + url = "http://${path}"; + }; + https = fetchZig { + inherit name hash; + url = "https://${path}"; + }; + }; + in fetcher.${proto}; in -linkFarm name [ - { - name = "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ"; - path = fetchZigArtifact { - name = "breakpad"; - url = "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz"; - hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="; - }; - } - { - name = "N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v"; - path = fetchZigArtifact { - name = "fontconfig"; - url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz"; - hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="; - }; - } - { - name = "N-V-__8AAKLKpwC4H27Ps_0iL3bPkQb-z6ZVSrB-x_3EEkub"; - path = fetchZigArtifact { - name = "freetype"; - url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz"; - hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw="; - }; - } - { - name = "N-V-__8AADcZkgn4cMhTUpIz6mShCKyqqB-NBtf_S2bHaTC-"; - path = fetchZigArtifact { - name = "gettext"; - url = "https://deps.files.ghostty.org/gettext-0.24.tar.gz"; - hash = "sha256-yRhQPVk9cNr0hE0XWhPYFq+stmfAb7oeydzVACwVGLc="; - }; - } - { - name = "N-V-__8AAMrJSwAUGb9-vTzkNR-5LXS81MR__ZRVfF3tWgG6"; - path = fetchZigArtifact { - name = "glfw"; - url = "https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz"; - hash = "sha256-M3N1XUAlMebBo5X1Py+9YxjKXgZ6eacqWRCbUmwLKQo="; - }; - } - { - name = "N-V-__8AABzkUgISeKGgXAzgtutgJsZc0-kkeqBBscJgMkvy"; - path = fetchZigArtifact { - name = "glslang"; - url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz"; - hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="; - }; - } - { - name = "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR"; - path = fetchZigArtifact { - name = "gobject"; - url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst"; - hash = "sha256-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI="; - }; - } - { - name = "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr"; - path = fetchZigArtifact { - name = "gtk4_layer_shell"; - url = "https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz"; - hash = "sha256-mChCgSYKXu9bT2OlXxbEv2p4ihAgptsDfssPcfozaYg="; - }; - } - { - name = "N-V-__8AAG02ugUcWec-Ndp-i7JTsJ0dgF8nnJRUInkGLG7G"; - path = fetchZigArtifact { - name = "harfbuzz"; - url = "https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz"; - hash = "sha256-8WNRuv4hRyX+LB1bWfDZPkmQWkskeJn7kNcM/5U6K5s="; - }; - } - { - name = "N-V-__8AAGmZhABbsPJLfbqrh6JTHsXhY6qCaLAQyx25e0XE"; - path = fetchZigArtifact { - name = "highway"; - url = "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz"; - hash = "sha256-h9T4iT704I8iSXNgj/6/lCaKgTgLp5wS6IQZaMgKohI="; - }; - } - { - name = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3"; - path = fetchZigArtifact { - name = "imgui"; - url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz"; - hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="; - }; - } - { - name = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC"; - path = fetchZigArtifact { - name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz"; - hash = "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs="; - }; - } - { - name = "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD"; - path = fetchZigArtifact { - name = "libpng"; - url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz"; - hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="; - }; - } - { - name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz"; - path = fetchZigArtifact { - name = "libxev"; - url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz"; - hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="; - }; - } - { - name = "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK"; - path = fetchZigArtifact { - name = "libxml2"; - url = "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz"; - hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="; - }; - } - { - name = "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c"; - path = fetchZigArtifact { - name = "oniguruma"; - url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz"; - hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA="; - }; - } - { - name = "N-V-__8AADYiAAB_80AWnH1AxXC0tql9thT-R-DYO1gBqTLc"; - path = fetchZigArtifact { - name = "pixels"; - url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz"; - hash = "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro="; - }; - } - { - name = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs"; - path = fetchZigArtifact { - name = "plasma_wayland_protocols"; - url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz"; - hash = "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE="; - }; - } - { - name = "N-V-__8AAPlZGwBEa-gxrcypGBZ2R8Bse4JYSfo_ul8i2jlG"; - path = fetchZigArtifact { - name = "sentry"; - url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz"; - hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8="; - }; - } - { - name = "N-V-__8AANb6pwD7O1WG6L5nvD_rNMvnSc9Cpg1ijSlTYywv"; - path = fetchZigArtifact { - name = "spirv_cross"; - url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz"; - hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="; - }; - } - { - name = "N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH"; - path = fetchZigArtifact { - name = "utfcpp"; - url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz"; - hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="; - }; - } - { - name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; - path = fetchZigArtifact { - name = "vaxis"; - url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; - hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; - }; - } - { - name = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t"; - path = fetchZigArtifact { - name = "wayland"; - url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz"; - hash = "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0="; - }; - } - { - name = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S"; - path = fetchZigArtifact { - name = "wayland_protocols"; - url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz"; - hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="; - }; - } - { - name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; - path = fetchZigArtifact { - name = "wuffs"; - url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz"; - hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="; - }; - } - { - name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW"; - path = fetchZigArtifact { - name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz"; - hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="; - }; - } - { - name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; - path = fetchZigArtifact { - name = "zf"; - url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; - hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; - }; - } - { - name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"; - path = fetchZigArtifact { - name = "zg"; - url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc"; - hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="; - }; - } - { - name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ"; - path = fetchZigArtifact { - name = "zig_js"; - url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz"; - hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="; - }; - } - { - name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt"; - path = fetchZigArtifact { - name = "zig_objc"; - url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; - hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; - }; - } - { - name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy"; - path = fetchZigArtifact { - name = "zig_wayland"; - url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz"; - hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="; - }; - } - { - name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; - path = fetchZigArtifact { - name = "zigimg"; - url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; - hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; - }; - } - { - name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; - path = fetchZigArtifact { - name = "ziglyph"; - url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; - hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; - }; - } - { - name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; - path = fetchZigArtifact { - name = "zlib"; - url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz"; - hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw="; - }; - } -] + linkFarm name [ + { + name = "N-V-__8AALw2uwF_03u4JRkZwRLc3Y9hakkYV7NKRR9-RIZJ"; + path = fetchZigArtifact { + name = "breakpad"; + url = "https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918.tar.gz"; + hash = "sha256-bMqYlD0amQdmzvYQd8Ca/1k4Bj/heh7+EijlQSttatk="; + }; + } + { + name = "N-V-__8AAIrfdwARSa-zMmxWwFuwpXf1T3asIN7s5jqi9c1v"; + path = fetchZigArtifact { + name = "fontconfig"; + url = "https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz"; + hash = "sha256-O6LdkhWHGKzsXKrxpxYEO1qgVcJ7CB2RSvPMtA3OilU="; + }; + } + { + name = "N-V-__8AAKLKpwC4H27Ps_0iL3bPkQb-z6ZVSrB-x_3EEkub"; + path = fetchZigArtifact { + name = "freetype"; + url = "https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz"; + hash = "sha256-QnIB9dUVFnDQXB9bRb713aHy592XHvVPD+qqf/0quQw="; + }; + } + { + name = "N-V-__8AADcZkgn4cMhTUpIz6mShCKyqqB-NBtf_S2bHaTC-"; + path = fetchZigArtifact { + name = "gettext"; + url = "https://deps.files.ghostty.org/gettext-0.24.tar.gz"; + hash = "sha256-yRhQPVk9cNr0hE0XWhPYFq+stmfAb7oeydzVACwVGLc="; + }; + } + { + name = "N-V-__8AAMrJSwAUGb9-vTzkNR-5LXS81MR__ZRVfF3tWgG6"; + path = fetchZigArtifact { + name = "glfw"; + url = "https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz"; + hash = "sha256-M3N1XUAlMebBo5X1Py+9YxjKXgZ6eacqWRCbUmwLKQo="; + }; + } + { + name = "N-V-__8AABzkUgISeKGgXAzgtutgJsZc0-kkeqBBscJgMkvy"; + path = fetchZigArtifact { + name = "glslang"; + url = "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz"; + hash = "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="; + }; + } + { + name = "gobject-0.2.0-Skun7IWDlQAOKu4BV7LapIxL9Imbq1JRmzvcIkazvAxR"; + path = fetchZigArtifact { + name = "gobject"; + url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst"; + hash = "sha256-hWcpl0Wd3XydT+RY7+VIoxXPhCzele1Ip76YSh+KmLI="; + }; + } + { + name = "N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr"; + path = fetchZigArtifact { + name = "gtk4_layer_shell"; + url = "https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz"; + hash = "sha256-mChCgSYKXu9bT2OlXxbEv2p4ihAgptsDfssPcfozaYg="; + }; + } + { + name = "N-V-__8AAG02ugUcWec-Ndp-i7JTsJ0dgF8nnJRUInkGLG7G"; + path = fetchZigArtifact { + name = "harfbuzz"; + url = "https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz"; + hash = "sha256-8WNRuv4hRyX+LB1bWfDZPkmQWkskeJn7kNcM/5U6K5s="; + }; + } + { + name = "N-V-__8AAGmZhABbsPJLfbqrh6JTHsXhY6qCaLAQyx25e0XE"; + path = fetchZigArtifact { + name = "highway"; + url = "https://deps.files.ghostty.org/highway-66486a10623fa0d72fe91260f96c892e41aceb06.tar.gz"; + hash = "sha256-h9T4iT704I8iSXNgj/6/lCaKgTgLp5wS6IQZaMgKohI="; + }; + } + { + name = "N-V-__8AAH0GaQC8a52s6vfIxg88OZgFgEW6DFxfSK4lX_l3"; + path = fetchZigArtifact { + name = "imgui"; + url = "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz"; + hash = "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="; + }; + } + { + name = "N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC"; + path = fetchZigArtifact { + name = "iterm2_themes"; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz"; + hash = "sha256-SVvSI8gp1ANAUwIMp/v+/ZUiOZ4mPy4nQHlxzThI2fs="; + }; + } + { + name = "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD"; + path = fetchZigArtifact { + name = "libpng"; + url = "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz"; + hash = "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="; + }; + } + { + name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz"; + path = fetchZigArtifact { + name = "libxev"; + url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz"; + hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="; + }; + } + { + name = "N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK"; + path = fetchZigArtifact { + name = "libxml2"; + url = "https://deps.files.ghostty.org/libxml2-2.11.5.tar.gz"; + hash = "sha256-bCgFni4+60K1tLFkieORamNGwQladP7jvGXNxdiaYhU="; + }; + } + { + name = "N-V-__8AAHjwMQDBXnLq3Q2QhaivE0kE2aD138vtX2Bq1g7c"; + path = fetchZigArtifact { + name = "oniguruma"; + url = "https://deps.files.ghostty.org/oniguruma-1220c15e72eadd0d9085a8af134904d9a0f5dfcbed5f606ad60edc60ebeccd9706bb.tar.gz"; + hash = "sha256-ABqhIC54RI9MC/GkjHblVodrNvFtks4yB+zP1h2Z8qA="; + }; + } + { + name = "N-V-__8AADYiAAB_80AWnH1AxXC0tql9thT-R-DYO1gBqTLc"; + path = fetchZigArtifact { + name = "pixels"; + url = "https://deps.files.ghostty.org/pixels-12207ff340169c7d40c570b4b6a97db614fe47e0d83b5801a932dcd44917424c8806.tar.gz"; + hash = "sha256-Veg7FtCRCCUCvxSb9FfzH0IJLFmCZQ4/+657SIcb8Ro="; + }; + } + { + name = "N-V-__8AAKYZBAB-CFHBKs3u4JkeiT4BMvyHu3Y5aaWF3Bbs"; + path = fetchZigArtifact { + name = "plasma_wayland_protocols"; + url = "https://deps.files.ghostty.org/plasma_wayland_protocols-12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566.tar.gz"; + hash = "sha256-XFi6IUrNjmvKNCbcCLAixGqN2Zeymhs+KLrfccIN9EE="; + }; + } + { + name = "N-V-__8AAPlZGwBEa-gxrcypGBZ2R8Bse4JYSfo_ul8i2jlG"; + path = fetchZigArtifact { + name = "sentry"; + url = "https://deps.files.ghostty.org/sentry-1220446be831adcca918167647c06c7b825849fa3fba5f22da394667974537a9c77e.tar.gz"; + hash = "sha256-KsZJfMjWGo0xCT5HrduMmyxFsWsHBbszSoNbZCPDGN8="; + }; + } + { + name = "N-V-__8AANb6pwD7O1WG6L5nvD_rNMvnSc9Cpg1ijSlTYywv"; + path = fetchZigArtifact { + name = "spirv_cross"; + url = "https://deps.files.ghostty.org/spirv_cross-1220fb3b5586e8be67bc3feb34cbe749cf42a60d628d2953632c2f8141302748c8da.tar.gz"; + hash = "sha256-tStvz8Ref6abHwahNiwVVHNETizAmZVVaxVsU7pmV+M="; + }; + } + { + name = "N-V-__8AAHffAgDU0YQmynL8K35WzkcnMUmBVQHQ0jlcKpjH"; + path = fetchZigArtifact { + name = "utfcpp"; + url = "https://deps.files.ghostty.org/utfcpp-1220d4d18426ca72fc2b7e56ce47273149815501d0d2395c2a98c726b31ba931e641.tar.gz"; + hash = "sha256-/8ZooxDndgfTk/PBizJxXyI9oerExNbgV5oR345rWc8="; + }; + } + { + name = "vaxis-0.1.0-BWNV_FUICQAFZnTCL11TUvnUr1Y0_ZdqtXHhd51d76Rn"; + path = fetchZigArtifact { + name = "vaxis"; + url = "git+https://github.com/rockorager/libvaxis#1f41c121e8fc153d9ce8c6eb64b2bbab68ad7d23"; + hash = "sha256-bNZ3oveT6vPChjimPJ/GGfcdivlAeJdl/xfWM+S/MHY="; + }; + } + { + name = "N-V-__8AAKrHGAAs2shYq8UkE6bGcR1QJtLTyOE_lcosMn6t"; + path = fetchZigArtifact { + name = "wayland"; + url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz"; + hash = "sha256-6kGR1o5DdnflHzqs3ieCmBAUTpMdOXoyfcYDXiw5xQ0="; + }; + } + { + name = "N-V-__8AAKw-DAAaV8bOAAGqA0-oD7o-HNIlPFYKRXSPT03S"; + path = fetchZigArtifact { + name = "wayland_protocols"; + url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz"; + hash = "sha256-XO3K3egbdeYPI+XoO13SuOtO+5+Peb16NH0UiusFMPg="; + }; + } + { + name = "N-V-__8AAAzZywE3s51XfsLbP9eyEw57ae9swYB9aGB6fCMs"; + path = fetchZigArtifact { + name = "wuffs"; + url = "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz"; + hash = "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="; + }; + } + { + name = "z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW"; + path = fetchZigArtifact { + name = "z2d"; + url = "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz"; + hash = "sha256-wiJs6/LUiy+ApC5s7VPypbBukjBr4vjx3v/l9OrT70U="; + }; + } + { + name = "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9"; + path = fetchZigArtifact { + name = "zf"; + url = "https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz"; + hash = "sha256-3nulNQd/4rZ4paeXJYXwAliNNyRNsIOX/q3z1JB8C7I="; + }; + } + { + name = "zg-0.13.4-AAAAAGiZ7QLz4pvECFa_wG4O4TP4FLABHHbemH2KakWM"; + path = fetchZigArtifact { + name = "zg"; + url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc"; + hash = "sha256-fo3l6cjkrr/godElTGnQzalBsasN7J73IDIRmw7v1gA="; + }; + } + { + name = "N-V-__8AAB9YCQBaZtQjJZVndk-g_GDIK-NTZcIa63bFp9yZ"; + path = fetchZigArtifact { + name = "zig_js"; + url = "https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz"; + hash = "sha256-fyNeCVbC9UAaKJY6JhAZlT0A479M/AKYMPIWEZbDWD0="; + }; + } + { + name = "zig_objc-0.0.0-Ir_Sp3TyAADEVRTxXlScq3t_uKAM91MYNerZkHfbD0yt"; + path = fetchZigArtifact { + name = "zig_objc"; + url = "https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz"; + hash = "sha256-zn1tR6xhSmDla4UJ3t+Gni4Ni3R8deSK3tEe7DGzNXw="; + }; + } + { + name = "wayland-0.4.0-dev-lQa1kjfIAQCmhhQu3xF0KH-94-TzeMXOqfnP0-Dg6Wyy"; + path = fetchZigArtifact { + name = "zig_wayland"; + url = "https://codeberg.org/ifreund/zig-wayland/archive/f3c5d503e540ada8cbcb056420de240af0c094f7.tar.gz"; + hash = "sha256-E77GZ15APYbbO1WzmuJi8eG9/iQFbc2CgkNBxjCLUhk="; + }; + } + { + name = "zigimg-0.1.0-lly-O6N2EABOxke8dqyzCwhtUCAafqP35zC7wsZ4Ddxj"; + path = fetchZigArtifact { + name = "zigimg"; + url = "git+https://github.com/TUSF/zigimg#31268548fe3276c0e95f318a6c0d2ab10565b58d"; + hash = "sha256-oblfr2FIzuqq0FLo/RrzCwUX1NJJuT53EwD3nP3KwN0="; + }; + } + { + name = "ziglyph-0.11.2-AAAAAHPtHwB4Mbzn1KvOV7Wpjo82NYEc_v0WC8oCLrkf"; + path = fetchZigArtifact { + name = "ziglyph"; + url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz"; + hash = "sha256-cse98+Ft8QUjX+P88yyYfaxJOJGQ9M7Ymw7jFxDz89k="; + }; + } + { + name = "N-V-__8AAB0eQwD-0MdOEBmz7intriBReIsIDNlukNVoNu6o"; + path = fetchZigArtifact { + name = "zlib"; + url = "https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz"; + hash = "sha256-F+iIY/NgBnKrSRgvIXKBtvxNPHYr3jYZNeQ2qVIU0Fw="; + }; + } + ] From ba5c773f0fbf5bac6c585caf31b9be6a9b6d9541 Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Sun, 27 Apr 2025 00:05:10 -0500 Subject: [PATCH 157/642] update flatpak packages --- flatpak/zig-packages.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index efe022a45..591198afd 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -151,9 +151,9 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/1e89605a624940c310c7a1d81b46a7c5c05919e3.tar.gz", - "dest": "vendor/p/z2d-0.6.0-j5P_HvLdCABu-dXpCeRM7Uk4m16vULg1980lMNCQj4_C", - "sha256": "3c429549467ab5e45b0f28453d64a2b8149ee34a60af4915462bc863c0a7f074" + "url": "https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz", + "dest": "vendor/p/z2d-0.6.1-j5P_HlerCgBokMgrkl9QhJUKXuZWBGdPnH7cSXwv_ScW", + "sha256": "c2226cebf2d48b2f80a42e6ced53f2a5b06e92306be2f8f1deffe5f4ead3ef45" }, { "type": "archive", From c7b8fd1354adb4885259d8104715df28b7211a77 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 27 Apr 2025 07:00:15 -0700 Subject: [PATCH 158/642] flatpak: update dependencies --- flatpak/zig-packages.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 591198afd..0c36a600a 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/c5e4212e763d652a422c22955cbd13038de1a356.tar.gz", - "dest": "vendor/p/N-V-__8AAIVuNwQhgzy1gME091DLGpUf4kDPd5zVEbxg-NVC", - "sha256": "495bd223c829d4034053020ca7fbfefd9522399e263f2e27407971cd3848d9fb" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz", + "dest": "vendor/p/N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym", + "sha256": "572e66ba22778495dc3af9851cb86a73e0ef761ef8186ebebbf2fe12c6af0dbd" }, { "type": "archive", From f0339d5e5b2b86134455c0ed4a4cb114d4a97fdd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 27 Apr 2025 07:01:37 -0700 Subject: [PATCH 159/642] ci: iTerm2 colorscheme update should update flatpak deps --- .github/workflows/update-colorschemes.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index f6147bb96..e8e3fe99a 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -50,6 +50,8 @@ jobs: if ! git diff --exit-code build.zig.zon; then nix develop -c ./nix/build-support/check-zig-cache.sh --update nix develop -c ./nix/build-support/check-zig-cache.sh + nix develop -c ./flatpak/build-support/check-zig-cache.sh --update + nix develop -c ./flatpak/build-support/check-zig-cache.sh fi # Verify the build still works. We choose an arbitrary build type From 42913c783040f9aae1f9c6ed5a6d1615246269dd Mon Sep 17 00:00:00 2001 From: Danil Ovchinnikov Date: Mon, 28 Apr 2025 03:23:24 +0300 Subject: [PATCH 160/642] i18n: fixed the translation for Russian Co-authored-by: TicClick --- po/ru_RU.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 2d13e6de7..9e9cf8077 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -45,7 +45,7 @@ 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 From 13f776d483d5120e6b7fab7b04ed2fd5894e77e6 Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Mon, 28 Apr 2025 10:20:29 +0800 Subject: [PATCH 161/642] Rename maximize notification and refine handler --- .../Terminal/BaseTerminalController.swift | 16 +++++++++------- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- macos/Sources/Ghostty/Package.swift | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 59cef503d..d6b0433ac 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -117,8 +117,8 @@ class BaseTerminalController: NSWindowController, object: nil) center.addObserver( self, - selector: #selector(toggleMaximize), - name: .ghosttyToggleMaximize, + selector: #selector(ghosttyMaximizeDidToggle(_:)), + name: .ghosttyMaximizeDidToggle, object: nil) // Listen for local events that we need to know of outside of @@ -239,6 +239,13 @@ class BaseTerminalController: NSWindowController, toggleCommandPalette(nil) } + @objc private func ghosttyMaximizeDidToggle(_ notification: Notification) { + guard let window else { return } + guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree?.contains(view: surfaceView) ?? false else { return } + window.zoom(nil) + } + // MARK: Local Events private func localEventHandler(_ event: NSEvent) -> NSEvent? { @@ -553,11 +560,6 @@ class BaseTerminalController: NSWindowController, window.performClose(sender) } - @IBAction func toggleMaximize(_ sender: Any) { - guard let window = window else { return } - window.zoom(self) - } - @IBAction func splitRight(_ sender: Any) { guard let surface = focusedSurface?.surface else { return } ghostty.split(surface: surface, direction: GHOSTTY_SPLIT_DIRECTION_RIGHT) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index e72acc05e..f7f5f475c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -783,7 +783,7 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } NotificationCenter.default.post( - name: .ghosttyToggleMaximize, + name: .ghosttyMaximizeDidToggle, object: surfaceView ) diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index ee0073aa1..80223776c 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -259,7 +259,7 @@ extension Notification.Name { static let ghosttyCommandPaletteDidToggle = Notification.Name("com.mitchellh.ghostty.commandPaletteDidToggle") /// Toggle maximize of current window - static let ghosttyToggleMaximize = Notification.Name("com.mitchellh.ghostty.toggleMaximize") + static let ghosttyMaximizeDidToggle = Notification.Name("com.mitchellh.ghostty.maximizeDidToggle") } // NOTE: I am moving all of these to Notification.Name extensions over time. This From e5e89bcbe4f67224b24f27e4ac6bb8b4a4d39ebc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 28 Apr 2025 14:00:35 -0700 Subject: [PATCH 162/642] macos: key input that clears preedit without text shouldn't encode Fixes #7225 --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 5985d64a0..921c32c8b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -975,7 +975,14 @@ extension Ghostty { event: event, translationEvent: translationEvent, text: translationEvent.ghosttyCharacters, - composing: markedText.length > 0 + + // We're composing if we have preedit (the obvious case). But we're also + // composing if we don't have preedit and we had marked text before, + // because this input probably just reset the preedit state. It shouldn't + // be encoded. Example: Japanese begin composing, the press backspace. + // This should only cancel the composing state but not actually delete + // the prior input characters (prior to the composing). + composing: markedText.length > 0 || markedTextBefore ) } } From 4e3975650187c3b6edf2687df7861403f9913b8b Mon Sep 17 00:00:00 2001 From: Kat <65649991+00-kat@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:22:37 +1000 Subject: [PATCH 163/642] Link to GTK CSS docs and add some useful tips to gtk-custom-css' docs. It may not be immediately obvious how to style Ghostty despite knowing of the existence of that configuration option; one who is more accustomed to web development would likely be very reliant on their browser's inspector for modifying and debugging the style of their application. GTK CSS also differs in some important ways from the CSS found in browsers, and hence linking to the GTK CSS documentation would save time for anyone new to styling GTK applications. --- src/config/Config.zig | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index f71e0972d..2db3db430 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2276,6 +2276,18 @@ keybind: Keybinds = .{}, /// Custom CSS files to be loaded. /// +/// GTK CSS documentation can be found at the following links: +/// +/// * - An overview of GTK CSS. +/// * - A comprehensive list +/// of supported CSS properties. +/// +/// Launch Ghostty with `env GTK_DEBUG=interactive ghostty` to tweak Ghostty's +/// CSS in real time using the GTK Inspector. Errors in your CSS files would +/// also be reported in the terminal you started Ghostty from. See +/// for more +/// information about the GTK Inspector. +/// /// This configuration can be repeated multiple times to load multiple files. /// Prepend a ? character to the file path to suppress errors if the file does /// not exist. If you want to include a file that begins with a literal ? From 87107a79343a6e2de802372aa75657ca40461e07 Mon Sep 17 00:00:00 2001 From: trag1c Date: Tue, 29 Apr 2025 18:32:59 +0200 Subject: [PATCH 164/642] ci: drop l10n review workflow --- .github/scripts/request_review.py | 189 ------------------------------ .github/workflows/review.yml | 37 ------ 2 files changed, 226 deletions(-) delete mode 100644 .github/scripts/request_review.py delete mode 100644 .github/workflows/review.yml diff --git a/.github/scripts/request_review.py b/.github/scripts/request_review.py deleted file mode 100644 index d799e7c58..000000000 --- a/.github/scripts/request_review.py +++ /dev/null @@ -1,189 +0,0 @@ -# /// script -# requires-python = ">=3.9" -# dependencies = [ -# "githubkit", -# "loguru", -# ] -# /// - -from __future__ import annotations - -import asyncio -import os -import re -import sys -from collections.abc import Iterator -from contextlib import contextmanager -from itertools import chain - -from githubkit import GitHub -from githubkit.exception import RequestFailed -from loguru import logger - -ORG_NAME = "ghostty-org" -REPO_NAME = "ghostty" -ALLOWED_PARENT_TEAM = "localization" -LOCALIZATION_TEAM_NAME_PATTERN = re.compile(r"[a-z]{2}_[A-Z]{2}") -LEVEL_MAP = {"DEBUG": "DBG", "WARNING": "WRN", "ERROR": "ERR"} - -logger.remove() -logger.add( - sys.stderr, - format=lambda record: ( - "{time:YYYY-MM-DD HH:mm:ss.SSS} | " - f"{LEVEL_MAP[record['level'].name]} | " - "{function}:{line} - " - "{message}\n" - ), - backtrace=True, - diagnose=True, -) - - -@contextmanager -def log_fail(message: str, *, die: bool = True) -> Iterator[None]: - try: - yield - except RequestFailed as exc: - logger.error(message) - logger.error(exc) - logger.error(exc.response.raw_response.json()) - if die: - sys.exit(1) - - -gh = GitHub(os.environ["GITHUB_TOKEN"]) - -with log_fail("Invalid token"): - # Do the simplest request as a test - gh.rest.rate_limit.get() - - -async def fetch_and_parse_codeowners() -> dict[str, str]: - logger.debug("Fetching CODEOWNERS file...") - with log_fail("Failed to fetch CODEOWNERS file"): - content = ( - await gh.rest.repos.async_get_content( - ORG_NAME, - REPO_NAME, - "CODEOWNERS", - headers={"Accept": "application/vnd.github.raw+json"}, - ) - ).text - - logger.debug("Parsing CODEOWNERS file...") - codeowners: dict[str, str] = {} - for line in content.splitlines(): - if not line or line.lstrip().startswith("#"): - continue - - # This assumes that all entries only list one owner - # and that this owner is a team (ghostty-org/foobar) - path, owner = line.split() - path = path.lstrip("/") - owner = owner.removeprefix(f"@{ORG_NAME}/") - - if not is_localization_team(owner): - logger.debug(f"Skipping non-l11n codeowner {owner!r} for {path}") - continue - - codeowners[path] = owner - logger.debug(f"Found codeowner {owner!r} for {path}") - return codeowners - - -async def get_team_members(team_name: str) -> list[str]: - logger.debug(f"Fetching team {team_name!r}...") - with log_fail(f"Failed to fetch team {team_name!r}"): - team = (await gh.rest.teams.async_get_by_name(ORG_NAME, team_name)).parsed_data - - if team.parent and team.parent.slug == ALLOWED_PARENT_TEAM: - logger.debug(f"Fetching team {team_name!r} members...") - with log_fail(f"Failed to fetch team {team_name!r} members"): - resp = await gh.rest.teams.async_list_members_in_org(ORG_NAME, team_name) - members = [m.login for m in resp.parsed_data] - logger.debug(f"Team {team_name!r} members: {', '.join(members)}") - return members - - logger.warning(f"Team {team_name} does not have a {ALLOWED_PARENT_TEAM!r} parent") - return [] - - -async def get_changed_files(pr_number: int) -> list[str]: - logger.debug("Gathering changed files...") - with log_fail("Failed to gather changed files"): - diff_entries = ( - await gh.rest.pulls.async_list_files( - ORG_NAME, - REPO_NAME, - pr_number, - per_page=3000, - headers={"Accept": "application/vnd.github+json"}, - ) - ).parsed_data - return [d.filename for d in diff_entries] - - -async def request_review(pr_number: int, user: str, pr_author: str) -> None: - if user == pr_author: - logger.debug(f"Skipping review request for {user!r} (is PR author)") - logger.debug(f"Requesting review from {user!r}...") - with log_fail(f"Failed to request review from {user}", die=False): - await gh.rest.pulls.async_request_reviewers( - ORG_NAME, - REPO_NAME, - pr_number, - headers={"Accept": "application/vnd.github+json"}, - data={"reviewers": [user]}, - ) - - -def is_localization_team(team_name: str) -> bool: - return LOCALIZATION_TEAM_NAME_PATTERN.fullmatch(team_name) is not None - - -async def get_pr_author(pr_number: int) -> str: - logger.debug("Fetching PR author...") - with log_fail("Failed to fetch PR author"): - resp = await gh.rest.pulls.async_get(ORG_NAME, REPO_NAME, pr_number) - pr_author = resp.parsed_data.user.login - logger.debug(f"Found author: {pr_author!r}") - return pr_author - - -async def main() -> None: - logger.debug("Reading PR number...") - pr_number = int(os.environ["PR_NUMBER"]) - logger.debug(f"Starting review request process for PR #{pr_number}...") - - changed_files = await get_changed_files(pr_number) - logger.debug(f"Changed files: {', '.join(map(repr, changed_files))}") - - pr_author = await get_pr_author(pr_number) - codeowners = await fetch_and_parse_codeowners() - - found_owners = set[str]() - for file in changed_files: - logger.debug(f"Finding owner for {file!r}...") - for path, owner in codeowners.items(): - if file.startswith(path): - logger.debug(f"Found owner: {owner!r}") - break - else: - logger.debug("No owner found") - continue - found_owners.add(owner) - - member_lists = await asyncio.gather( - *(get_team_members(owner) for owner in found_owners) - ) - await asyncio.gather( - *( - request_review(pr_number, user, pr_author) - for user in chain.from_iterable(member_lists) - ) - ) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/.github/workflows/review.yml b/.github/workflows/review.yml deleted file mode 100644 index 9abe0b5e2..000000000 --- a/.github/workflows/review.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Request Review - -on: - pull_request: - types: - - opened - - synchronize - -env: - PY_COLORS: 1 - -jobs: - review: - runs-on: namespace-profile-ghostty-xsm - steps: - - uses: actions/checkout@v4 - - - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 - with: - path: | - /nix - /zig - - - uses: cachix/install-nix-action@v30 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Request Localization Review - env: - GITHUB_TOKEN: ${{ secrets.GH_REVIEW_TOKEN }} - PR_NUMBER: ${{ github.event.pull_request.number }} - run: nix develop -c uv run .github/scripts/request_review.py From 2c1ade763fedbb8a0e402b341778d7608eac3a09 Mon Sep 17 00:00:00 2001 From: Tim Culverhouse Date: Tue, 29 Apr 2025 18:42:16 -0500 Subject: [PATCH 165/642] terminal(dcs): convert all xtgettcap queries to upper XTGETTCAP queries are a semicolon-delimited list of hex encoded terminfo capability names. Ghostty encodes a map using upper case hex encodings, meaning when an application uses a lower case encoding the capability is not found. To fix, we convert the entire list we receive in the query to upper case prior to processing further. Fixes: #7229 --- src/terminal/dcs.zig | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/terminal/dcs.zig b/src/terminal/dcs.zig index da6f3ae23..db5f95c4f 100644 --- a/src/terminal/dcs.zig +++ b/src/terminal/dcs.zig @@ -162,7 +162,12 @@ pub const Handler = struct { break :tmux .{ .tmux = .{ .exit = {} } }; }, - .xtgettcap => |list| .{ .xtgettcap = .{ .data = list } }, + .xtgettcap => |list| xtgettcap: { + for (list.items, 0..) |b, i| { + list.items[i] = std.ascii.toUpper(b); + } + break :xtgettcap .{ .xtgettcap = .{ .data = list } }; + }, .decrqss => |buffer| .{ .decrqss = switch (buffer.len) { 0 => .none, @@ -306,6 +311,21 @@ test "XTGETTCAP command" { try testing.expect(cmd.xtgettcap.next() == null); } +test "XTGETTCAP mixed case" { + const testing = std.testing; + const alloc = testing.allocator; + + var h: Handler = .{}; + defer h.deinit(); + try testing.expect(h.hook(alloc, .{ .intermediates = "+", .final = 'q' }) == null); + for ("536d756C78") |byte| _ = h.put(byte); + var cmd = h.unhook().?; + defer cmd.deinit(); + try testing.expect(cmd == .xtgettcap); + try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); + try testing.expect(cmd.xtgettcap.next() == null); +} + test "XTGETTCAP command multiple keys" { const testing = std.testing; const alloc = testing.allocator; @@ -333,7 +353,7 @@ test "XTGETTCAP command invalid data" { var cmd = h.unhook().?; defer cmd.deinit(); try testing.expect(cmd == .xtgettcap); - try testing.expectEqualStrings("who", cmd.xtgettcap.next().?); + try testing.expectEqualStrings("WHO", cmd.xtgettcap.next().?); try testing.expectEqualStrings("536D756C78", cmd.xtgettcap.next().?); try testing.expect(cmd.xtgettcap.next() == null); } From 529129204717f2321a8d6b8ac3bbd9a8d3a1d495 Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 30 Apr 2025 15:47:00 +0900 Subject: [PATCH 166/642] Add `runtimeUntil`, fix remove GDK_DEBUG `gl-no-fractional` --- src/apprt/gtk/App.zig | 6 ++++-- src/apprt/gtk/gtk_version.zig | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 72c0d7509..0ef9f33c6 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -190,7 +190,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // For the remainder of "why" see the 4.14 comment below. gdk_disable.@"gles-api" = true; gdk_disable.vulkan = true; - gdk_debug.@"gl-no-fractional" = true; break :environment; } if (gtk_version.runtimeAtLeast(4, 14, 0)) { @@ -201,8 +200,11 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // // Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589 gdk_debug.@"gl-disable-gles" = true; - gdk_debug.@"gl-no-fractional" = true; gdk_debug.@"vulkan-disable" = true; + + if (gtk_version.runtimeUntil(4, 17, 5)) { + gdk_debug.@"gl-no-fractional" = true; + } break :environment; } // Versions prior to 4.14 are a bit of an unknown for Ghostty. It diff --git a/src/apprt/gtk/gtk_version.zig b/src/apprt/gtk/gtk_version.zig index 59d7a5782..5d75fb4fe 100644 --- a/src/apprt/gtk/gtk_version.zig +++ b/src/apprt/gtk/gtk_version.zig @@ -87,10 +87,23 @@ pub inline fn runtimeAtLeast( }) != .lt; } +pub inline fn runtimeUntil( + comptime major: u16, + comptime minor: u16, + comptime micro: u16, +) bool { + const runtime_version = getRuntimeVersion(); + return runtime_version.order(.{ + .major = major, + .minor = minor, + .patch = micro, + }) == .lt; +} + test "atLeast" { const testing = std.testing; - const funs = &.{ atLeast, runtimeAtLeast }; + const funs = &.{ atLeast, runtimeAtLeast, runtimeUntil }; inline for (funs) |fun| { try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION)); From 6f4fe56b931bc17b1a0f5e590fb538e6a8a13188 Mon Sep 17 00:00:00 2001 From: Morgan Date: Wed, 30 Apr 2025 23:48:15 +0900 Subject: [PATCH 167/642] Add comment about `gl-no-fractional` --- src/apprt/gtk/App.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 0ef9f33c6..9bd61939a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -159,6 +159,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { opengl: bool = false, /// disable GLES, Ghostty can't use GLES @"gl-disable-gles": bool = false, + // GTK's new renderer can cause blurry font when using fractional scaling. @"gl-no-fractional": bool = false, /// Disabling Vulkan can improve startup times by hundreds of /// milliseconds on some systems. We don't use Vulkan so we can just @@ -203,6 +204,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { gdk_debug.@"vulkan-disable" = true; if (gtk_version.runtimeUntil(4, 17, 5)) { + // Removed at GTK v4.17.5 gdk_debug.@"gl-no-fractional" = true; } break :environment; From 0af5a291acca1dddcb83448308b71aa3e91f5b25 Mon Sep 17 00:00:00 2001 From: Leorize Date: Thu, 1 May 2025 01:36:41 -0500 Subject: [PATCH 168/642] apprt/gtk: ensure configuration is loaded on startup Restores the app configuration code removed in https://github.com/ghostty-org/ghostty/pull/6792. The was unnoticed due to `colorSchemeEvent` triggering a configuration reload if `window-theme` deviates from the default (i.e. dark mode is used). Fixes https://github.com/ghostty-org/ghostty/discussions/7206 --- src/apprt/gtk/App.zig | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 72c0d7509..5373e578c 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1291,6 +1291,13 @@ pub fn run(self: *App) !void { // Setup our actions self.initActions(); + // On startup, we want to check for configuration errors right away + // so we can show our error window. We also need to setup other initial + // state. + self.syncConfigChanges(null) catch |err| { + log.warn("error handling configuration changes err={}", .{err}); + }; + while (self.running) { _ = glib.MainContext.iteration(self.ctx, 1); From f83729ba4874666023ab39b111db98ca029fb02b Mon Sep 17 00:00:00 2001 From: Martin Hettiger Date: Mon, 24 Mar 2025 22:13:50 +0100 Subject: [PATCH 169/642] macos: add float on top feature for terminal windows --- macos/Sources/App/macOS/AppDelegate.swift | 51 +++++++++++++++++++ macos/Sources/App/macOS/MainMenu.xib | 15 ++++++ .../Features/Terminal/TerminalWindow.swift | 5 ++ 3 files changed, 71 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 682099e92..169f57ff2 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -52,6 +52,8 @@ class AppDelegate: NSObject, @IBOutlet private var menuSelectSplitLeft: NSMenuItem? @IBOutlet private var menuSelectSplitRight: NSMenuItem? @IBOutlet private var menuReturnToDefaultSize: NSMenuItem? + @IBOutlet private var menuFloatOnTop: NSMenuItem? + @IBOutlet private var menuUseAsDefault: NSMenuItem? @IBOutlet private var menuIncreaseFontSize: NSMenuItem? @IBOutlet private var menuDecreaseFontSize: NSMenuItem? @@ -175,6 +177,12 @@ class AppDelegate: NSObject, handler: localEventHandler) // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(windowDidBecomeKey), + name: NSWindow.didBecomeKeyNotification, + object: nil + ) NotificationCenter.default.addObserver( self, selector: #selector(quickTerminalDidChangeVisibility), @@ -497,6 +505,16 @@ class AppDelegate: NSObject, return event } + @objc private func windowDidBecomeKey(_ notification: Notification) { + guard let terminal = notification.object as? TerminalWindow else { + // If some other window became key we always turn this off + self.menuFloatOnTop?.state = .off + return + } + + self.menuFloatOnTop?.state = terminal.level == .floating ? .on : .off + } + @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { guard let quickController = notification.object as? QuickTerminalController else { return } self.menuQuickTerminal?.state = if (quickController.visible) { .on } else { .off } @@ -844,6 +862,22 @@ class AppDelegate: NSObject, hiddenState = nil } + @IBAction func floatOnTop(_ menuItem: NSMenuItem) { + menuItem.state = menuItem.state == .on ? .off : .on + guard let window = NSApp.keyWindow else { return } + window.level = menuItem.state == .on ? .floating : .normal + } + + @IBAction func useAsDefault(_ sender: NSMenuItem) { + let ud = UserDefaults.standard + let key = TerminalWindow.defaultLevelKey + if (menuFloatOnTop?.state == .on) { + ud.set(NSWindow.Level.floating, forKey: key) + } else { + ud.removeObject(forKey: key) + } + } + @IBAction func bringAllToFront(_ sender: Any) { if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) @@ -899,3 +933,20 @@ class AppDelegate: NSObject, } } } + +// MARK: NSMenuItemValidation + +extension AppDelegate: NSMenuItemValidation { + func validateMenuItem(_ item: NSMenuItem) -> Bool { + switch item.action { + case #selector(floatOnTop(_:)), + #selector(useAsDefault(_:)): + // Float on top items only active if the key window is a primary + // terminal window (not quick terminal). + return NSApp.keyWindow is TerminalWindow + + default: + return true + } + } +} diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 8f7b16aa9..724f21355 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -25,6 +25,7 @@ + @@ -56,6 +57,7 @@ +
@@ -402,6 +404,19 @@ + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 3209449e4..62b8dc5bf 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -1,6 +1,9 @@ import Cocoa class TerminalWindow: NSWindow { + /// This is the key in UserDefaults to use for the default `level` value. + static let defaultLevelKey: String = "TerminalDefaultLevel" + @objc dynamic var keyEquivalent: String = "" /// This is used to determine if certain elements should be drawn light or dark and should @@ -63,6 +66,8 @@ class TerminalWindow: NSWindow { if titlebarTabs { generateToolbar() } + + level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } deinit { From 6e11d947e7ae6f37567faddb863b48107cdf278b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 1 May 2025 09:29:34 -0700 Subject: [PATCH 170/642] Binding for toggling window float on top (macOS only) This adds a keybinding and apprt action for #7237. --- include/ghostty.h | 9 +++ macos/Sources/App/macOS/AppDelegate.swift | 55 ++++++++------- macos/Sources/Ghostty/Ghostty.App.swift | 40 +++++++++++ macos/Sources/Ghostty/Package.swift | 22 ++++++ src/Surface.zig | 6 ++ src/apprt/action.zig | 11 +++ src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 1 + src/input/Binding.zig | 82 +++++++++++++---------- src/input/command.zig | 6 ++ 10 files changed, 173 insertions(+), 60 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index f7504eb7e..18c547910 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -429,6 +429,13 @@ typedef enum { GHOSTTY_FULLSCREEN_NON_NATIVE_PADDED_NOTCH, } ghostty_action_fullscreen_e; +// apprt.action.FloatWindow +typedef enum { + GHOSTTY_FLOAT_WINDOW_ON, + GHOSTTY_FLOAT_WINDOW_OFF, + GHOSTTY_FLOAT_WINDOW_TOGGLE, +} ghostty_action_float_window_e; + // apprt.action.SecureInput typedef enum { GHOSTTY_SECURE_INPUT_ON, @@ -610,6 +617,7 @@ typedef enum { GHOSTTY_ACTION_RENDERER_HEALTH, GHOSTTY_ACTION_OPEN_CONFIG, GHOSTTY_ACTION_QUIT_TIMER, + GHOSTTY_ACTION_FLOAT_WINDOW, GHOSTTY_ACTION_SECURE_INPUT, GHOSTTY_ACTION_KEY_SEQUENCE, GHOSTTY_ACTION_COLOR_CHANGE, @@ -638,6 +646,7 @@ typedef union { ghostty_action_mouse_over_link_s mouse_over_link; ghostty_action_renderer_health_e renderer_health; ghostty_action_quit_timer_e quit_timer; + ghostty_action_float_window_e float_window; ghostty_action_secure_input_e secure_input; ghostty_action_key_sequence_s key_sequence; ghostty_action_color_change_s color_change; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 169f57ff2..a3a3185d9 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -414,6 +414,7 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "prompt_surface_title", menuItem: self.menuChangeTitle) syncMenuShortcut(config, action: "toggle_quick_terminal", menuItem: self.menuQuickTerminal) syncMenuShortcut(config, action: "toggle_visibility", menuItem: self.menuToggleVisibility) + syncMenuShortcut(config, action: "toggle_window_float_on_top", menuItem: self.menuFloatOnTop) syncMenuShortcut(config, action: "inspector:toggle", menuItem: self.menuTerminalInspector) syncMenuShortcut(config, action: "toggle_command_palette", menuItem: self.menuCommandPalette) @@ -506,13 +507,7 @@ class AppDelegate: NSObject, } @objc private func windowDidBecomeKey(_ notification: Notification) { - guard let terminal = notification.object as? TerminalWindow else { - // If some other window became key we always turn this off - self.menuFloatOnTop?.state = .off - return - } - - self.menuFloatOnTop?.state = terminal.level == .floating ? .on : .off + syncFloatOnTopMenu(notification.object as? NSWindow) } @objc private func quickTerminalDidChangeVisibility(_ notification: Notification) { @@ -862,22 +857,6 @@ class AppDelegate: NSObject, hiddenState = nil } - @IBAction func floatOnTop(_ menuItem: NSMenuItem) { - menuItem.state = menuItem.state == .on ? .off : .on - guard let window = NSApp.keyWindow else { return } - window.level = menuItem.state == .on ? .floating : .normal - } - - @IBAction func useAsDefault(_ sender: NSMenuItem) { - let ud = UserDefaults.standard - let key = TerminalWindow.defaultLevelKey - if (menuFloatOnTop?.state == .on) { - ud.set(NSWindow.Level.floating, forKey: key) - } else { - ud.removeObject(forKey: key) - } - } - @IBAction func bringAllToFront(_ sender: Any) { if !NSApp.isActive { NSApp.activate(ignoringOtherApps: true) @@ -934,6 +913,36 @@ class AppDelegate: NSObject, } } +// MARK: Floating Windows + +extension AppDelegate { + func syncFloatOnTopMenu(_ window: NSWindow?) { + guard let window = (window ?? NSApp.keyWindow) as? TerminalWindow else { + // If some other window became key we always turn this off + self.menuFloatOnTop?.state = .off + return + } + + self.menuFloatOnTop?.state = window.level == .floating ? .on : .off + } + + @IBAction func floatOnTop(_ menuItem: NSMenuItem) { + menuItem.state = menuItem.state == .on ? .off : .on + guard let window = NSApp.keyWindow else { return } + window.level = menuItem.state == .on ? .floating : .normal + } + + @IBAction func useAsDefault(_ sender: NSMenuItem) { + let ud = UserDefaults.standard + let key = TerminalWindow.defaultLevelKey + if (menuFloatOnTop?.state == .on) { + ud.set(NSWindow.Level.floating, forKey: key) + } else { + ud.removeObject(forKey: key) + } + } +} + // MARK: NSMenuItemValidation extension AppDelegate: NSMenuItemValidation { diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 677129960..c06287087 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -496,6 +496,9 @@ extension Ghostty { case GHOSTTY_ACTION_OPEN_CONFIG: ghostty_config_open() + case GHOSTTY_ACTION_FLOAT_WINDOW: + toggleFloatWindow(app, target: target, mode: action.action.float_window) + case GHOSTTY_ACTION_SECURE_INPUT: toggleSecureInput(app, target: target, mode: action.action.secure_input) @@ -1026,6 +1029,43 @@ extension Ghostty { } } + private static func toggleFloatWindow( + _ app: ghostty_app_t, + target: ghostty_target_s, + mode mode_raw: ghostty_action_float_window_e + ) { + guard let mode = SetFloatWIndow.from(mode_raw) else { return } + + switch (target.tag) { + case GHOSTTY_TARGET_APP: + Ghostty.logger.warning("toggle float window does nothing with an app target") + return + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return } + guard let surfaceView = self.surfaceView(from: surface) else { return } + guard let window = surfaceView.window as? TerminalWindow else { return } + + switch (mode) { + case .on: + window.level = .floating + + case .off: + window.level = .normal + + case .toggle: + window.level = window.level == .floating ? .normal : .floating + } + + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + appDelegate.syncFloatOnTopMenu(window) + } + + default: + assertionFailure() + } + } + private static func toggleSecureInput( _ app: ghostty_app_t, target: ghostty_target_s, diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index e2c770899..366bb8113 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -42,6 +42,28 @@ extension Ghostty { // MARK: Swift Types for C Types extension Ghostty { + enum SetFloatWIndow { + case on + case off + case toggle + + static func from(_ c: ghostty_action_float_window_e) -> Self? { + switch (c) { + case GHOSTTY_FLOAT_WINDOW_ON: + return .on + + case GHOSTTY_FLOAT_WINDOW_OFF: + return .off + + case GHOSTTY_FLOAT_WINDOW_TOGGLE: + return .toggle + + default: + return nil + } + } + } + enum SetSecureInput { case on case off diff --git a/src/Surface.zig b/src/Surface.zig index c776fed36..6e62f6639 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4289,6 +4289,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .toggle_window_float_on_top => return try self.rt_app.performAction( + .{ .surface = self }, + .float_window, + .toggle, + ), + .toggle_secure_input => return try self.rt_app.performAction( .{ .surface = self }, .secure_input, diff --git a/src/apprt/action.zig b/src/apprt/action.zig index da0ebf8e6..4be296f09 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -205,6 +205,10 @@ pub const Action = union(Key) { /// happen and can be ignored or cause a restart it isn't that important. quit_timer: QuitTimer, + /// Set the window floating state. A floating window is one that is + /// always on top of other windows even when not focused. + float_window: FloatWindow, + /// Set the secure input functionality on or off. "Secure input" means /// that the user is currently at some sort of prompt where they may be /// entering a password or other sensitive information. This can be used @@ -289,6 +293,7 @@ pub const Action = union(Key) { renderer_health, open_config, quit_timer, + float_window, secure_input, key_sequence, color_change, @@ -425,6 +430,12 @@ pub const Fullscreen = enum(c_int) { macos_non_native_padded_notch, }; +pub const FloatWindow = enum(c_int) { + on, + off, + toggle, +}; + pub const SecureInput = enum(c_int) { on, off, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 66b994051..9d1c8a6b5 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -235,6 +235,7 @@ pub const App = struct { .inspector, .render_inspector, .quit_timer, + .float_window, .secure_input, .key_sequence, .desktop_notification, diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 5373e578c..2de22d8c2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -488,6 +488,7 @@ pub fn performAction( // Unimplemented .close_all_windows, + .float_window, .toggle_command_palette, .toggle_visibility, .cell_size, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 1a2961a53..10e16f1fe 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -222,12 +222,12 @@ pub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool { pub const Action = union(enum) { /// Ignore this key combination, don't send it to the child process, just /// black hole it. - ignore: void, + ignore, /// This action is used to flag that the binding should be removed from /// the set. This should never exist in an active set and `set.put` has an /// assertion to verify this. - unbind: void, + unbind, /// Send a CSI sequence. The value should be the CSI sequence without the /// CSI header (`ESC [` or `\x1b[`). @@ -252,35 +252,35 @@ pub const Action = union(enum) { /// If you do this while in a TUI program such as vim, this may break /// the program. If you do this while in a shell, you may have to press /// enter after to get a new prompt. - reset: void, + reset, /// Copy and paste. - copy_to_clipboard: void, - paste_from_clipboard: void, - paste_from_selection: void, + copy_to_clipboard, + paste_from_clipboard, + paste_from_selection, /// Copy the URL under the cursor to the clipboard. If there is no /// URL under the cursor, this does nothing. - copy_url_to_clipboard: void, + copy_url_to_clipboard, /// Increase/decrease the font size by a certain amount. increase_font_size: f32, decrease_font_size: f32, /// Reset the font size to the original configured size. - reset_font_size: void, + reset_font_size, /// Clear the screen. This also clears all scrollback. - clear_screen: void, + clear_screen, /// Select all text on the screen. - select_all: void, + select_all, /// Scroll the screen varying amounts. - scroll_to_top: void, - scroll_to_bottom: void, - scroll_page_up: void, - scroll_page_down: void, + scroll_to_top, + scroll_to_bottom, + scroll_page_up, + scroll_page_down, scroll_page_fractional: f32, scroll_page_lines: i16, @@ -321,19 +321,19 @@ pub const Action = union(enum) { /// Open a new window. If the application isn't currently focused, /// this will bring it to the front. - new_window: void, + new_window, /// Open a new tab. - new_tab: void, + new_tab, /// Go to the previous tab. - previous_tab: void, + previous_tab, /// Go to the next tab. - next_tab: void, + next_tab, /// Go to the last tab (the one with the highest index) - last_tab: void, + last_tab, /// Go to the tab with the specific number, 1-indexed. If the tab number /// is higher than the number of tabs, this will go to the last tab. @@ -346,10 +346,10 @@ pub const Action = union(enum) { /// Toggle the tab overview. /// This only works with libadwaita enabled currently. - toggle_tab_overview: void, + toggle_tab_overview, /// Change the title of the current focused surface via a prompt. - prompt_surface_title: void, + prompt_surface_title, /// Create a new split in the given direction. /// @@ -365,7 +365,7 @@ pub const Action = union(enum) { goto_split: SplitFocusDirection, /// zoom/unzoom the current split. - toggle_split_zoom: void, + toggle_split_zoom, /// Resize the current split in a given direction. /// @@ -378,12 +378,12 @@ pub const Action = union(enum) { resize_split: SplitResizeParameter, /// Equalize all splits in the current window - equalize_splits: void, + equalize_splits, /// Reset the window to the default size. The "default size" is the /// size that a new window would be created with. This has no effect /// if the window is fullscreen. - reset_window_size: void, + reset_window_size, /// Control the terminal inspector visibility. /// @@ -397,39 +397,46 @@ pub const Action = union(enum) { /// Open the configuration file in the default OS editor. If your default OS /// editor isn't configured then this will fail. Currently, any failures to /// open the configuration will show up only in the logs. - open_config: void, + open_config, /// Reload the configuration. The exact meaning depends on the app runtime /// in use but this usually involves re-reading the configuration file /// and applying any changes. Note that not all changes can be applied at /// runtime. - reload_config: void, + reload_config, /// Close the current "surface", whether that is a window, tab, split, etc. /// This only closes ONE surface. This will trigger close confirmation as /// configured. - close_surface: void, + close_surface, /// Close the current tab, regardless of how many splits there may be. /// This will trigger close confirmation as configured. - close_tab: void, + close_tab, /// Close the window, regardless of how many tabs or splits there may be. /// This will trigger close confirmation as configured. - close_window: void, + close_window, /// Close all windows. This will trigger close confirmation as configured. /// This only works for macOS currently. - close_all_windows: void, + close_all_windows, /// Toggle maximized window state. This only works on Linux. - toggle_maximize: void, + toggle_maximize, /// Toggle fullscreen mode of window. - toggle_fullscreen: void, + toggle_fullscreen, /// Toggle window decorations on and off. This only works on Linux. - toggle_window_decorations: void, + toggle_window_decorations, + + /// Toggle whether the terminal window is always on top of other + /// windows even when it is not focused. Terminal windows always start + /// as normal (not always on top) windows. + /// + /// This only works on macOS. + toggle_window_float_on_top, /// Toggle secure input mode on or off. This is used to prevent apps /// that monitor input from seeing what you type. This is useful for @@ -439,7 +446,7 @@ pub const Action = union(enum) { /// terminal. You must toggle it off to disable it, or quit Ghostty. /// /// This only works on macOS, since this is a system API on macOS. - toggle_secure_input: void, + toggle_secure_input, /// Toggle the command palette. The command palette is a UI element /// that lets you see what actions you can perform, their associated @@ -488,7 +495,7 @@ pub const Action = union(enum) { /// plugin enabled, open System Settings > Apps & Windows > Window /// Management > Desktop Effects, and enable the plugin in the plugin list. /// Ghostty would then need to be restarted for this to take effect. - toggle_quick_terminal: void, + toggle_quick_terminal, /// Show/hide all windows. If all windows become shown, we also ensure /// Ghostty becomes focused. When hiding all windows, focus is yielded @@ -497,10 +504,10 @@ pub const Action = union(enum) { /// Note: When the focused surface is fullscreen, this method does nothing. /// /// This currently only works on macOS. - toggle_visibility: void, + toggle_visibility, /// Quit ghostty. - quit: void, + quit, /// Crash ghostty in the desired thread for the focused surface. /// @@ -797,6 +804,7 @@ pub const Action = union(enum) { .toggle_maximize, .toggle_fullscreen, .toggle_window_decorations, + .toggle_window_float_on_top, .toggle_secure_input, .toggle_command_palette, .reset_window_size, diff --git a/src/input/command.zig b/src/input/command.zig index c757736c7..701d537a1 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -346,6 +346,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle the window decorations.", }}, + .toggle_window_float_on_top => comptime &.{.{ + .action = .toggle_window_float_on_top, + .title = "Toggle Float on Top", + .description = "Toggle the float on top state of the current window.", + }}, + .toggle_secure_input => comptime &.{.{ .action = .toggle_secure_input, .title = "Toggle Secure Input", From 0b5160e9f01b6d939a1c213478cbcbec2982e3aa Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 1 May 2025 18:51:24 +0200 Subject: [PATCH 171/642] implement dark/light theme filtering in theme preview --- src/cli/list_themes.zig | 45 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 8ebac4487..54e71b96f 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -24,6 +24,12 @@ pub const Options = struct { /// If true, force a plain list of themes. plain: bool = false, + /// If true, print only the dark themes. + dark: bool = false, + + /// If true, print only the light themes. + light: bool = false, + pub fn deinit(self: Options) void { _ = self; } @@ -137,11 +143,30 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { if (std.mem.eql(u8, entry.name, ".DS_Store")) continue; count += 1; - try themes.append(.{ - .location = loc.location, - .path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }), - .theme = try alloc.dupe(u8, entry.name), - }); + + const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }); + // if there is no need to filter just append the theme to the list + if (!opts.dark and !opts.light) { + try themes.append(.{ + .path = path, + .location = loc.location, + .theme = try alloc.dupe(u8, entry.name), + }); + continue; + } + + // otherwise check if the theme should be included based on the provided options + var config = try Config.default(alloc); + defer config.deinit(); + try config.loadFile(config._arena.?.allocator(), path); + + if (shouldIncludeTheme(opts, config)) { + try themes.append(.{ + .path = path, + .location = loc.location, + .theme = try alloc.dupe(u8, entry.name), + }); + } }, else => {}, } @@ -1594,3 +1619,13 @@ fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement) !void { defer app.deinit(); try app.run(); } + +fn shouldIncludeTheme(opts: Options, theme_config: Config) bool { + const rf = @as(f32, @floatFromInt(theme_config.background.r)) / 255.0; + const gf = @as(f32, @floatFromInt(theme_config.background.g)) / 255.0; + const bf = @as(f32, @floatFromInt(theme_config.background.b)) / 255.0; + const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf; + const is_dark = luminance < 0.5; + + return (opts.dark and is_dark) or (opts.light and !is_dark); +} From 418c46538c740c202ae7c35e5756b90d9fcd91a9 Mon Sep 17 00:00:00 2001 From: Maciej Bartczak <39600846+maciekbartczak@users.noreply.github.com> Date: Thu, 1 May 2025 19:41:02 +0200 Subject: [PATCH 172/642] use enum for the color scheme args --- src/cli/list_themes.zig | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 54e71b96f..54f4c0969 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -24,11 +24,8 @@ pub const Options = struct { /// If true, force a plain list of themes. plain: bool = false, - /// If true, print only the dark themes. - dark: bool = false, - - /// If true, print only the light themes. - light: bool = false, + /// Specifies the color scheme of the themes to include in the list. + color: enum { all, dark, light } = .all, pub fn deinit(self: Options) void { _ = self; @@ -99,6 +96,9 @@ const ThemeListElement = struct { /// * `--path`: Show the full path to the theme. /// /// * `--plain`: Force a plain listing of themes. +/// +/// * `--color`: Specify the color scheme of the themes included in the list. +/// This can be `dark`, `light`, or `all`. The default is `all`. pub fn run(gpa_alloc: std.mem.Allocator) !u8 { var opts: Options = .{}; defer opts.deinit(); @@ -146,7 +146,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 { const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name }); // if there is no need to filter just append the theme to the list - if (!opts.dark and !opts.light) { + if (opts.color == .all) { try themes.append(.{ .path = path, .location = loc.location, @@ -1627,5 +1627,5 @@ fn shouldIncludeTheme(opts: Options, theme_config: Config) bool { const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf; const is_dark = luminance < 0.5; - return (opts.dark and is_dark) or (opts.light and !is_dark); + return (opts.color == .dark and is_dark) or (opts.color == .light and !is_dark); } From cfedd477b2843c4624aac88b76ec24cd7ecd36d6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 30 Apr 2025 14:02:46 -0600 Subject: [PATCH 173/642] font/freetype: introduce mutexes to ensure thread safety of Library and Face For details see comments and FreeType docs @ https://freetype.org/freetype2/docs/reference/ft2-library_setup.html#ft_library https://freetype.org/freetype2/docs/reference/ft2-face_creation.html#ft_face tl;dr: FT_New_Face and FT_Done_Face require the Library to be locked for thread safety, and FT_Load_Glyph and FT_Render_Glyph and friends need the face to be locked for thread safety, since we're sharing faces across threads. --- src/font/CodepointResolver.zig | 6 +-- src/font/Collection.zig | 20 ++++---- src/font/DeferredFace.zig | 4 +- src/font/SharedGrid.zig | 2 +- src/font/SharedGridSet.zig | 2 +- src/font/face/coretext.zig | 17 ++++--- src/font/face/freetype.zig | 87 +++++++++++++++++++++++++--------- src/font/library.zig | 24 ++++++++-- src/font/opentype/svg.zig | 2 +- src/font/shaper/coretext.zig | 2 +- src/font/shaper/harfbuzz.zig | 2 +- 11 files changed, 115 insertions(+), 53 deletions(-) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 326ca0186..37093b59a 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -380,7 +380,7 @@ test getIndex { const testEmoji = font.embedded.emoji; const testEmojiText = font.embedded.emoji_text; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = Collection.init(); @@ -461,7 +461,7 @@ test "getIndex disabled font style" { var atlas_grayscale = try font.Atlas.init(alloc, 512, .grayscale); defer atlas_grayscale.deinit(alloc); - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = Collection.init(); @@ -513,7 +513,7 @@ test "getIndex box glyph" { const testing = std.testing; const alloc = testing.allocator; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); const c = Collection.init(); diff --git a/src/font/Collection.zig b/src/font/Collection.zig index cfc633b04..66e13c109 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -78,8 +78,8 @@ pub const AddError = Allocator.Error || error{ /// next in priority if others exist already, i.e. it'll be the _last_ to be /// searched for a glyph in that list. /// -/// The collection takes ownership of the face. The face will be deallocated -/// when the collection is deallocated. +/// 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 @@ -700,7 +700,7 @@ test "add full" { const alloc = testing.allocator; const testFont = font.embedded.regular; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -746,7 +746,7 @@ test getFace { const alloc = testing.allocator; const testFont = font.embedded.regular; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -770,7 +770,7 @@ test getIndex { const alloc = testing.allocator; const testFont = font.embedded.regular; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -801,7 +801,7 @@ test completeStyles { const alloc = testing.allocator; const testFont = font.embedded.regular; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -828,7 +828,7 @@ test setSize { const alloc = testing.allocator; const testFont = font.embedded.regular; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -851,7 +851,7 @@ test hasCodepoint { const alloc = testing.allocator; const testFont = font.embedded.regular; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -875,7 +875,7 @@ test "hasCodepoint emoji default graphical" { const alloc = testing.allocator; const testEmoji = font.embedded.emoji; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); @@ -898,7 +898,7 @@ test "metrics" { const alloc = testing.allocator; const testFont = font.embedded.inconsolata; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var c = init(); diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 3ee104386..d61f5492f 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -407,7 +407,7 @@ test "fontconfig" { const alloc = testing.allocator; // Load freetype - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); // Get a deferred face from fontconfig @@ -437,7 +437,7 @@ test "coretext" { const alloc = testing.allocator; // Load freetype - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); // Get a deferred face from fontconfig diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 65c7ecd87..72e97fad8 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -338,7 +338,7 @@ test getIndex { const alloc = testing.allocator; // const testEmoji = @import("test.zig").fontEmoji; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var grid = try testGrid(.normal, alloc, lib); diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index ca535eaf8..8ad30629e 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -50,7 +50,7 @@ pub const InitError = Library.InitError; /// Initialize a new SharedGridSet. pub fn init(alloc: Allocator) InitError!SharedGridSet { - var font_lib = try Library.init(); + var font_lib = try Library.init(alloc); errdefer font_lib.deinit(); return .{ diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 3749b4824..639eae43c 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -46,7 +46,11 @@ pub const Face = struct { }; /// Initialize a CoreText-based font from a TTF/TTC in memory. - pub fn init(lib: font.Library, source: [:0]const u8, opts: font.face.Options) !Face { + pub fn init( + lib: font.Library, + source: [:0]const u8, + opts: font.face.Options, + ) !Face { _ = lib; const data = try macos.foundation.Data.createWithBytesNoCopy(source); @@ -914,7 +918,7 @@ test "in-memory" { var atlas = try font.Atlas.init(alloc, 512, .grayscale); defer atlas.deinit(alloc); - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); @@ -941,7 +945,7 @@ test "variable" { var atlas = try font.Atlas.init(alloc, 512, .grayscale); defer atlas.deinit(alloc); - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); @@ -968,7 +972,7 @@ test "variable set variation" { var atlas = try font.Atlas.init(alloc, 512, .grayscale); defer atlas.deinit(alloc); - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); @@ -996,7 +1000,7 @@ test "svg font table" { const alloc = testing.allocator; const testFont = font.embedded.julia_mono; - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); @@ -1010,9 +1014,10 @@ test "svg font table" { test "glyphIndex colored vs text" { const testing = std.testing; + const alloc = testing.allocator; const testFont = font.embedded.julia_mono; - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index c2eab4599..bf86b88de 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -29,12 +29,20 @@ pub const Face = struct { assert(font.face.FreetypeLoadFlags != void); } - /// Our freetype library - lib: freetype.Library, + /// Our Library + lib: Library, /// Our font face. face: freetype.Face, + /// This mutex MUST be held while doing anything with the + /// glyph slot on the freetype face, because this struct + /// may be shared across multiple surfaces. + /// + /// This means that anywhere where `self.face.loadGlyph` + /// is called, this mutex must be held. + ft_mutex: *std.Thread.Mutex, + /// Harfbuzz font corresponding to this face. hb_font: harfbuzz.Font, @@ -59,30 +67,52 @@ pub const Face = struct { }; /// Initialize a new font face with the given source in-memory. - pub fn initFile(lib: Library, path: [:0]const u8, index: i32, opts: font.face.Options) !Face { + pub fn initFile( + lib: Library, + path: [:0]const u8, + index: i32, + opts: font.face.Options, + ) !Face { + lib.mutex.lock(); + defer lib.mutex.unlock(); const face = try lib.lib.initFace(path, index); errdefer face.deinit(); return try initFace(lib, face, opts); } /// Initialize a new font face with the given source in-memory. - pub fn init(lib: Library, source: [:0]const u8, opts: font.face.Options) !Face { + pub fn init( + lib: Library, + source: [:0]const u8, + opts: font.face.Options, + ) !Face { + lib.mutex.lock(); + defer lib.mutex.unlock(); const face = try lib.lib.initMemoryFace(source, 0); errdefer face.deinit(); return try initFace(lib, face, opts); } - fn initFace(lib: Library, face: freetype.Face, opts: font.face.Options) !Face { + fn initFace( + lib: Library, + face: freetype.Face, + opts: font.face.Options, + ) !Face { try face.selectCharmap(.unicode); try setSize_(face, opts.size); var hb_font = try harfbuzz.freetype.createFont(face.handle); errdefer hb_font.destroy(); + const ft_mutex = try lib.alloc.create(std.Thread.Mutex); + errdefer lib.alloc.destroy(ft_mutex); + ft_mutex.* = .{}; + var result: Face = .{ - .lib = lib.lib, + .lib = lib, .face = face, .hb_font = hb_font, + .ft_mutex = ft_mutex, .load_flags = opts.freetype_load_flags, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -114,7 +144,13 @@ pub const Face = struct { } pub fn deinit(self: *Face) void { - self.face.deinit(); + self.lib.alloc.destroy(self.ft_mutex); + { + self.lib.mutex.lock(); + defer self.lib.mutex.unlock(); + + self.face.deinit(); + } self.hb_font.destroy(); self.* = undefined; } @@ -147,11 +183,7 @@ pub const Face = struct { self.face.ref(); errdefer self.face.deinit(); - var f = try initFace( - .{ .lib = self.lib }, - self.face, - opts, - ); + var f = try initFace(self.lib, self.face, opts); errdefer f.deinit(); f.synthetic = self.synthetic; f.synthetic.bold = true; @@ -166,11 +198,7 @@ pub const Face = struct { self.face.ref(); errdefer self.face.deinit(); - var f = try initFace( - .{ .lib = self.lib }, - self.face, - opts, - ); + var f = try initFace(self.lib, self.face, opts); errdefer f.deinit(); f.synthetic = self.synthetic; f.synthetic.italic = true; @@ -228,7 +256,7 @@ pub const Face = struct { // first thing we have to do is get all the vars and put them into // an array. const mm = try self.face.getMMVar(); - defer self.lib.doneMMVar(mm); + defer self.lib.lib.doneMMVar(mm); // To avoid allocations, we cap the number of variation axes we can // support. This is arbitrary but Firefox caps this at 16 so I @@ -270,6 +298,9 @@ pub const Face = struct { /// Returns true if the given glyph ID is colorized. pub fn isColorGlyph(self: *const Face, glyph_id: u32) bool { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + // Load the glyph and see what pixel mode it renders with. // All modes other than BGRA are non-color. // If the glyph fails to load, just return false. @@ -296,6 +327,9 @@ pub const Face = struct { glyph_index: u32, opts: font.face.RenderOptions, ) !Glyph { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + const metrics = opts.grid_metrics; // If we have synthetic italic, then we apply a transformation matrix. @@ -741,6 +775,9 @@ pub const Face = struct { // If we fail to load any visible ASCII we just use max_advance from // the metrics provided by FreeType. const cell_width: f64 = cell_width: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + var max: f64 = 0.0; var c: u8 = ' '; while (c < 127) : (c += 1) { @@ -780,6 +817,8 @@ pub const Face = struct { break :heights .{ cap: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ .render = true, @@ -791,6 +830,8 @@ pub const Face = struct { break :cap null; }, ex: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ .render = true, @@ -832,7 +873,7 @@ test { const testFont = font.embedded.inconsolata; const alloc = testing.allocator; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var atlas = try font.Atlas.init(alloc, 512, .grayscale); @@ -881,7 +922,7 @@ test "color emoji" { const alloc = testing.allocator; const testFont = font.embedded.emoji; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var atlas = try font.Atlas.init(alloc, 512, .rgba); @@ -936,7 +977,7 @@ test "mono to rgba" { const alloc = testing.allocator; const testFont = font.embedded.emoji; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var atlas = try font.Atlas.init(alloc, 512, .rgba); @@ -958,7 +999,7 @@ test "svg font table" { const alloc = testing.allocator; const testFont = font.embedded.julia_mono; - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12, .xdpi = 72, .ydpi = 72 } }); @@ -995,7 +1036,7 @@ test "bitmap glyph" { const alloc = testing.allocator; const testFont = font.embedded.terminus_ttf; - var lib = try Library.init(); + var lib = try Library.init(alloc); defer lib.deinit(); var atlas = try font.Atlas.init(alloc, 512, .grayscale); diff --git a/src/font/library.zig b/src/font/library.zig index b00bbfce0..43aa101b7 100644 --- a/src/font/library.zig +++ b/src/font/library.zig @@ -1,5 +1,7 @@ //! A library represents the shared state that the underlying font //! library implementation(s) require per-process. +const std = @import("std"); +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const options = @import("main.zig").options; const freetype = @import("freetype"); @@ -24,13 +26,26 @@ pub const Library = switch (options.backend) { pub const FreetypeLibrary = struct { lib: freetype.Library, - pub const InitError = freetype.Error; + alloc: Allocator, - pub fn init() InitError!Library { - return Library{ .lib = try freetype.Library.init() }; + /// Mutex to be held any time the library is + /// being used to create or destroy a face. + mutex: *std.Thread.Mutex, + + pub const InitError = freetype.Error || Allocator.Error; + + pub fn init(alloc: Allocator) InitError!Library { + const lib = try freetype.Library.init(); + errdefer lib.deinit(); + + const mutex = try alloc.create(std.Thread.Mutex); + mutex.* = .{}; + + return Library{ .lib = lib, .alloc = alloc, .mutex = mutex }; } pub fn deinit(self: *Library) void { + self.alloc.destroy(self.mutex); self.lib.deinit(); } }; @@ -38,7 +53,8 @@ pub const FreetypeLibrary = struct { pub const NoopLibrary = struct { pub const InitError = error{}; - pub fn init() InitError!Library { + pub fn init(alloc: Allocator) InitError!Library { + _ = alloc; return Library{}; } diff --git a/src/font/opentype/svg.zig b/src/font/opentype/svg.zig index 01d172d17..ff8eeed49 100644 --- a/src/font/opentype/svg.zig +++ b/src/font/opentype/svg.zig @@ -99,7 +99,7 @@ test "SVG" { const alloc = testing.allocator; const testFont = font.embedded.julia_mono; - var lib = try font.Library.init(); + var lib = try font.Library.init(alloc); defer lib.deinit(); var face = try font.Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index ec64fe6eb..f2ac5b85d 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1761,7 +1761,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { .nerd_font => font.embedded.nerd_font, }; - var lib = try Library.init(); + var lib = try Library.init(alloc); errdefer lib.deinit(); var c = Collection.init(); diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index b284dc140..eb8130f79 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -1220,7 +1220,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { .arabic => font.embedded.arabic, }; - var lib = try Library.init(); + var lib = try Library.init(alloc); errdefer lib.deinit(); var c = Collection.init(); From 5319d3836619ebbf2d7beefc17197f5c42fc1553 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 1 May 2025 18:37:24 -0600 Subject: [PATCH 174/642] fix(tests): correctly deinit font faces --- src/font/Collection.zig | 21 ++++++++++++--------- src/font/DeferredFace.zig | 6 ++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 66e13c109..59f89d402 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -714,15 +714,18 @@ test "add full" { ) }); } - try testing.expectError(error.CollectionFull, c.add( - alloc, - .regular, - .{ .loaded = try Face.init( - lib, - testFont, - .{ .size = .{ .points = 12 } }, - ) }, - )); + var face = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12 } }, + ); + // We have to deinit it manually since the + // collection doesn't do it if adding fails. + defer face.deinit(); + try testing.expectError( + error.CollectionFull, + c.add(alloc, .regular, .{ .loaded = face }), + ); } test "add deferred without loading options" { diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index d61f5492f..8794ccea9 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -425,7 +425,8 @@ test "fontconfig" { try testing.expect(n.len > 0); // Load it and verify it works - const face = try def.load(lib, .{ .size = .{ .points = 12 } }); + var face = try def.load(lib, .{ .size = .{ .points = 12 } }); + defer face.deinit(); try testing.expect(face.glyphIndex(' ') != null); } @@ -456,6 +457,7 @@ test "coretext" { try testing.expect(n.len > 0); // Load it and verify it works - const face = try def.load(lib, .{ .size = .{ .points = 12 } }); + var face = try def.load(lib, .{ .size = .{ .points = 12 } }); + defer face.deinit(); try testing.expect(face.glyphIndex(' ') != null); } From b2138eeaf004eb9b62e910df3cc95acdd086d051 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Fri, 2 May 2025 10:59:52 -0400 Subject: [PATCH 175/642] codeowners: correct shell_integration.zig filename --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 0665aa407..3d8a4da3d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -144,7 +144,7 @@ # Shell /src/shell-integration/ @ghostty-org/shell -/src/termio/shell-integration.zig @ghostty-org/shell +/src/termio/shell_integration.zig @ghostty-org/shell # Terminal /src/simd/ @ghostty-org/terminal From c0f41aba45ce85f5798ff14a95586b16b0cd5da2 Mon Sep 17 00:00:00 2001 From: Leorize Date: Fri, 2 May 2025 14:06:01 -0500 Subject: [PATCH 176/642] flatpak: update GNOME runtime to 48 Notable dependencies updates: - GTK 4.16.13 -> 4.18.4 - Libadwaita 1.6.6 -> 1.7.2 --- flatpak/com.mitchellh.ghostty.Devel.yml | 2 +- flatpak/com.mitchellh.ghostty.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/flatpak/com.mitchellh.ghostty.Devel.yml b/flatpak/com.mitchellh.ghostty.Devel.yml index a939fda9a..244c3987f 100644 --- a/flatpak/com.mitchellh.ghostty.Devel.yml +++ b/flatpak/com.mitchellh.ghostty.Devel.yml @@ -1,6 +1,6 @@ app-id: com.mitchellh.ghostty.Devel runtime: org.gnome.Platform -runtime-version: "47" +runtime-version: "48" sdk: org.gnome.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.ziglang diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml index 6f387481c..17c92633f 100644 --- a/flatpak/com.mitchellh.ghostty.yml +++ b/flatpak/com.mitchellh.ghostty.yml @@ -1,6 +1,6 @@ app-id: com.mitchellh.ghostty runtime: org.gnome.Platform -runtime-version: "47" +runtime-version: "48" sdk: org.gnome.Sdk sdk-extensions: - org.freedesktop.Sdk.Extension.ziglang From 10dcf1dfe9f311ede45edc76256ea5acdb980dc8 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 14:35:32 -0500 Subject: [PATCH 177/642] fix most of the feedback from 7012 --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 35ed2dc33..e958acd55 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -16,22 +16,26 @@ body: - The feature or configuration option you encounter the issue with. - The expected behavior. - The actual behavior (and how it deviates from the expected behavior, if it is not immediately obvious). - - Relevant Ghostty logs or other stacktraces. - - Relevant screenshots, screen recordings, or other supporting media (as needed). + - Screenshots, screen recordings, or other supporting media (as needed). - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description. >[!TIP] > **Not sure what information to include?** > Here are some recommendations: - > - **Input issues:** include your keyboard layout, a screenshot of the terminal inspector's logged keystrokes (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. + > - **Input issues:** include your keyboard layout, a screenshot of the terminal inspector's logged keystrokes from the Terminal Inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. > - **Font issues:** include the problematic character(s), the output of `ghostty +show-face` for these character(s), and if they work in other applications. > - **VT issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction. > - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version. - + > - **Crashes:** (macOS) include the [Sentry UUID](https://github.com/ghostty-org/ghostty?tab=readme-ov-file#crash-reports); (Linux) try to reproduce using a debug build and provide the stack trace. placeholder: | Example: When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.` validations: required: true + - type: textarea + attributes: + label: Ghostty Logs + description: | + Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be found via the command provided in [#7194](https://github.com/ghostty-org/ghostty/discussions/7194) - type: textarea attributes: label: Reproduction Steps @@ -93,9 +97,9 @@ body: required: false - type: textarea attributes: - label: Ghostty Configuration + label: Ghostty Configuration (Minimal) description: | - Please provide the minimum configuration needed to reproduce this issue. If you cannot determine this, paste the output of `ghostty +show-config` here. + Please provide the **minimum** configuration needed to reproduce this issue. If you cannot determine this, paste the contents of your Ghostty configuration file here. placeholder: | font-family = CommitMono Nerd Font font-family-bold = CommitMono Nerd Font @@ -112,9 +116,9 @@ body: attributes: label: Additional Relevant Configuration description: | - If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. + If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration and versions needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | - `tmux.conf` + `tmux.conf` (tmux 3.5a) --- set -g default-terminal "tmux-256color" set-option -sa terminal-overrides ",xterm*:Tc" From 60e1a73c041e934818c205234ec67525517d6ebd Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 14:45:53 -0500 Subject: [PATCH 178/642] update log textarea render to display inputs in codeblock --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index e958acd55..93aa87b48 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -36,6 +36,7 @@ body: label: Ghostty Logs description: | Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be found via the command provided in [#7194](https://github.com/ghostty-org/ghostty/discussions/7194) + render: text - type: textarea attributes: label: Reproduction Steps From e174599533d79f0b744d570a943c536efe689027 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 3 May 2025 07:12:28 -0700 Subject: [PATCH 179/642] ci: workaround broken lxd start with snap builder https://discourse.ubuntu.com/t/lxd-doesn-t-start-snap-lxd-device-directory-nonexistent/59785 https://github.com/canonical/lxd-pkg-snap/pull/789 This is required until Namespace or further upstream fixes are made. --- .github/workflows/test.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index be28d50fb..e6e3a77a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -374,6 +374,11 @@ jobs: /zig - run: sudo apt install -y udev - run: sudo systemctl start systemd-udevd + # Workaround until this is fixed: https://github.com/canonical/lxd-pkg-snap/pull/789 + - run: | + _LXD_SNAP_DEVCGROUP_CONFIG="/var/lib/snapd/cgroup/snap.lxd.device" + sudo mkdir -p /var/lib/snapd/cgroup + echo 'self-managed=true' | sudo tee "${_LXD_SNAP_DEVCGROUP_CONFIG}" - uses: snapcore/action-build@v1 with: path: dist From abf5f1859868e70fda4116b94638c2fa1979e803 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 4 May 2025 00:14:51 +0000 Subject: [PATCH 180/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 180746436..7c5ff8ffc 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz", - .hash = "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz", + .hash = "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 640167615..513ee0dcd 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym": { + "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz", - "hash": "sha256-Vy5muiJ3hJXcOvmFHLhqc+Dvdh74GG6+u/L+EsavDb0=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz", + "hash": "sha256-xpDitXpZrdU/EcgLyG4G0cEiT4r42viy+DJALmy2sQE=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index becb2ae87..46cf07cc9 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym"; + name = "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz"; - hash = "sha256-Vy5muiJ3hJXcOvmFHLhqc+Dvdh74GG6+u/L+EsavDb0="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz"; + hash = "sha256-xpDitXpZrdU/EcgLyG4G0cEiT4r42viy+DJALmy2sQE="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index a8cffe2b2..5f06418a7 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz From 4d27fc18ebba617188424852655be7250418891a Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Sat, 3 May 2025 20:50:04 -0500 Subject: [PATCH 181/642] use log stream command instead of link to discussion Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 93aa87b48..7a6b17ff3 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -35,7 +35,7 @@ body: attributes: label: Ghostty Logs description: | - Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be found via the command provided in [#7194](https://github.com/ghostty-org/ghostty/discussions/7194) + Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be viewed with `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. render: text - type: textarea attributes: From 71f52fd19820730d09b820f582fffd2453b2c88a Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 20:53:00 -0500 Subject: [PATCH 182/642] re-order ghostty logs field --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 7a6b17ff3..b762674e4 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -31,12 +31,6 @@ body: Example: When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.` validations: required: true - - type: textarea - attributes: - label: Ghostty Logs - description: | - Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be viewed with `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. - render: text - type: textarea attributes: label: Reproduction Steps @@ -49,6 +43,12 @@ body: 4. Observe `xterm-ghostty` error message above. validations: required: true + - type: textarea + attributes: + label: Ghostty Logs + description: | + Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be viewed with `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. + render: text - type: textarea attributes: label: Ghostty Version From 41e3c8830f18cde64403de5a9b254129310c1b99 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 20:55:47 -0500 Subject: [PATCH 183/642] change VT to terminal emulation Co-authored-by: Leah Amelia Chen --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index b762674e4..81b2c6264 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -24,7 +24,7 @@ body: > Here are some recommendations: > - **Input issues:** include your keyboard layout, a screenshot of the terminal inspector's logged keystrokes from the Terminal Inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. > - **Font issues:** include the problematic character(s), the output of `ghostty +show-face` for these character(s), and if they work in other applications. - > - **VT issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction. + > - **Terminal emulation issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction. > - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version. > - **Crashes:** (macOS) include the [Sentry UUID](https://github.com/ghostty-org/ghostty?tab=readme-ov-file#crash-reports); (Linux) try to reproduce using a debug build and provide the stack trace. placeholder: | From 51b69253222e82106d04bceb1d964aa1428e0bd9 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 21:00:53 -0500 Subject: [PATCH 184/642] add Linux log hint --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 81b2c6264..fa48c7dce 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -47,7 +47,7 @@ body: attributes: label: Ghostty Logs description: | - Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. For macOS users, logs can be viewed with `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`. + Provide any captured Ghostty logs or stacktraces during your issue reproduction in this field. On Linux, logs can be found by running `ghostty` from the command-line; on macOS, logs can be viewed with `sudo log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'` from another terminal emulator. render: text - type: textarea attributes: From 9c2f8d8ad38e43155573149d8e9786cfa2a8230d Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Sat, 3 May 2025 21:02:27 -0500 Subject: [PATCH 185/642] cleanup the input issue hint Co-authored-by: trag1c --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index fa48c7dce..fc032aa1a 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -22,7 +22,7 @@ body: >[!TIP] > **Not sure what information to include?** > Here are some recommendations: - > - **Input issues:** include your keyboard layout, a screenshot of the terminal inspector's logged keystrokes from the Terminal Inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. + > - **Input issues:** include your keyboard layout, a screenshot of logged keystrokes from the Terminal Inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. > - **Font issues:** include the problematic character(s), the output of `ghostty +show-face` for these character(s), and if they work in other applications. > - **Terminal emulation issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction. > - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version. From 6ffb6207e7f784a201da889debecd5c91641c650 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 21:22:49 -0500 Subject: [PATCH 186/642] split out expected & actual behavior fields --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 22 ++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index fc032aa1a..ffe5a7243 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -14,8 +14,6 @@ body: description: | Provide a detailed description of the issue. Include relevant information, such as: - The feature or configuration option you encounter the issue with. - - The expected behavior. - - The actual behavior (and how it deviates from the expected behavior, if it is not immediately obvious). - Screenshots, screen recordings, or other supporting media (as needed). - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description. @@ -31,6 +29,26 @@ body: Example: When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.` validations: required: true + - type: textarea + attributes: + label: Expected Behavior + description: | + Provide a summary of how you expect Ghostty to behave in this situation. Include any relevant documentation links. + placeholder: | + Example: When I run `clear`, I expect the screen to be cleared and the prompt to be redrawn at the top of the window. + render: text + validations: + required: true + - type: textarea + attributes: + label: Actual Behavior + description: | + Provide a summary of how Ghostty actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically. + placeholder: | + Example: When I run `clear`, the screen is not cleared, and an error is printed: `Error opening terminal: xterm-ghostty.` + render: text + validations: + required: true - type: textarea attributes: label: Reproduction Steps From 0bf168c834508b824914054793c1d08eb73e98a9 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 21:27:48 -0500 Subject: [PATCH 187/642] terminal inspector no longer proper noun --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index ffe5a7243..f1442b9a4 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -20,7 +20,7 @@ body: >[!TIP] > **Not sure what information to include?** > Here are some recommendations: - > - **Input issues:** include your keyboard layout, a screenshot of logged keystrokes from the Terminal Inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. + > - **Input issues:** include your keyboard layout, a screenshot of logged keystrokes from the terminal inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. > - **Font issues:** include the problematic character(s), the output of `ghostty +show-face` for these character(s), and if they work in other applications. > - **Terminal emulation issues (including image rendering issues):** attach an [asciinema](https://docs.asciinema.org/getting-started/) cast file, shell script, or text file for reproduction. > - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version. From 1f9a4e6794a93c740722f71ecbb14ce227cdaebd Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Sat, 3 May 2025 21:29:59 -0500 Subject: [PATCH 188/642] more direct naming of minimal configuration Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index f1442b9a4..8537c19d5 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -116,7 +116,7 @@ body: required: false - type: textarea attributes: - label: Ghostty Configuration (Minimal) + label: Minimal Ghostty Configuration description: | Please provide the **minimum** configuration needed to reproduce this issue. If you cannot determine this, paste the contents of your Ghostty configuration file here. placeholder: | From cc95475ae996af3ceb5864106b1cce39bf5000b9 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sat, 3 May 2025 21:48:59 -0500 Subject: [PATCH 189/642] update placeholder text remove the 'example:' prefixes and make them sound less like it's AI-generated --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 8537c19d5..f6bc0b058 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -26,7 +26,7 @@ body: > - **Renderer issues:** (Linux) include your OpenGL version, graphics card, driver version. > - **Crashes:** (macOS) include the [Sentry UUID](https://github.com/ghostty-org/ghostty?tab=readme-ov-file#crash-reports); (Linux) try to reproduce using a debug build and provide the stack trace. placeholder: | - Example: When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.` + When using SSH to connect to my remote Linux machine from my local macOS device in Ghostty, I try to run `clear`, and the screen does not clear. Instead, I see the following error message printed to the terminal: `Error opening terminal: xterm-ghostty.` validations: required: true - type: textarea @@ -35,7 +35,7 @@ body: description: | Provide a summary of how you expect Ghostty to behave in this situation. Include any relevant documentation links. placeholder: | - Example: When I run `clear`, I expect the screen to be cleared and the prompt to be redrawn at the top of the window. + The screen to be cleared and the prompt is redrawn at the top of the window. render: text validations: required: true @@ -45,7 +45,7 @@ body: description: | Provide a summary of how Ghostty actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically. placeholder: | - Example: When I run `clear`, the screen is not cleared, and an error is printed: `Error opening terminal: xterm-ghostty.` + The screen is not cleared, and an error is printed: `Error opening terminal: xterm-ghostty`. render: text validations: required: true From 050375cbb63df93d5dbefa2565675a7a8d0590e7 Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Sat, 3 May 2025 21:56:08 -0500 Subject: [PATCH 190/642] make minimum configuration more explicit Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index f6bc0b058..8cdb34737 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -118,7 +118,7 @@ body: attributes: label: Minimal Ghostty Configuration description: | - Please provide the **minimum** configuration needed to reproduce this issue. If you cannot determine this, paste the contents of your Ghostty configuration file here. + Please provide the **minimum** configuration needed to reproduce this issue. If you can still reproduce the issue with one of the lines removed, do not include that line. If and **only** if you are not able to determine this, paste the contents of your Ghostty configuration file here. placeholder: | font-family = CommitMono Nerd Font font-family-bold = CommitMono Nerd Font From 233ef4f7822d4110f179c31d36f2d0b8e834972b Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Sun, 4 May 2025 11:58:50 -0500 Subject: [PATCH 191/642] more updates to expected behavior placeholder Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 8cdb34737..aabe19c27 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -35,7 +35,7 @@ body: description: | Provide a summary of how you expect Ghostty to behave in this situation. Include any relevant documentation links. placeholder: | - The screen to be cleared and the prompt is redrawn at the top of the window. + The screen is cleared and the prompt is redrawn at the top of the window. render: text validations: required: true From 46d3de26fca9377f01c6a979da57580d0e85371f Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sun, 4 May 2025 11:59:57 -0500 Subject: [PATCH 192/642] remove renderers prone to jailbreak --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index aabe19c27..7f402183c 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -36,7 +36,6 @@ body: Provide a summary of how you expect Ghostty to behave in this situation. Include any relevant documentation links. placeholder: | The screen is cleared and the prompt is redrawn at the top of the window. - render: text validations: required: true - type: textarea @@ -46,7 +45,6 @@ body: Provide a summary of how Ghostty actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically. placeholder: | The screen is not cleared, and an error is printed: `Error opening terminal: xterm-ghostty`. - render: text validations: required: true - type: textarea @@ -143,7 +141,6 @@ body: set-option -sa terminal-overrides ",xterm*:Tc" set -g base-index 1 setw -g pane-base-index 1 - render: text validations: required: false - type: markdown From 431116c9d8702f2e1149ad4b5b22e6130091cc1f Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sun, 4 May 2025 12:01:22 -0500 Subject: [PATCH 193/642] do not ask users for a summary --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 7f402183c..8b304b4b6 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -33,7 +33,7 @@ body: attributes: label: Expected Behavior description: | - Provide a summary of how you expect Ghostty to behave in this situation. Include any relevant documentation links. + Describe how you expect Ghostty to behave in this situation. Include any relevant documentation links. placeholder: | The screen is cleared and the prompt is redrawn at the top of the window. validations: @@ -42,7 +42,7 @@ body: attributes: label: Actual Behavior description: | - Provide a summary of how Ghostty actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically. + Describe how Ghostty actually behaves in this situation. If it is not immediately obvious how the actual behavior differs from the expected behavior described above, please be sure to mention the deviation specifically. placeholder: | The screen is not cleared, and an error is printed: `Error opening terminal: xterm-ghostty`. validations: From 8dfa6beb15c1c87dba9ac450a01ee25b02a3e0f4 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sun, 4 May 2025 12:04:09 -0500 Subject: [PATCH 194/642] add acknowledgement for previewing format before submitting Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 8b304b4b6..26992962c 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -157,3 +157,5 @@ body: required: true - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion. required: true + - label: I have checked the “Preview” tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (```) on separate lines. + required: true From 11db0ed8ae759936923475541976aac510056fc6 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Sun, 4 May 2025 12:17:29 -0500 Subject: [PATCH 195/642] re-apply formatting & overwrite, not append --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 26992962c..5e49000c9 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -157,5 +157,5 @@ body: required: true - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion. required: true - - label: I have checked the “Preview” tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (```) on separate lines. + - label: I have checked the “Preview” tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (```) on separate lines. required: true From 37974dba06549ab714cbf36694e3439d9dbc9cf9 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Fri, 2 May 2025 07:42:51 -0400 Subject: [PATCH 196/642] bash: explicitly request a login shell Prior to #7044, on macOS, our shell integrated command line would be executed under `exec -l`, which caused bash to be started as a login shell. Now that we're using direct command execution, add `--login` to our bash command's arguments on macOS to get that same behavior. --- src/termio/shell_integration.zig | 64 +++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 2cf809694..fb62327d3 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -239,7 +239,7 @@ fn setupBash( resource_dir: []const u8, env: *EnvMap, ) !?config.Command { - var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 2); + var args = try std.ArrayList([:0]const u8).initCapacity(alloc, 3); defer args.deinit(); // Iterator that yields each argument in the original command line. @@ -247,12 +247,17 @@ fn setupBash( var iter = try command.argIterator(alloc); defer iter.deinit(); - // Start accumulating arguments with the executable and `--posix` mode flag. + // Start accumulating arguments with the executable and initial flags. if (iter.next()) |exe| { try args.append(try alloc.dupeZ(u8, exe)); } else return null; try args.append("--posix"); + // On macOS, we request a login shell to match that platform's norms. + if (comptime builtin.target.os.tag.isDarwin()) { + try args.append("--login"); + } + // Stores the list of intercepted command line flags that will be passed // to our shell integration script: --norc --noprofile // We always include at least "1" so the script can differentiate between @@ -342,9 +347,12 @@ test "bash" { const command = try setupBash(alloc, .{ .shell = "bash" }, ".", &env); - try testing.expectEqual(2, command.?.direct.len); + try testing.expect(command.?.direct.len >= 2); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } try testing.expectEqualStrings("./shell-integration/bash/ghostty.bash", env.get("ENV").?); try testing.expectEqualStrings("1", env.get("GHOSTTY_BASH_INJECT").?); } @@ -387,9 +395,12 @@ test "bash: inject flags" { const command = try setupBash(alloc, .{ .shell = "bash --norc" }, ".", &env); - try testing.expectEqual(2, command.?.direct.len); + try testing.expect(command.?.direct.len >= 2); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } try testing.expectEqualStrings("1 --norc", env.get("GHOSTTY_BASH_INJECT").?); } @@ -400,9 +411,12 @@ test "bash: inject flags" { const command = try setupBash(alloc, .{ .shell = "bash --noprofile" }, ".", &env); - try testing.expectEqual(2, command.?.direct.len); + try testing.expect(command.?.direct.len >= 2); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } try testing.expectEqualStrings("1 --noprofile", env.get("GHOSTTY_BASH_INJECT").?); } } @@ -419,18 +433,24 @@ test "bash: rcfile" { // bash --rcfile { const command = try setupBash(alloc, .{ .shell = "bash --rcfile profile.sh" }, ".", &env); - try testing.expectEqual(2, command.?.direct.len); + try testing.expect(command.?.direct.len >= 2); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } // bash --init-file { const command = try setupBash(alloc, .{ .shell = "bash --init-file profile.sh" }, ".", &env); - try testing.expectEqual(2, command.?.direct.len); + try testing.expect(command.?.direct.len >= 2); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } try testing.expectEqualStrings("profile.sh", env.get("GHOSTTY_BASH_RCFILE").?); } } @@ -476,25 +496,35 @@ test "bash: additional arguments" { // "-" argument separator { const command = try setupBash(alloc, .{ .shell = "bash - --arg file1 file2" }, ".", &env); - try testing.expectEqual(6, command.?.direct.len); + try testing.expect(command.?.direct.len >= 6); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); - try testing.expectEqualStrings("-", command.?.direct[2]); - try testing.expectEqualStrings("--arg", command.?.direct[3]); - try testing.expectEqualStrings("file1", command.?.direct[4]); - try testing.expectEqualStrings("file2", command.?.direct[5]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } + + const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2; + try testing.expectEqualStrings("-", command.?.direct[offset + 0]); + try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]); + try testing.expectEqualStrings("file1", command.?.direct[offset + 2]); + try testing.expectEqualStrings("file2", command.?.direct[offset + 3]); } // "--" argument separator { const command = try setupBash(alloc, .{ .shell = "bash -- --arg file1 file2" }, ".", &env); - try testing.expectEqual(6, command.?.direct.len); + try testing.expect(command.?.direct.len >= 6); try testing.expectEqualStrings("bash", command.?.direct[0]); try testing.expectEqualStrings("--posix", command.?.direct[1]); - try testing.expectEqualStrings("--", command.?.direct[2]); - try testing.expectEqualStrings("--arg", command.?.direct[3]); - try testing.expectEqualStrings("file1", command.?.direct[4]); - try testing.expectEqualStrings("file2", command.?.direct[5]); + if (comptime builtin.target.os.tag.isDarwin()) { + try testing.expectEqualStrings("--login", command.?.direct[2]); + } + + const offset = if (comptime builtin.target.os.tag.isDarwin()) 3 else 2; + try testing.expectEqualStrings("--", command.?.direct[offset + 0]); + try testing.expectEqualStrings("--arg", command.?.direct[offset + 1]); + try testing.expectEqualStrings("file1", command.?.direct[offset + 2]); + try testing.expectEqualStrings("file2", command.?.direct[offset + 3]); } } From f84367880b02346c56f79c16cc1b4e93b2e5437f Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 5 May 2025 19:23:40 -0500 Subject: [PATCH 197/642] Properly enclose code block backticks Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 5e49000c9..1448b7385 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -157,5 +157,5 @@ body: required: true - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion. required: true - - label: I have checked the “Preview” tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (```) on separate lines. + - label: I have checked the “Preview” tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines. required: true From ad16f984cf1f70e189a39f507efe51da7fefca36 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 5 May 2025 19:26:46 -0500 Subject: [PATCH 198/642] remove smart quotes in favor of ascii Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 1448b7385..0ed334592 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -157,5 +157,5 @@ body: required: true - label: I have searched the Ghostty repository (both open and closed Discussions and Issues) and confirm this is not a duplicate of an existing issue or discussion. required: true - - label: I have checked the “Preview” tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines. + - label: I have checked the "Preview" tab on all text fields to ensure that everything looks right, and have wrapped all configuration and code in code blocks with a group of three backticks (` ``` `) on separate lines. required: true From f9c1b6b7cf477a49b771c69bb6af890c148754af Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 5 May 2025 19:28:31 -0500 Subject: [PATCH 199/642] fixup markdown in additional config field Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 0ed334592..c71a9c496 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -135,7 +135,7 @@ body: description: | If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration and versions needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | - `tmux.conf` (tmux 3.5a) + #### `tmux.conf` (tmux 3.5a) --- set -g default-terminal "tmux-256color" set-option -sa terminal-overrides ",xterm*:Tc" From d0b9242f496db5b60979f0f57b2b83d4171484a4 Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 5 May 2025 21:34:33 -0500 Subject: [PATCH 200/642] replace dashes with code block backticks for additional config Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index c71a9c496..6f042ba86 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -136,11 +136,12 @@ body: If your issue involves other programs, tools, or applications in addition to Ghostty (e.g. Neovim, tmux, Zellij, etc.), please provide the minimum configuration and versions needed for all relevant programs to reproduce the issue here. If you use custom CSS or shaders for Ghostty, also include them here, if applicable to your issue. placeholder: | #### `tmux.conf` (tmux 3.5a) - --- + ``` set -g default-terminal "tmux-256color" set-option -sa terminal-overrides ",xterm*:Tc" set -g base-index 1 setw -g pane-base-index 1 + ``` validations: required: false - type: markdown From ad0d426bba0a0003e01fd367dc1da42616e413a9 Mon Sep 17 00:00:00 2001 From: taylrfnt <43214679+taylrfnt@users.noreply.github.com> Date: Mon, 5 May 2025 21:36:38 -0500 Subject: [PATCH 201/642] consistent spacing with tip & important highlights Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 6f042ba86..605a34e6c 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -17,7 +17,7 @@ body: - Screenshots, screen recordings, or other supporting media (as needed). - If this is a regression of an existing issue that was closed or resolved, please include the previous item reference (Discussion, Issue, PR, commit) in your description. - >[!TIP] + > [!TIP] > **Not sure what information to include?** > Here are some recommendations: > - **Input issues:** include your keyboard layout, a screenshot of logged keystrokes from the terminal inspector's "Keyboard" tab (Linux: ctrl+shift+i; MacOS: cmd+alt+i), input method, Linux input method engine (IBus, Fcitx 5, or none) and its version. From ac11ebbb4ad61c59dc69e462ef19fbc9752d2efd Mon Sep 17 00:00:00 2001 From: taylrfnt Date: Mon, 5 May 2025 21:37:49 -0500 Subject: [PATCH 202/642] update applied label to proper name Co-authored-by: Kat <65649991+00-kat@users.noreply.github.com> --- .github/DISCUSSION_TEMPLATE/issue-triage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/DISCUSSION_TEMPLATE/issue-triage.yml b/.github/DISCUSSION_TEMPLATE/issue-triage.yml index 605a34e6c..270bb86f5 100644 --- a/.github/DISCUSSION_TEMPLATE/issue-triage.yml +++ b/.github/DISCUSSION_TEMPLATE/issue-triage.yml @@ -1,4 +1,4 @@ -labels: ["needs confirmation"] +labels: ["needs-confirmation"] body: - type: markdown attributes: From 1bf686d324d7b5c507b3898f188a2fe2f77b589e Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Tue, 6 May 2025 08:44:52 -0500 Subject: [PATCH 203/642] gtk: fix comment about adwaita version --- src/input/Binding.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 10e16f1fe..434ed9f0d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -345,7 +345,7 @@ pub const Action = union(enum) { move_tab: isize, /// Toggle the tab overview. - /// This only works with libadwaita enabled currently. + /// This only works with libadwaita version 1.4.0 or newer. toggle_tab_overview, /// Change the title of the current focused surface via a prompt. From 3c405a591ae895280e5e617bd267fdfa8d3bfdce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 May 2025 07:19:34 -0700 Subject: [PATCH 204/642] update flatpak cache --- flatpak/zig-packages.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 0c36a600a..bc3b6cd0c 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/5233095e442645995e8af1fcb7b011478ee86f32.tar.gz", - "dest": "vendor/p/N-V-__8AAA38OASk6VOHVXwuyGVAeYu0nghqa1RSIliXV5ym", - "sha256": "572e66ba22778495dc3af9851cb86a73e0ef761ef8186ebebbf2fe12c6af0dbd" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz", + "dest": "vendor/p/N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A", + "sha256": "c690e2b57a59add53f11c80bc86e06d1c1224f8af8daf8b2f832402e6cb6b101" }, { "type": "archive", From 9221d392dee482f568112de7b3ab79571a04b763 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 May 2025 07:20:42 -0700 Subject: [PATCH 205/642] ci: add flatpak JSON for iterm2 theme updates I forgot to add the path in the GitHub action. --- .github/workflows/update-colorschemes.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index e8e3fe99a..fed6d2db7 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -71,6 +71,7 @@ jobs: build.zig.zon.nix build.zig.zon.txt build.zig.zon.json + flatpak/zig-packages.json body: | Upstream revision: https://github.com/mbadolato/iTerm2-Color-Schemes/tree/${{ steps.zig_fetch.outputs.upstream_rev }} labels: dependencies From 9b789172464a67037d89b82e9b296ae4da1ff3cf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 May 2025 10:32:39 -0700 Subject: [PATCH 206/642] macOS: handle scenario cgWindowId is nil Fixes #7114 Supercedes #7271 This fixes a crash that could occur with non-native fullscreen and `fullscreen = true` set at once. The "windowNumber" can be `<= 0` if the window "doesn't have a window device." I don't fully know all the scenarios this is true but it is true when the window is not visible, at least. --- macos/Sources/Helpers/Fullscreen.swift | 25 ++++++++++++------- .../Sources/Helpers/NSWindow+Extension.swift | 8 ++++-- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index b6fb08271..5233af62d 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -355,16 +355,23 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.styleMask = window.styleMask self.dock = window.screen?.hasDock ?? false - // We hide the menu only if this window is not on any fullscreen - // spaces. We do this because fullscreen spaces already hide the - // menu and if we insert/remove this presentation option we get - // issues (see #7075) - let activeSpace = CGSSpace.active() - let spaces = CGSSpace.list(for: window.cgWindowId) - if spaces.contains(activeSpace) { - self.menu = activeSpace.type != .fullscreen + if let cgWindowId = window.cgWindowId { + // We hide the menu only if this window is not on any fullscreen + // spaces. We do this because fullscreen spaces already hide the + // menu and if we insert/remove this presentation option we get + // issues (see #7075) + let activeSpace = CGSSpace.active() + let spaces = CGSSpace.list(for: cgWindowId) + if spaces.contains(activeSpace) { + self.menu = activeSpace.type != .fullscreen + } else { + self.menu = spaces.allSatisfy { $0.type != .fullscreen } + } } else { - self.menu = spaces.allSatisfy { $0.type != .fullscreen } + // Window doesn't have a window device, its not visible or something. + // In this case, we assume we can hide the menu. We may want to do + // something more sophisticated but this works for now. + self.menu = true } } } diff --git a/macos/Sources/Helpers/NSWindow+Extension.swift b/macos/Sources/Helpers/NSWindow+Extension.swift index c7523bdb7..06a9fa4e0 100644 --- a/macos/Sources/Helpers/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/NSWindow+Extension.swift @@ -2,7 +2,11 @@ import AppKit extension NSWindow { /// Get the CGWindowID type for the window (used for low level CoreGraphics APIs). - var cgWindowId: CGWindowID { - CGWindowID(windowNumber) + var cgWindowId: CGWindowID? { + // "If the window doesn’t have a window device, the value of this + // property is equal to or less than 0." - Docs. In practice I've + // found this is true if a window is not visible. + guard windowNumber > 0 else { return nil } + return CGWindowID(windowNumber) } } From e2a0f439c6332884577eabb2491a343afde22c42 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 May 2025 12:57:53 -0700 Subject: [PATCH 207/642] macOS: save/restore firstResponder on non-native fullscreen Fixes #6999 It appears that at some point one of the operations causes focus to move away for non-native fullscreen. We previously relied on the delegate method to restore this but a better approach appears to handle this directly in the fullscreen implementations. This fixes the linked issue. I still think long term all the `Ghostty.moveFocus` stuff is a code smell and we should be auditing all that code to see if we can eliminate it. But this is a step in the right direction, and removes one of those uses. --- .../Terminal/BaseTerminalController.swift | 8 -------- .../Features/Terminal/TerminalController.swift | 4 +--- macos/Sources/Helpers/Fullscreen.swift | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b502e56e0..36dc5f93c 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -361,14 +361,6 @@ class BaseTerminalController: NSWindowController, } } - func fullscreenDidChange() { - // For some reason focus can get lost when we change fullscreen. Regardless of - // mode above we just move it back. - if let focusedSurface { - Ghostty.moveFocus(to: focusedSurface) - } - } - // MARK: Clipboard Confirmation @objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f384b97ed..cf2dd3348 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -121,9 +121,7 @@ class TerminalController: BaseTerminalController { } - override func fullscreenDidChange() { - super.fullscreenDidChange() - + func fullscreenDidChange() { // When our fullscreen state changes, we resync our appearance because some // properties change when fullscreen or not. guard let focusedSurface else { return } diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 5233af62d..6094bf844 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -171,6 +171,13 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { guard let savedState = SavedState(window) else { return } self.savedState = savedState + // Get our current first responder on this window. For non-native fullscreen + // we have to restore this because for some reason the operations below + // lose it (see: https://github.com/ghostty-org/ghostty/issues/6999). + // I don't know the root cause here so if we can figure that out there may + // be a nicer way than this. + let firstResponder = window.firstResponder + // We hide the dock if the window is on a screen with the dock. // We must hide the dock FIRST then hide the menu: // If you specify autoHideMenuBar, it must be accompanied by either hideDock or autoHideDock. @@ -207,6 +214,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // https://github.com/ghostty-org/ghostty/issues/1996 DispatchQueue.main.async { self.window.setFrame(self.fullscreenFrame(screen), display: true) + if let firstResponder { + self.window.makeFirstResponder(firstResponder) + } + self.delegate?.fullscreenDidChange() } } @@ -220,6 +231,9 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { let center = NotificationCenter.default center.removeObserver(self, name: NSWindow.didChangeScreenNotification, object: window) + // See enter where we do the same thing to understand why. + let firstResponder = window.firstResponder + // Unhide our elements if savedState.dock { unhideDock() @@ -258,6 +272,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { } } + if let firstResponder { + window.makeFirstResponder(firstResponder) + } + // Unset our saved state, we're restored! self.savedState = nil From b6f120a7492a6cce071b336570fa7ba1b746dd88 Mon Sep 17 00:00:00 2001 From: Leorize Date: Wed, 26 Mar 2025 01:30:15 -0500 Subject: [PATCH 208/642] termio, flatpak: support spawning terminals in cwd Implements path access testing for Flatpak via test spawning. This is required since Flatpak reserves certain paths from being accessible regardless of permissions. Ref: https://docs.flatpak.org/en/latest/sandbox-permissions.html#reserved-paths --- src/termio/Exec.zig | 60 +++++++++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index abe49a47b..23c626879 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -745,7 +745,7 @@ const Subprocess = struct { }); arena: std.heap.ArenaAllocator, - cwd: ?[]const u8, + cwd: ?[:0]const u8, env: ?EnvMap, args: []const [:0]const u8, grid_size: renderer.GridSize, @@ -985,8 +985,8 @@ const Subprocess = struct { // We have to copy the cwd because there is no guarantee that // pointers in full_config remain valid. - const cwd: ?[]u8 = if (cfg.working_directory) |cwd| - try alloc.dupe(u8, cwd) + const cwd: ?[:0]u8 = if (cfg.working_directory) |cwd| + try alloc.dupeZ(u8, cwd) else null; @@ -1048,6 +1048,47 @@ const Subprocess = struct { log.debug("starting command command={s}", .{self.args}); + // If we can't access the cwd, then don't set any cwd and inherit. + // This is important because our cwd can be set by the shell (OSC 7) + // and we don't want to break new windows. + const cwd: ?[:0]const u8 = if (self.cwd) |proposed| cwd: { + if ((comptime build_config.flatpak) and internal_os.isFlatpak()) { + // Flatpak sandboxing prevents access to certain reserved paths + // regardless of configured permissions. Perform a test spawn + // to get around this problem + // + // https://docs.flatpak.org/en/latest/sandbox-permissions.html#reserved-paths + log.info("flatpak detected, will use host command to verify cwd access", .{}); + const dev_null = try std.fs.cwd().openFile("/dev/null", .{ .mode = .read_write }); + defer dev_null.close(); + var cmd: internal_os.FlatpakHostCommand = .{ + .argv = &[_][]const u8{ + "/bin/sh", + "-c", + ":", + }, + .cwd = proposed, + .stdin = dev_null.handle, + .stdout = dev_null.handle, + .stderr = dev_null.handle, + }; + _ = cmd.spawn(alloc) catch |err| { + log.warn("cannot spawn command at cwd, ignoring: {}", .{err}); + break :cwd null; + }; + _ = try cmd.wait(); + + break :cwd proposed; + } + + if (std.fs.cwd().access(proposed, .{})) { + break :cwd proposed; + } else |err| { + log.warn("cannot access cwd, ignoring: {}", .{err}); + break :cwd null; + } + } else null; + // In flatpak, we use the HostCommand to execute our shell. if (internal_os.isFlatpak()) flatpak: { if (comptime !build_config.flatpak) { @@ -1058,6 +1099,7 @@ const Subprocess = struct { // Flatpak command must have a stable pointer. self.flatpak_command = .{ .argv = self.args, + .cwd = cwd, .env = if (self.env) |*env| env else null, .stdin = pty.slave, .stdout = pty.slave, @@ -1083,18 +1125,6 @@ const Subprocess = struct { }; } - // If we can't access the cwd, then don't set any cwd and inherit. - // This is important because our cwd can be set by the shell (OSC 7) - // and we don't want to break new windows. - const cwd: ?[]const u8 = if (self.cwd) |proposed| cwd: { - if (std.fs.cwd().access(proposed, .{})) { - break :cwd proposed; - } else |err| { - log.warn("cannot access cwd, ignoring: {}", .{err}); - break :cwd null; - } - } else null; - // Build our subcommand var cmd: Command = .{ .path = self.args[0], From 2caa8a3fe117121bd91ec86ccf1b9ec64e3f5688 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 6 May 2025 14:54:59 -0700 Subject: [PATCH 209/642] macOS: move window title handling fully to AppKit Fixes #7236 Supersedes #7249 This removes all of our `focusedValue`-based tracking of the surface title and moves it completely to the window controller. The window controller now sets up event listeners (via Combine) when the focused surface changes and updates the window title accordingly. There is some complicated logic here to handle when we lose focus to something other than a surface. In this case, we want our title to be the last focused surface so long as it exists. --- .../Terminal/BaseTerminalController.swift | 23 +++++++++++++++++++ .../Features/Terminal/TerminalView.swift | 15 ------------ .../Ghostty/Ghostty.TerminalSplit.swift | 3 --- macos/Sources/Ghostty/InspectorView.swift | 1 - macos/Sources/Ghostty/SurfaceView.swift | 12 ---------- 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 98e3b87f9..62384586a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -1,5 +1,6 @@ import Cocoa import SwiftUI +import Combine import GhosttyKit /// A base class for windows that can contain Ghostty windows. This base class implements @@ -71,6 +72,9 @@ class BaseTerminalController: NSWindowController, /// The configuration derived from the Ghostty config so we don't need to rely on references. private var derivedConfig: DerivedConfig + /// The cancellables related to our focused surface. + private var focusedSurfaceCancellables: Set = [] + struct SavedFrame { let window: NSRect let screen: NSRect @@ -286,7 +290,26 @@ class BaseTerminalController: NSWindowController, func surfaceTreeDidChange() {} func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { + let lastFocusedSurface = focusedSurface focusedSurface = to + + // Important to cancel any prior subscriptions + focusedSurfaceCancellables = [] + + // Setup our title listener. If we have a focused surface we always use that. + // Otherwise, we try to use our last focused surface. In either case, we only + // want to care if the surface is in the tree so we don't listen to titles of + // closed surfaces. + if let titleSurface = focusedSurface ?? lastFocusedSurface, + surfaceTree?.contains(view: titleSurface) ?? false { + // If we have a surface, we want to listen for title changes. + titleSurface.$title + .sink { [weak self] in self?.titleDidChange(to: $0) } + .store(in: &focusedSurfaceCancellables) + } else { + // There is no surface to listen to titles for. + titleDidChange(to: "👻") + } } func titleDidChange(to: String) { diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 1178c75a5..7caceb071 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -8,9 +8,6 @@ protocol TerminalViewDelegate: AnyObject { /// Called when the currently focused surface changed. This can be nil. func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) - /// The title of the terminal should change. - func titleDidChange(to: String) - /// The URL of the pwd should change. func pwdDidChange(to: URL?) @@ -59,19 +56,10 @@ struct TerminalView: View { // Various state values sent back up from the currently focused terminals. @FocusedValue(\.ghosttySurfaceView) private var focusedSurface - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle @FocusedValue(\.ghosttySurfacePwd) private var surfacePwd @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize - // The title for our window - private var title: String { - if let surfaceTitle, !surfaceTitle.isEmpty { - return surfaceTitle - } - return "👻" - } - // The pwd of the focused surface as a URL private var pwdURL: URL? { guard let surfacePwd, surfacePwd != "" else { return nil } @@ -105,9 +93,6 @@ struct TerminalView: View { self.delegate?.focusedSurfaceDidChange(to: newValue) } } - .onChange(of: title) { newValue in - self.delegate?.titleDidChange(to: newValue) - } .onChange(of: pwdURL) { newValue in self.delegate?.pwdDidChange(to: newValue) } diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index 127c925e1..3e942d774 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -45,8 +45,6 @@ extension Ghostty { /// this one. @Binding var zoomedSurface: SurfaceView? - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? - var body: some View { let center = NotificationCenter.default let pubZoom = center.publisher(for: Notification.didToggleSplitZoom) @@ -77,7 +75,6 @@ extension Ghostty { .onReceive(pubZoom) { onZoom(notification: $0) } } } - .navigationTitle(surfaceTitle ?? "Ghostty") .id(node) // Needed for change detection on node } else { // On these events we want to reset the split state and call it. diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index b6147647e..a6e80bd47 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -31,7 +31,6 @@ extension Ghostty { }, right: { InspectorViewRepresentable(surfaceView: surfaceView) .focused($inspectorFocus) - .focusedValue(\.ghosttySurfaceTitle, surfaceView.title) .focusedValue(\.ghosttySurfaceView, surfaceView) }) } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 3b9c10067..1e9a4cfef 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -6,14 +6,12 @@ extension Ghostty { /// Render a terminal for the active app in the environment. struct Terminal: View { @EnvironmentObject private var ghostty: Ghostty.App - @FocusedValue(\.ghosttySurfaceTitle) private var surfaceTitle: String? var body: some View { if let app = self.ghostty.app { SurfaceForApp(app) { surfaceView in SurfaceWrapper(surfaceView: surfaceView) } - .navigationTitle(surfaceTitle ?? "Ghostty") } } } @@ -83,7 +81,6 @@ extension Ghostty { Surface(view: surfaceView, size: geo.size) .focused($surfaceFocus) - .focusedValue(\.ghosttySurfaceTitle, title) .focusedValue(\.ghosttySurfacePwd, surfaceView.pwd) .focusedValue(\.ghosttySurfaceView, surfaceView) .focusedValue(\.ghosttySurfaceCellSize, surfaceView.cellSize) @@ -496,15 +493,6 @@ extension FocusedValues { typealias Value = Ghostty.SurfaceView } - var ghosttySurfaceTitle: String? { - get { self[FocusedGhosttySurfaceTitle.self] } - set { self[FocusedGhosttySurfaceTitle.self] = newValue } - } - - struct FocusedGhosttySurfaceTitle: FocusedValueKey { - typealias Value = String - } - var ghosttySurfacePwd: String? { get { self[FocusedGhosttySurfacePwd.self] } set { self[FocusedGhosttySurfacePwd.self] = newValue } From 071531c5c5233f44e32619a6d6486a6bebfe18b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?fn=20=E2=8C=83=20=E2=8C=A5?= <70830482+FnControlOption@users.noreply.github.com> Date: Sat, 3 May 2025 09:07:12 -0700 Subject: [PATCH 210/642] Add "Scroll to Selection" command --- src/Surface.zig | 8 ++++++++ src/input/Binding.zig | 2 ++ src/input/command.zig | 6 ++++++ 3 files changed, 16 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 6e62f6639..0d4c9d984 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4119,6 +4119,14 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool }, .unlocked); }, + .scroll_to_selection => { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + const sel = self.io.terminal.screen.selection orelse return false; + const tl = sel.topLeft(&self.io.terminal.screen); + self.io.terminal.screen.scroll(.{ .pin = tl }); + }, + .scroll_page_up => { const rows: isize = @intCast(self.size.grid().rows); self.io.queueMessage(.{ diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 434ed9f0d..6bb50fc5d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -279,6 +279,7 @@ pub const Action = union(enum) { /// Scroll the screen varying amounts. scroll_to_top, scroll_to_bottom, + scroll_to_selection, scroll_page_up, scroll_page_down, scroll_page_fractional: f32, @@ -789,6 +790,7 @@ pub const Action = union(enum) { .select_all, .scroll_to_top, .scroll_to_bottom, + .scroll_to_selection, .scroll_page_up, .scroll_page_down, .scroll_page_fractional, diff --git a/src/input/command.zig b/src/input/command.zig index 701d537a1..1f685269b 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -170,6 +170,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Scroll to the bottom of the screen.", }}, + .scroll_to_selection => comptime &.{.{ + .action = .scroll_to_selection, + .title = "Scroll to Selection", + .description = "Scroll to the selected text.", + }}, + .scroll_page_up => comptime &.{.{ .action = .scroll_page_up, .title = "Scroll Page Up", From e8c845b758d08572ce8f4d7e5312a6abe2f7ffb9 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 7 May 2025 08:41:09 -0500 Subject: [PATCH 211/642] core: fixup callconv(.C) -> callconv(.c) https://ziglang.org/download/0.14.0/release-notes.html#Calling-Convention-Enhancements-and-setAlignStack-Replaced --- pkg/cimgui/main.zig | 22 ++++----- pkg/glfw/Joystick.zig | 2 +- pkg/glfw/Monitor.zig | 2 +- pkg/glfw/Window.zig | 32 ++++++------- pkg/glfw/errors.zig | 2 +- pkg/glfw/opengl.zig | 2 +- pkg/glfw/vulkan.zig | 4 +- pkg/harfbuzz/blob.zig | 4 +- pkg/macos/foundation/array.zig | 4 +- pkg/macos/video/display_link.zig | 2 +- pkg/opengl/glad.zig | 4 +- pkg/sentry/transport.zig | 4 +- src/apprt/embedded.zig | 12 ++--- src/apprt/gtk/App.zig | 2 +- src/apprt/gtk/ClipboardConfirmationWindow.zig | 6 +-- src/apprt/gtk/CloseDialog.zig | 4 +- src/apprt/gtk/ConfigErrorsDialog.zig | 2 +- src/apprt/gtk/ImguiWidget.zig | 28 +++++------ src/apprt/gtk/ResizeOverlay.zig | 4 +- src/apprt/gtk/Surface.zig | 48 +++++++++---------- src/apprt/gtk/Tab.zig | 2 +- src/apprt/gtk/TabView.zig | 8 ++-- src/apprt/gtk/URLWidget.zig | 4 +- src/apprt/gtk/Window.zig | 36 +++++++------- src/apprt/gtk/inspector.zig | 2 +- src/apprt/gtk/menu.zig | 2 +- src/apprt/gtk/winproto/wayland.zig | 2 +- src/crash/sentry.zig | 4 +- src/os/flatpak.zig | 2 +- src/renderer/Metal.zig | 2 +- src/renderer/shadertoy.zig | 2 +- 31 files changed, 128 insertions(+), 128 deletions(-) diff --git a/pkg/cimgui/main.zig b/pkg/cimgui/main.zig index e6e54c357..b890a49ee 100644 --- a/pkg/cimgui/main.zig +++ b/pkg/cimgui/main.zig @@ -1,20 +1,20 @@ pub const c = @import("c.zig").c; // OpenGL -pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.C) bool; -pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.C) void; -pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.C) void; -pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.C) void; +pub extern fn ImGui_ImplOpenGL3_Init(?[*:0]const u8) callconv(.c) bool; +pub extern fn ImGui_ImplOpenGL3_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplOpenGL3_NewFrame() callconv(.c) void; +pub extern fn ImGui_ImplOpenGL3_RenderDrawData(*c.ImDrawData) callconv(.c) void; // Metal -pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.C) bool; -pub extern fn ImGui_ImplMetal_Shutdown() callconv(.C) void; -pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.C) void; -pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.C) void; +pub extern fn ImGui_ImplMetal_Init(*anyopaque) callconv(.c) bool; +pub extern fn ImGui_ImplMetal_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplMetal_NewFrame(*anyopaque) callconv(.c) void; +pub extern fn ImGui_ImplMetal_RenderDrawData(*c.ImDrawData, *anyopaque, *anyopaque) callconv(.c) void; // OSX -pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.C) bool; -pub extern fn ImGui_ImplOSX_Shutdown() callconv(.C) void; -pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.C) void; +pub extern fn ImGui_ImplOSX_Init(*anyopaque) callconv(.c) bool; +pub extern fn ImGui_ImplOSX_Shutdown() callconv(.c) void; +pub extern fn ImGui_ImplOSX_NewFrame(*anyopaque) callconv(.c) void; test {} diff --git a/pkg/glfw/Joystick.zig b/pkg/glfw/Joystick.zig index dd55c731d..a8152513e 100644 --- a/pkg/glfw/Joystick.zig +++ b/pkg/glfw/Joystick.zig @@ -333,7 +333,7 @@ pub inline fn setCallback(comptime callback: ?fn (joystick: Joystick, event: Eve if (callback) |user_callback| { const CWrapper = struct { - pub fn joystickCallbackWrapper(jid: c_int, event: c_int) callconv(.C) void { + pub fn joystickCallbackWrapper(jid: c_int, event: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ Joystick{ .jid = @as(Joystick.Id, @enumFromInt(jid)) }, @as(Event, @enumFromInt(event)), diff --git a/pkg/glfw/Monitor.zig b/pkg/glfw/Monitor.zig index 868872e19..4accb23cd 100644 --- a/pkg/glfw/Monitor.zig +++ b/pkg/glfw/Monitor.zig @@ -389,7 +389,7 @@ pub inline fn setCallback(comptime callback: ?fn (monitor: Monitor, event: Event if (callback) |user_callback| { const CWrapper = struct { - pub fn monitorCallbackWrapper(monitor: ?*c.GLFWmonitor, event: c_int) callconv(.C) void { + pub fn monitorCallbackWrapper(monitor: ?*c.GLFWmonitor, event: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ Monitor{ .handle = monitor.? }, @as(Event, @enumFromInt(event)), diff --git a/pkg/glfw/Window.zig b/pkg/glfw/Window.zig index 29dcac23e..804184f0e 100644 --- a/pkg/glfw/Window.zig +++ b/pkg/glfw/Window.zig @@ -1230,7 +1230,7 @@ pub inline fn setPosCallback(self: Window, comptime callback: ?fn (window: Windo if (callback) |user_callback| { const CWrapper = struct { - pub fn posCallbackWrapper(handle: ?*c.GLFWwindow, xpos: c_int, ypos: c_int) callconv(.C) void { + pub fn posCallbackWrapper(handle: ?*c.GLFWwindow, xpos: c_int, ypos: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as(i32, @intCast(xpos)), @@ -1263,7 +1263,7 @@ pub inline fn setSizeCallback(self: Window, comptime callback: ?fn (window: Wind if (callback) |user_callback| { const CWrapper = struct { - pub fn sizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void { + pub fn sizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as(i32, @intCast(width)), @@ -1304,7 +1304,7 @@ pub inline fn setCloseCallback(self: Window, comptime callback: ?fn (window: Win if (callback) |user_callback| { const CWrapper = struct { - pub fn closeCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.C) void { + pub fn closeCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), }); @@ -1341,7 +1341,7 @@ pub inline fn setRefreshCallback(self: Window, comptime callback: ?fn (window: W if (callback) |user_callback| { const CWrapper = struct { - pub fn refreshCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.C) void { + pub fn refreshCallbackWrapper(handle: ?*c.GLFWwindow) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), }); @@ -1379,7 +1379,7 @@ pub inline fn setFocusCallback(self: Window, comptime callback: ?fn (window: Win if (callback) |user_callback| { const CWrapper = struct { - pub fn focusCallbackWrapper(handle: ?*c.GLFWwindow, focused: c_int) callconv(.C) void { + pub fn focusCallbackWrapper(handle: ?*c.GLFWwindow, focused: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), focused == c.GLFW_TRUE, @@ -1413,7 +1413,7 @@ pub inline fn setIconifyCallback(self: Window, comptime callback: ?fn (window: W if (callback) |user_callback| { const CWrapper = struct { - pub fn iconifyCallbackWrapper(handle: ?*c.GLFWwindow, iconified: c_int) callconv(.C) void { + pub fn iconifyCallbackWrapper(handle: ?*c.GLFWwindow, iconified: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), iconified == c.GLFW_TRUE, @@ -1448,7 +1448,7 @@ pub inline fn setMaximizeCallback(self: Window, comptime callback: ?fn (window: if (callback) |user_callback| { const CWrapper = struct { - pub fn maximizeCallbackWrapper(handle: ?*c.GLFWwindow, maximized: c_int) callconv(.C) void { + pub fn maximizeCallbackWrapper(handle: ?*c.GLFWwindow, maximized: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), maximized == c.GLFW_TRUE, @@ -1483,7 +1483,7 @@ pub inline fn setFramebufferSizeCallback(self: Window, comptime callback: ?fn (w if (callback) |user_callback| { const CWrapper = struct { - pub fn framebufferSizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.C) void { + pub fn framebufferSizeCallbackWrapper(handle: ?*c.GLFWwindow, width: c_int, height: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as(u32, @intCast(width)), @@ -1519,7 +1519,7 @@ pub inline fn setContentScaleCallback(self: Window, comptime callback: ?fn (wind if (callback) |user_callback| { const CWrapper = struct { - pub fn windowScaleCallbackWrapper(handle: ?*c.GLFWwindow, xscale: f32, yscale: f32) callconv(.C) void { + pub fn windowScaleCallbackWrapper(handle: ?*c.GLFWwindow, xscale: f32, yscale: f32) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), xscale, @@ -1871,7 +1871,7 @@ pub inline fn setKeyCallback(self: Window, comptime callback: ?fn (window: Windo if (callback) |user_callback| { const CWrapper = struct { - pub fn keyCallbackWrapper(handle: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.C) void { + pub fn keyCallbackWrapper(handle: ?*c.GLFWwindow, key: c_int, scancode: c_int, action: c_int, mods: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as(Key, @enumFromInt(key)), @@ -1917,7 +1917,7 @@ pub inline fn setCharCallback(self: Window, comptime callback: ?fn (window: Wind if (callback) |user_callback| { const CWrapper = struct { - pub fn charCallbackWrapper(handle: ?*c.GLFWwindow, codepoint: c_uint) callconv(.C) void { + pub fn charCallbackWrapper(handle: ?*c.GLFWwindow, codepoint: c_uint) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as(u21, @intCast(codepoint)), @@ -1958,7 +1958,7 @@ pub inline fn setMouseButtonCallback(self: Window, comptime callback: ?fn (windo if (callback) |user_callback| { const CWrapper = struct { - pub fn mouseButtonCallbackWrapper(handle: ?*c.GLFWwindow, button: c_int, action: c_int, mods: c_int) callconv(.C) void { + pub fn mouseButtonCallbackWrapper(handle: ?*c.GLFWwindow, button: c_int, action: c_int, mods: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as(MouseButton, @enumFromInt(button)), @@ -1996,7 +1996,7 @@ pub inline fn setCursorPosCallback(self: Window, comptime callback: ?fn (window: if (callback) |user_callback| { const CWrapper = struct { - pub fn cursorPosCallbackWrapper(handle: ?*c.GLFWwindow, xpos: f64, ypos: f64) callconv(.C) void { + pub fn cursorPosCallbackWrapper(handle: ?*c.GLFWwindow, xpos: f64, ypos: f64) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), xpos, @@ -2030,7 +2030,7 @@ pub inline fn setCursorEnterCallback(self: Window, comptime callback: ?fn (windo if (callback) |user_callback| { const CWrapper = struct { - pub fn cursorEnterCallbackWrapper(handle: ?*c.GLFWwindow, entered: c_int) callconv(.C) void { + pub fn cursorEnterCallbackWrapper(handle: ?*c.GLFWwindow, entered: c_int) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), entered == c.GLFW_TRUE, @@ -2067,7 +2067,7 @@ pub inline fn setScrollCallback(self: Window, comptime callback: ?fn (window: Wi if (callback) |user_callback| { const CWrapper = struct { - pub fn scrollCallbackWrapper(handle: ?*c.GLFWwindow, xoffset: f64, yoffset: f64) callconv(.C) void { + pub fn scrollCallbackWrapper(handle: ?*c.GLFWwindow, xoffset: f64, yoffset: f64) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), xoffset, @@ -2110,7 +2110,7 @@ pub inline fn setDropCallback(self: Window, comptime callback: ?fn (window: Wind if (callback) |user_callback| { const CWrapper = struct { - pub fn dropCallbackWrapper(handle: ?*c.GLFWwindow, path_count: c_int, paths: [*c][*c]const u8) callconv(.C) void { + pub fn dropCallbackWrapper(handle: ?*c.GLFWwindow, path_count: c_int, paths: [*c][*c]const u8) callconv(.c) void { @call(.always_inline, user_callback, .{ from(handle.?), @as([*][*:0]const u8, @ptrCast(paths))[0..@as(u32, @intCast(path_count))], diff --git a/pkg/glfw/errors.zig b/pkg/glfw/errors.zig index ce98ec5cd..b9721fd05 100644 --- a/pkg/glfw/errors.zig +++ b/pkg/glfw/errors.zig @@ -300,7 +300,7 @@ pub inline fn mustGetErrorString() [:0]const u8 { pub fn setErrorCallback(comptime callback: ?fn (error_code: ErrorCode, description: [:0]const u8) void) void { if (callback) |user_callback| { const CWrapper = struct { - pub fn errorCallbackWrapper(err_int: c_int, c_description: [*c]const u8) callconv(.C) void { + pub fn errorCallbackWrapper(err_int: c_int, c_description: [*c]const u8) callconv(.c) void { convertError(err_int) catch |error_code| { user_callback(error_code, mem.sliceTo(c_description, 0)); }; diff --git a/pkg/glfw/opengl.zig b/pkg/glfw/opengl.zig index de99582c2..04bc3a65c 100644 --- a/pkg/glfw/opengl.zig +++ b/pkg/glfw/opengl.zig @@ -161,7 +161,7 @@ pub const GLProc = *const fn () callconv(if (builtin.os.tag == .windows and buil /// @thread_safety This function may be called from any thread. /// /// see also: context_glext, glfwExtensionSupported -pub fn getProcAddress(proc_name: [*:0]const u8) callconv(.C) ?GLProc { +pub fn getProcAddress(proc_name: [*:0]const u8) callconv(.c) ?GLProc { internal_debug.assertInitialized(); if (c.glfwGetProcAddress(proc_name)) |proc_address| return @ptrCast(proc_address); return null; diff --git a/pkg/glfw/vulkan.zig b/pkg/glfw/vulkan.zig index 6c6021d02..1b84145d5 100644 --- a/pkg/glfw/vulkan.zig +++ b/pkg/glfw/vulkan.zig @@ -33,7 +33,7 @@ pub fn initVulkanLoader(loader_function: ?VKGetInstanceProcAddr) void { c.glfwInitVulkanLoader(loader_function orelse null); } -pub const VKGetInstanceProcAddr = *const fn (vk_instance: c.VkInstance, name: [*c]const u8) callconv(.C) ?VKProc; +pub const VKGetInstanceProcAddr = *const fn (vk_instance: c.VkInstance, name: [*c]const u8) callconv(.c) ?VKProc; /// Returns whether the Vulkan loader and an ICD have been found. /// @@ -127,7 +127,7 @@ pub const VKProc = *const fn () callconv(if (builtin.os.tag == .windows and buil /// @pointer_lifetime The returned function pointer is valid until the library is terminated. /// /// @thread_safety This function may be called from any thread. -pub fn getInstanceProcAddress(vk_instance: ?*anyopaque, proc_name: [*:0]const u8) callconv(.C) ?VKProc { +pub fn getInstanceProcAddress(vk_instance: ?*anyopaque, proc_name: [*:0]const u8) callconv(.c) ?VKProc { internal_debug.assertInitialized(); if (c.glfwGetInstanceProcAddress(if (vk_instance) |v| @as(c.VkInstance, @ptrCast(v)) else null, proc_name)) |proc_address| return proc_address; return null; diff --git a/pkg/harfbuzz/blob.zig b/pkg/harfbuzz/blob.zig index d25df6974..9472e4c75 100644 --- a/pkg/harfbuzz/blob.zig +++ b/pkg/harfbuzz/blob.zig @@ -77,11 +77,11 @@ pub const Blob = struct { comptime T: type, key: ?*anyopaque, ptr: ?*T, - comptime destroycb: ?*const fn (?*T) callconv(.C) void, + comptime destroycb: ?*const fn (?*T) callconv(.c) void, replace: bool, ) bool { const Callback = struct { - pub fn callback(data: ?*anyopaque) callconv(.C) void { + pub fn callback(data: ?*anyopaque) callconv(.c) void { @call(.{ .modifier = .always_inline }, destroycb, .{ @as(?*T, @ptrCast(@alignCast(data))), }); diff --git a/pkg/macos/foundation/array.zig b/pkg/macos/foundation/array.zig index 37fa2b985..d3a977539 100644 --- a/pkg/macos/foundation/array.zig +++ b/pkg/macos/foundation/array.zig @@ -84,7 +84,7 @@ pub const MutableArray = opaque { a: *const Elem, b: *const Elem, context: ?*Context, - ) callconv(.C) ComparisonResult, + ) callconv(.c) ComparisonResult, ) void { CFArraySortValues( self, @@ -155,7 +155,7 @@ test "array sorting" { void, null, struct { - fn compare(a: *const u8, b: *const u8, _: ?*void) callconv(.C) ComparisonResult { + fn compare(a: *const u8, b: *const u8, _: ?*void) callconv(.c) ComparisonResult { if (a.* > b.*) return .greater; if (a.* == b.*) return .equal; return .less; diff --git a/pkg/macos/video/display_link.zig b/pkg/macos/video/display_link.zig index ca0c80d0b..4bbf58a0c 100644 --- a/pkg/macos/video/display_link.zig +++ b/pkg/macos/video/display_link.zig @@ -66,7 +66,7 @@ pub const DisplayLink = opaque { flagsIn: c.CVOptionFlags, flagsOut: *c.CVOptionFlags, inner_userinfo: ?*anyopaque, - ) callconv(.C) c.CVReturn { + ) callconv(.c) c.CVReturn { _ = inNow; _ = inOutputTime; _ = flagsIn; diff --git a/pkg/opengl/glad.zig b/pkg/opengl/glad.zig index 79a2e4d6b..663e75e12 100644 --- a/pkg/opengl/glad.zig +++ b/pkg/opengl/glad.zig @@ -13,8 +13,8 @@ pub threadlocal var context: Context = undefined; /// The getProcAddress param is an anytype so that we can accept multiple /// forms of the function depending on what we're interfacing with. pub fn load(getProcAddress: anytype) !c_int { - const GlProc = *const fn () callconv(.C) void; - const GlfwFn = *const fn ([*:0]const u8) callconv(.C) ?GlProc; + const GlProc = *const fn () callconv(.c) void; + const GlfwFn = *const fn ([*:0]const u8) callconv(.c) ?GlProc; const res = switch (@TypeOf(getProcAddress)) { // glfw diff --git a/pkg/sentry/transport.zig b/pkg/sentry/transport.zig index 835b87cd3..747187211 100644 --- a/pkg/sentry/transport.zig +++ b/pkg/sentry/transport.zig @@ -5,8 +5,8 @@ const Envelope = @import("envelope.zig").Envelope; /// sentry_transport_t pub const Transport = opaque { - pub const SendFunc = *const fn (envelope: *Envelope, state: ?*anyopaque) callconv(.C) void; - pub const FreeFunc = *const fn (state: ?*anyopaque) callconv(.C) void; + pub const SendFunc = *const fn (envelope: *Envelope, state: ?*anyopaque) callconv(.c) void; + pub const FreeFunc = *const fn (state: ?*anyopaque) callconv(.c) void; pub fn init(f: SendFunc) *Transport { return @ptrCast(c.sentry_transport_new(@ptrCast(f)).?); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 22ae6e488..c953300cd 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -43,15 +43,15 @@ pub const App = struct { /// Callback called to wakeup the event loop. This should trigger /// a full tick of the app loop. - wakeup: *const fn (AppUD) callconv(.C) void, + wakeup: *const fn (AppUD) callconv(.c) void, /// Callback called to handle an action. - action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.C) bool, + action: *const fn (*App, apprt.Target.C, apprt.Action.C) callconv(.c) bool, /// Read the clipboard value. The return value must be preserved /// by the host until the next call. If there is no valid clipboard /// value then this should return null. - read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.C) void, + read_clipboard: *const fn (SurfaceUD, c_int, *apprt.ClipboardRequest) callconv(.c) void, /// This may be called after a read clipboard call to request /// confirmation that the clipboard value is safe to read. The embedder @@ -61,13 +61,13 @@ pub const App = struct { [*:0]const u8, *apprt.ClipboardRequest, apprt.ClipboardRequestType, - ) callconv(.C) void, + ) callconv(.c) void, /// Write the clipboard value. - write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.C) void, + write_clipboard: *const fn (SurfaceUD, [*:0]const u8, c_int, bool) callconv(.c) void, /// Close the current surface given by this function. - close_surface: ?*const fn (SurfaceUD, bool) callconv(.C) void = null, + close_surface: ?*const fn (SurfaceUD, bool) callconv(.c) void = null, }; /// This is the key event sent for ghostty_surface_key and diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 9ca1629ba..c9a973611 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1527,7 +1527,7 @@ fn adwNotifyDark( style_manager: *adw.StyleManager, _: *gobject.ParamSpec, self: *App, -) callconv(.C) void { +) callconv(.c) void { const color_scheme: apprt.ColorScheme = if (style_manager.getDark() == 0) .light else diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index 583a58a2c..f10fc79ac 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -152,7 +152,7 @@ fn init( } } -fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void { +fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void { if (std.mem.orderZ(u8, response, "ok") == .eq) { self.core_surface.completeClipboardRequest( self.pending_req, @@ -165,7 +165,7 @@ fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) self.destroy(); } -fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void { +fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void { self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true)); self.text_view.as(gtk.Widget).removeCssClass("blurred"); @@ -173,7 +173,7 @@ fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false)); } -fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.C) void { +fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void { self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false)); self.text_view.as(gtk.Widget).addCssClass("blurred"); diff --git a/src/apprt/gtk/CloseDialog.zig b/src/apprt/gtk/CloseDialog.zig index ea683c477..559737cf4 100644 --- a/src/apprt/gtk/CloseDialog.zig +++ b/src/apprt/gtk/CloseDialog.zig @@ -64,7 +64,7 @@ fn responseCallback( _: *DialogType, response: [*:0]const u8, target: *Target, -) callconv(.C) void { +) callconv(.c) void { const alloc = target.allocator(); defer alloc.destroy(target); @@ -141,7 +141,7 @@ pub const Target = union(enum) { } }; -fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.C) c_int { +fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int { const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1))); // Confusingly, `isActive` returns 1 when active, diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig index 7a245a1a0..a1a2a61af 100644 --- a/src/apprt/gtk/ConfigErrorsDialog.zig +++ b/src/apprt/gtk/ConfigErrorsDialog.zig @@ -67,7 +67,7 @@ pub fn maybePresent(app: *App, window: ?*Window) void { } } -fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.C) void { +fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void { if (std.mem.orderZ(u8, response, "reload") == .eq) { app.reloadConfig(.app, .{}) catch |err| { log.warn("error reloading config error={}", .{err}); diff --git a/src/apprt/gtk/ImguiWidget.zig b/src/apprt/gtk/ImguiWidget.zig index f1f0c8f6b..338fd7982 100644 --- a/src/apprt/gtk/ImguiWidget.zig +++ b/src/apprt/gtk/ImguiWidget.zig @@ -221,12 +221,12 @@ fn translateMouseButton(button: c_uint) ?c_int { }; } -fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void { +fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void { log.debug("imgui widget destroy", .{}); self.deinit(); } -fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void { +fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void { log.debug("gl surface realized", .{}); // We need to make the context current so we can call GL functions. @@ -242,7 +242,7 @@ fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void { _ = cimgui.ImGui_ImplOpenGL3_Init(null); } -fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void { +fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void { _ = area; log.debug("gl surface unrealized", .{}); @@ -250,7 +250,7 @@ fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.C) void { cimgui.ImGui_ImplOpenGL3_Shutdown(); } -fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.C) void { +fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void { cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); const scale_factor = area.as(gtk.Widget).getScaleFactor(); @@ -273,7 +273,7 @@ fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) active_style.* = style.*; } -fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.C) c_int { +fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int { cimgui.c.igSetCurrentContext(self.ig_ctx); // Setup our frame. We render twice because some ImGui behaviors @@ -307,7 +307,7 @@ fn gtkMouseMotion( x: f64, y: f64, self: *ImguiWidget, -) callconv(.C) void { +) callconv(.c) void { cimgui.c.igSetCurrentContext(self.ig_ctx); const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO(); const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor()); @@ -325,7 +325,7 @@ fn gtkMouseDown( _: f64, _: f64, self: *ImguiWidget, -) callconv(.C) void { +) callconv(.c) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); @@ -343,7 +343,7 @@ fn gtkMouseUp( _: f64, _: f64, self: *ImguiWidget, -) callconv(.C) void { +) callconv(.c) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); @@ -359,7 +359,7 @@ fn gtkMouseScroll( x: f64, y: f64, self: *ImguiWidget, -) callconv(.C) c_int { +) callconv(.c) c_int { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); @@ -373,7 +373,7 @@ fn gtkMouseScroll( return @intFromBool(true); } -fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) void { +fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); @@ -381,7 +381,7 @@ fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) cimgui.c.ImGuiIO_AddFocusEvent(io, true); } -fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.C) void { +fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); @@ -393,7 +393,7 @@ fn gtkInputCommit( _: *gtk.IMMulticontext, bytes: [*:0]u8, self: *ImguiWidget, -) callconv(.C) void { +) callconv(.c) void { self.queueRender(); cimgui.c.igSetCurrentContext(self.ig_ctx); @@ -407,7 +407,7 @@ fn gtkKeyPressed( keycode: c_uint, gtk_mods: gdk.ModifierType, self: *ImguiWidget, -) callconv(.C) c_int { +) callconv(.c) c_int { return @intFromBool(self.keyEvent( .press, ec_key, @@ -423,7 +423,7 @@ fn gtkKeyReleased( keycode: c_uint, gtk_mods: gdk.ModifierType, self: *ImguiWidget, -) callconv(.C) void { +) callconv(.c) void { _ = self.keyEvent( .release, ec_key, diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig index 47f2aea1a..767cf097d 100644 --- a/src/apprt/gtk/ResizeOverlay.zig +++ b/src/apprt/gtk/ResizeOverlay.zig @@ -104,7 +104,7 @@ pub fn maybeShow(self: *ResizeOverlay) void { /// Actually update the overlay widget. This should only be called from a GTK /// idle handler. -fn gtkUpdate(ud: ?*anyopaque) callconv(.C) c_int { +fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int { const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0)); // No matter what our idler is complete with this callback @@ -198,7 +198,7 @@ fn setPosition(label: *gtk.Label, config: *DerivedConfig) void { /// If this fires, it means that the delay period has expired and the resize /// overlay widget should be hidden. -fn gtkTimerExpired(ud: ?*anyopaque) callconv(.C) c_int { +fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int { const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0)); self.timer = null; if (self.label) |label| hide(label); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 4ad2eeb13..7ff96480e 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1025,7 +1025,7 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8, source: SetTitleSource) !vo self.update_title_timer = glib.timeoutAdd(75, updateTitleTimerExpired, self); } -fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.C) c_int { +fn updateTitleTimerExpired(ud: ?*anyopaque) callconv(.c) c_int { const self: *Surface = @ptrCast(@alignCast(ud.?)); self.updateTitleLabels(); @@ -1265,7 +1265,7 @@ fn gtkClipboardRead( source: ?*gobject.Object, res: *gio.AsyncResult, ud: ?*anyopaque, -) callconv(.C) void { +) callconv(.c) void { const clipboard = gobject.ext.cast(gdk.Clipboard, source orelse return) orelse return; const req: *ClipboardRequest = @ptrCast(@alignCast(ud orelse return)); const self = req.self; @@ -1349,7 +1349,7 @@ pub fn showDesktopNotification( app.sendNotification(body.ptr, notification); } -fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void { +fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void { log.debug("gl surface realized", .{}); // We need to make the context current so we can call GL functions. @@ -1377,7 +1377,7 @@ fn gtkRealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void { /// This is called when the underlying OpenGL resources must be released. /// This is usually due to the OpenGL area changing GDK surfaces. -fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void { +fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.c) void { log.debug("gl surface unrealized", .{}); // See gtkRealize for why we do this here. @@ -1405,7 +1405,7 @@ fn gtkUnrealize(gl_area: *gtk.GLArea, self: *Surface) callconv(.C) void { } /// render signal -fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_int { +fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.c) c_int { self.render() catch |err| { log.err("surface failed to render: {}", .{err}); return 0; @@ -1415,7 +1415,7 @@ fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *Surface) callconv(.C) c_i } /// resize signal -fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.C) void { +fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) callconv(.c) void { // Some debug output to help understand what GTK is telling us. { const scale_factor = scale: { @@ -1471,7 +1471,7 @@ fn gtkResize(gl_area: *gtk.GLArea, width: c_int, height: c_int, self: *Surface) } /// "destroy" signal for surface -fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.C) void { +fn gtkDestroy(_: *gtk.GLArea, self: *Surface) callconv(.c) void { log.debug("gl destroy", .{}); const alloc = self.app.core_app.alloc; @@ -1505,7 +1505,7 @@ fn gtkMouseDown( x: f64, y: f64, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return; const gtk_mods = event.getModifierState(); @@ -1538,7 +1538,7 @@ fn gtkMouseUp( _: f64, _: f64, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { const event = gesture.as(gtk.EventController).getCurrentEvent() orelse return; const gtk_mods = event.getModifierState(); @@ -1557,7 +1557,7 @@ fn gtkMouseMotion( x: f64, y: f64, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { const event = ec.as(gtk.EventController).getCurrentEvent() orelse return; const scaled = self.scaledCoordinates(x, y); @@ -1603,7 +1603,7 @@ fn gtkMouseMotion( fn gtkMouseLeave( ec_motion: *gtk.EventControllerMotion, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { const event = ec_motion.as(gtk.EventController).getCurrentEvent() orelse return; // Get our modifiers @@ -1618,14 +1618,14 @@ fn gtkMouseLeave( fn gtkMouseScrollPrecisionBegin( _: *gtk.EventControllerScroll, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { self.precision_scroll = true; } fn gtkMouseScrollPrecisionEnd( _: *gtk.EventControllerScroll, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { self.precision_scroll = false; } @@ -1634,7 +1634,7 @@ fn gtkMouseScroll( x: f64, y: f64, self: *Surface, -) callconv(.C) c_int { +) callconv(.c) c_int { const scaled = self.scaledCoordinates(x, y); // GTK doesn't support any of the scroll mods. @@ -1664,7 +1664,7 @@ fn gtkKeyPressed( keycode: c_uint, gtk_mods: gdk.ModifierType, self: *Surface, -) callconv(.C) c_int { +) callconv(.c) c_int { return @intFromBool(self.keyEvent( .press, ec_key, @@ -1680,7 +1680,7 @@ fn gtkKeyReleased( keycode: c_uint, state: gdk.ModifierType, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { _ = self.keyEvent( .release, ec_key, @@ -1971,7 +1971,7 @@ pub fn keyEvent( fn gtkInputPreeditStart( _: *gtk.IMMulticontext, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { // log.warn("GTKIM: preedit start", .{}); // Start our composing state for the input method and reset our @@ -1983,7 +1983,7 @@ fn gtkInputPreeditStart( fn gtkInputPreeditChanged( ctx: *gtk.IMMulticontext, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { // Any preedit change should mark that we're composing. Its possible this // is false using fcitx5-hangul and typing "dkssud" ("안녕"). The // second "s" results in a "commit" for "안" which sets composing to false, @@ -2009,7 +2009,7 @@ fn gtkInputPreeditChanged( fn gtkInputPreeditEnd( _: *gtk.IMMulticontext, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { // log.warn("GTKIM: preedit end", .{}); // End our composing state for GTK, allowing us to commit the text. @@ -2025,7 +2025,7 @@ fn gtkInputCommit( _: *gtk.IMMulticontext, bytes: [*:0]u8, self: *Surface, -) callconv(.C) void { +) callconv(.c) void { const str = std.mem.sliceTo(bytes, 0); // log.debug("GTKIM: input commit composing={} keyevent={} str={s}", .{ @@ -2100,7 +2100,7 @@ fn gtkInputCommit( }; } -fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void { +fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void { if (!self.realized) return; // Notify our IM context @@ -2125,7 +2125,7 @@ fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void }; } -fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.C) void { +fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *Surface) callconv(.c) void { if (!self.realized) return; // Notify our IM context @@ -2243,7 +2243,7 @@ fn gtkDrop( _: f64, _: f64, self: *Surface, -) callconv(.C) c_int { +) callconv(.c) c_int { const alloc = self.app.core_app.alloc; if (g_value_holds(value, gdk.FileList.getGObjectType())) { @@ -2395,7 +2395,7 @@ fn g_value_holds(value_: ?*gobject.Value, g_type: gobject.Type) bool { return false; } -fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void { +fn gtkPromptTitleResponse(source_object: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void { if (!adw_version.supportsDialogs()) return; const dialog = gobject.ext.cast(adw.AlertDialog, source_object.?).?; const self: *Surface = @ptrCast(@alignCast(ud)); diff --git a/src/apprt/gtk/Tab.zig b/src/apprt/gtk/Tab.zig index 57a9644d9..c32fa19fc 100644 --- a/src/apprt/gtk/Tab.zig +++ b/src/apprt/gtk/Tab.zig @@ -161,7 +161,7 @@ pub fn closeWithConfirmation(tab: *Tab) void { } } -fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.C) void { +fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void { log.debug("tab box destroy", .{}); const alloc = self.window.app.core_app.alloc; diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig index ddd0951d2..29a069a6d 100644 --- a/src/apprt/gtk/TabView.zig +++ b/src/apprt/gtk/TabView.zig @@ -227,7 +227,7 @@ pub fn createWindow(window: *Window) !*Window { return new_window; } -fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.C) void { +fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void { const child = page.getChild().as(gobject.Object); const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return)); tab.window = self.window; @@ -239,7 +239,7 @@ fn adwClosePage( _: *adw.TabView, page: *adw.TabPage, self: *TabView, -) callconv(.C) c_int { +) callconv(.c) c_int { const child = page.getChild().as(gobject.Object); const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0)); self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close)); @@ -251,7 +251,7 @@ fn adwClosePage( fn adwTabViewCreateWindow( _: *adw.TabView, self: *TabView, -) callconv(.C) ?*adw.TabView { +) callconv(.c) ?*adw.TabView { const window = createWindow(self.window) catch |err| { log.warn("error creating new window error={}", .{err}); return null; @@ -259,7 +259,7 @@ fn adwTabViewCreateWindow( return window.notebook.tab_view; } -fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.C) void { +fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void { const page = self.tab_view.getSelectedPage() orelse return; // If the tab was previously marked as needing attention diff --git a/src/apprt/gtk/URLWidget.zig b/src/apprt/gtk/URLWidget.zig index d1628aa6e..e59827aaf 100644 --- a/src/apprt/gtk/URLWidget.zig +++ b/src/apprt/gtk/URLWidget.zig @@ -101,7 +101,7 @@ fn gtkLeftEnter( _: f64, _: f64, right: *gtk.Label, -) callconv(.C) void { +) callconv(.c) void { right.as(gtk.Widget).removeCssClass("hidden"); } @@ -110,6 +110,6 @@ fn gtkLeftEnter( fn gtkLeftLeave( _: *gtk.EventControllerMotion, right: *gtk.Label, -) callconv(.C) void { +) callconv(.c) void { right.as(gtk.Widget).addCssClass("hidden"); } diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index e130cd1be..d82087ff0 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -792,7 +792,7 @@ fn gtkWindowNotifyIsActive( _: *adw.ApplicationWindow, _: *gobject.ParamSpec, self: *Window, -) callconv(.C) void { +) callconv(.c) void { if (!self.isQuickTerminal()) return; // Hide when we're unfocused @@ -883,7 +883,7 @@ fn adwTabOverviewOpen( fn adwTabOverviewFocusTimer( ud: ?*anyopaque, -) callconv(.C) c_int { +) callconv(.c) c_int { if (!adw_version.supportsTabOverview()) unreachable; const self: *Window = @ptrCast(@alignCast(ud orelse return 0)); self.adw_tab_overview_focus_timer = null; @@ -970,7 +970,7 @@ fn gtkActionAbout( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { const name = "Ghostty"; const icon = "com.mitchellh.ghostty"; const website = "https://ghostty.org"; @@ -1014,7 +1014,7 @@ fn gtkActionClose( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.closeWithConfirmation(); } @@ -1022,7 +1022,7 @@ fn gtkActionNewWindow( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .new_window = {} }); } @@ -1030,7 +1030,7 @@ fn gtkActionNewTab( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .new_tab = {} }); } @@ -1038,7 +1038,7 @@ fn gtkActionCloseTab( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .close_tab = {} }); } @@ -1046,7 +1046,7 @@ fn gtkActionSplitRight( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .new_split = .right }); } @@ -1054,7 +1054,7 @@ fn gtkActionSplitDown( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .new_split = .down }); } @@ -1062,7 +1062,7 @@ fn gtkActionSplitLeft( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .new_split = .left }); } @@ -1070,7 +1070,7 @@ fn gtkActionSplitUp( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .new_split = .up }); } @@ -1078,7 +1078,7 @@ fn gtkActionToggleInspector( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .inspector = .toggle }); } @@ -1086,7 +1086,7 @@ fn gtkActionCopy( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .copy_to_clipboard = {} }); } @@ -1094,7 +1094,7 @@ fn gtkActionPaste( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .paste_from_clipboard = {} }); } @@ -1102,7 +1102,7 @@ fn gtkActionReset( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .reset = {} }); } @@ -1110,7 +1110,7 @@ fn gtkActionClear( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .clear_screen = {} }); } @@ -1118,7 +1118,7 @@ fn gtkActionPromptTitle( _: *gio.SimpleAction, _: ?*glib.Variant, self: *Window, -) callconv(.C) void { +) callconv(.c) void { self.performBindingAction(.{ .prompt_surface_title = {} }); } @@ -1133,7 +1133,7 @@ fn gtkTitlebarMenuActivate( btn: *gtk.MenuButton, _: *gobject.ParamSpec, self: *Window, -) callconv(.C) void { +) callconv(.c) void { // debian 12 is stuck on GTK 4.8 if (!gtk_version.atLeast(4, 10, 0)) return; const active = btn.getActive() != 0; diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig index aa4f6e435..e3e61e258 100644 --- a/src/apprt/gtk/inspector.zig +++ b/src/apprt/gtk/inspector.zig @@ -177,7 +177,7 @@ const Window = struct { } /// "destroy" signal for the window - fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.C) void { + fn gtkDestroy(_: *gtk.ApplicationWindow, self: *Window) callconv(.c) void { log.debug("window destroy", .{}); self.deinit(); } diff --git a/src/apprt/gtk/menu.zig b/src/apprt/gtk/menu.zig index f63a0eb5f..d9d0083d0 100644 --- a/src/apprt/gtk/menu.zig +++ b/src/apprt/gtk/menu.zig @@ -130,7 +130,7 @@ pub fn Menu( } /// Refocus tab that lost focus because of the popover menu - fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.C) void { + fn gtkRefocusTerm(_: *gtk.PopoverMenu, self: *Self) callconv(.c) void { const window: *Window = switch (T) { Window => self.parent, Surface => self.parent.container.window() orelse return, diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 6737e98e2..5f5feca6e 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -410,7 +410,7 @@ pub const Window = struct { _: *gdk.Surface, monitor: *gdk.Monitor, apprt_window: *ApprtWindow, - ) callconv(.C) void { + ) callconv(.c) void { const window = apprt_window.window.as(gtk.Window); const size = apprt_window.config.quick_terminal_size; const position = apprt_window.config.quick_terminal_position; diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index e9c49048c..c29184020 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -166,7 +166,7 @@ fn beforeSend( event_val: sentry.c.sentry_value_t, _: ?*anyopaque, _: ?*anyopaque, -) callconv(.C) sentry.c.sentry_value_t { +) callconv(.c) sentry.c.sentry_value_t { // The native SDK at the time of writing doesn't support thread-local // scopes. The full SDK has one global scope. So we use the beforeSend // handler to set thread-specific data such as window size, grid size, @@ -237,7 +237,7 @@ fn beforeSend( } pub const Transport = struct { - pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.C) void { + pub fn send(envelope: *sentry.Envelope, ud: ?*anyopaque) callconv(.c) void { _ = ud; defer envelope.deinit(); diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig index 61a217929..7b92a8ba9 100644 --- a/src/os/flatpak.zig +++ b/src/os/flatpak.zig @@ -444,7 +444,7 @@ pub const FlatpakHostCommand = struct { _: [*c]const u8, params: ?*c.GVariant, ud: ?*anyopaque, - ) callconv(.C) void { + ) callconv(.c) void { const self = @as(*FlatpakHostCommand, @ptrCast(@alignCast(ud))); const state = state: { self.state_mutex.lock(); diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index e6f77216f..ddc94b1ec 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1524,7 +1524,7 @@ const CompletionBlock = objc.Block(struct { self: *Metal }, .{ fn bufferCompleted( block: *const CompletionBlock.Context, buffer_id: objc.c.id, -) callconv(.C) void { +) callconv(.c) void { const self = block.self; const buffer = objc.Object.fromId(buffer_id); diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 8c9b68447..45d86cbfe 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -250,7 +250,7 @@ fn spvCross( // It would be better to get this out into an output parameter to // show users but for now we can just log it. c.spvc_context_set_error_callback(ctx, @ptrCast(&(struct { - fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.C) void { + fn callback(_: ?*anyopaque, msg_ptr: [*c]const u8) callconv(.c) void { const msg = std.mem.sliceTo(msg_ptr, 0); std.log.warn("spirv-cross error message={s}", .{msg}); } From 362c5cb05ffc548398ee5beb6e1a5ecb24e6f665 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 7 May 2025 08:17:15 -0700 Subject: [PATCH 212/642] Allow struct/union/enum binding types to have default values This allows for `keybind = super+d=new_split` to now work (defaults to "auto"). This will also let us convert void types to union/enum/struct types in the future without breaking existing bindings. --- src/input/Binding.zig | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6bb50fc5d..6583e1462 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -568,6 +568,8 @@ pub const Action = union(enum) { left, up, auto, // splits along the larger direction + + pub const default: SplitDirection = .auto; }; pub const SplitFocusDirection = enum { @@ -729,7 +731,28 @@ pub const Action = union(enum) { Action.CursorKey => return Error.InvalidAction, else => { - const idx = colonIdx orelse return Error.InvalidFormat; + // Get the parameter after the colon. The parameter + // can be optional for action types that can have a + // "default" decl. + const idx = colonIdx orelse { + switch (@typeInfo(field.type)) { + .@"struct", + .@"union", + .@"enum", + => if (@hasDecl(field.type, "default")) { + return @unionInit( + Action, + field.name, + @field(field.type, "default"), + ); + }, + + else => {}, + } + + return Error.InvalidFormat; + }; + const param = input[idx + 1 ..]; return @unionInit( Action, @@ -2015,6 +2038,17 @@ test "parse: action with enum" { } } +test "parse: action with enum with default" { + const testing = std.testing; + + // parameter + { + const binding = try parseSingle("a=new_split"); + try testing.expect(binding.action == .new_split); + try testing.expectEqual(Action.SplitDirection.auto, binding.action.new_split); + } +} + test "parse: action with int" { const testing = std.testing; From 00a2d544204b169c5a1a6e221a265153d6687326 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 7 May 2025 10:36:31 -0500 Subject: [PATCH 213/642] gtk: only allow one config error dialog at a time This fixes a problem introduced by #7241 that would cause multiple error dialogs to be shown. --- src/apprt/gtk/App.zig | 3 ++ src/apprt/gtk/ConfigErrorsDialog.zig | 58 ++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index c9a973611..06cc41b9d 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -74,6 +74,9 @@ cursor_none: ?*gdk.Cursor, /// The clipboard confirmation window, if it is currently open. clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null, +/// The config errors dialog, if it is currently open. +config_errors_dialog: ?ConfigErrorsDialog = null, + /// The window containing the quick terminal. /// Null when never initialized. quick_terminal: ?*Window = null, diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig index a1a2a61af..c2de2f1dc 100644 --- a/src/apprt/gtk/ConfigErrorsDialog.zig +++ b/src/apprt/gtk/ConfigErrorsDialog.zig @@ -29,15 +29,39 @@ error_message: *gtk.TextBuffer, pub fn maybePresent(app: *App, window: ?*Window) void { if (app.config._diagnostics.empty()) return; - var builder = switch (DialogType) { - adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5), - adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2), - else => unreachable, - }; - defer builder.deinit(); + const config_errors_dialog = config_errors_dialog: { + if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog; - const dialog = builder.getObject(DialogType, "config_errors_dialog").?; - const error_message = builder.getObject(gtk.TextBuffer, "error_message").?; + var builder = switch (DialogType) { + adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5), + adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2), + else => unreachable, + }; + // defer builder.deinit(); + + const dialog = builder.getObject(DialogType, "config_errors_dialog").?; + const error_message = builder.getObject(gtk.TextBuffer, "error_message").?; + + _ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{}); + + app.config_errors_dialog = .{ + .builder = builder, + .dialog = dialog, + .error_message = error_message, + }; + + break :config_errors_dialog app.config_errors_dialog.?; + }; + + { + var start = std.mem.zeroes(gtk.TextIter); + config_errors_dialog.error_message.getStartIter(&start); + + var end = std.mem.zeroes(gtk.TextIter); + config_errors_dialog.error_message.getEndIter(&end); + + config_errors_dialog.error_message.delete(&start, &end); + } var msg_buf: [4095:0]u8 = undefined; var fbs = std.io.fixedBufferStream(&msg_buf); @@ -52,22 +76,24 @@ pub fn maybePresent(app: *App, window: ?*Window) void { continue; }; - error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos)); - error_message.insertAtCursor("\n", 1); + config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos)); + config_errors_dialog.error_message.insertAtCursor("\n", 1); } - _ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{}); - - const parent = if (window) |w| w.window.as(gtk.Widget) else null; - switch (DialogType) { - adw.AlertDialog => dialog.as(adw.Dialog).present(parent), - adw.MessageDialog => dialog.as(gtk.Window).present(), + adw.AlertDialog => { + const parent = if (window) |w| w.window.as(gtk.Widget) else null; + config_errors_dialog.dialog.as(adw.Dialog).present(parent); + }, + adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(), else => unreachable, } } fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void { + if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit(); + app.config_errors_dialog = null; + if (std.mem.orderZ(u8, response, "reload") == .eq) { app.reloadConfig(.app, .{}) catch |err| { log.warn("error reloading config error={}", .{err}); From 5d81a31a49cb3971c3e886d4a400b5db80476e23 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 7 May 2025 11:12:07 -0500 Subject: [PATCH 214/642] gtk: remove dead code --- src/apprt/gtk/ConfigErrorsDialog.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig index c2de2f1dc..ccc5599ad 100644 --- a/src/apprt/gtk/ConfigErrorsDialog.zig +++ b/src/apprt/gtk/ConfigErrorsDialog.zig @@ -37,7 +37,6 @@ pub fn maybePresent(app: *App, window: ?*Window) void { adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2), else => unreachable, }; - // defer builder.deinit(); const dialog = builder.getObject(DialogType, "config_errors_dialog").?; const error_message = builder.getObject(gtk.TextBuffer, "error_message").?; From 69a744b52153237dd31ad377d95cc2b645d41a85 Mon Sep 17 00:00:00 2001 From: tangowithfoxtrot <5676771+tangowithfoxtrot@users.noreply.github.com> Date: Wed, 7 May 2025 11:46:36 -0700 Subject: [PATCH 215/642] docs: fix minor grammatical error --- 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 95dcf3420..7850fd068 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2004,7 +2004,7 @@ keybind: Keybinds = .{}, /// macOS doesn't have a distinct "alt" key and instead has the "option" /// key which behaves slightly differently. On macOS by default, the -/// option key plus a character will sometimes produces a Unicode character. +/// option key plus a character will sometimes produce a Unicode character. /// For example, on US standard layouts option-b produces "∫". This may be /// undesirable if you want to use "option" as an "alt" key for keybindings /// in terminal programs or shells. From 9c70f8aee17ab68e4b9bd5a3bd5449dd4b2981ee Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 16 Jan 2025 16:08:35 -0600 Subject: [PATCH 216/642] core: add context menu key --- include/ghostty.h | 3 +++ src/input/key.zig | 4 +++- src/input/keycodes.zig | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index 18c547910..9409fa7c6 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -240,6 +240,9 @@ typedef enum { GHOSTTY_KEY_KP_DELETE, GHOSTTY_KEY_KP_BEGIN, + // special keys + GHOSTTY_KEY_CONTEXT_MENU, + // modifiers GHOSTTY_KEY_LEFT_SHIFT, GHOSTTY_KEY_LEFT_CONTROL, diff --git a/src/input/key.zig b/src/input/key.zig index ec65170f2..c0f80e294 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -401,7 +401,8 @@ pub const Key = enum(c_int) { kp_delete, kp_begin, - // TODO: media keys + // special keys + context_menu, // modifiers left_shift, @@ -579,6 +580,7 @@ pub const Key = enum(c_int) { .backspace => cimgui.c.ImGuiKey_Backspace, .print_screen => cimgui.c.ImGuiKey_PrintScreen, .pause => cimgui.c.ImGuiKey_Pause, + .context_menu => cimgui.c.ImGuiKey_Menu, .f1 => cimgui.c.ImGuiKey_F1, .f2 => cimgui.c.ImGuiKey_F2, diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig index 67ce46daf..e9adbc156 100644 --- a/src/input/keycodes.zig +++ b/src/input/keycodes.zig @@ -153,6 +153,7 @@ const code_to_key = code_to_key: { .{ "Numpad0", .kp_0 }, .{ "NumpadDecimal", .kp_decimal }, .{ "NumpadEqual", .kp_equal }, + .{ "ContextMenu", .context_menu }, .{ "ControlLeft", .left_control }, .{ "ShiftLeft", .left_shift }, .{ "AltLeft", .left_alt }, From a8b450f03dccf2d7beb5165db04243bbade6531b Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Wed, 7 May 2025 23:23:00 +0800 Subject: [PATCH 217/642] macOS: use file parent dir for `openTerminal` service cwd (#7286) --- .../Features/Services/ServiceProvider.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index bb95cb55a..d5a8c7d45 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -47,14 +47,29 @@ class ServiceProvider: NSObject { let terminalManager = delegate.terminalManager for path in paths { - // We only open in directories. - var isDirectory = ObjCBool(true) + // Check if the path exists and determine if it's a directory + var isDirectory = ObjCBool(false) guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { continue } - guard isDirectory.boolValue else { continue } + + let targetDirectoryPath: String + + if isDirectory.boolValue { + // Path is already a directory, use it directly + targetDirectoryPath = path + } else { + // Path is a file, get its parent directory + let parentDirectoryPath = (path as NSString).deletingLastPathComponent + var isParentPathDirectory = ObjCBool(true) + guard FileManager.default.fileExists(atPath: parentDirectoryPath, isDirectory: &isParentPathDirectory), + isParentPathDirectory.boolValue else { + continue + } + targetDirectoryPath = parentDirectoryPath + } // Build our config var config = Ghostty.SurfaceConfiguration() - config.workingDirectory = path + config.workingDirectory = targetDirectoryPath switch (target) { case .window: From 3043012c1b39fe36968dfa1cbe99fbaddeed469f Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Thu, 8 May 2025 08:33:24 +0800 Subject: [PATCH 218/642] macOS: simplify path handling in `openTerminal` --- .../Features/Services/ServiceProvider.swift | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index d5a8c7d45..7e45a7b99 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -47,29 +47,19 @@ class ServiceProvider: NSObject { let terminalManager = delegate.terminalManager for path in paths { - // Check if the path exists and determine if it's a directory - var isDirectory = ObjCBool(false) + // We only open in directories. + var isDirectory = ObjCBool(true) guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { continue } - let targetDirectoryPath: String - - if isDirectory.boolValue { - // Path is already a directory, use it directly - targetDirectoryPath = path - } else { - // Path is a file, get its parent directory - let parentDirectoryPath = (path as NSString).deletingLastPathComponent - var isParentPathDirectory = ObjCBool(true) - guard FileManager.default.fileExists(atPath: parentDirectoryPath, isDirectory: &isParentPathDirectory), - isParentPathDirectory.boolValue else { - continue - } - targetDirectoryPath = parentDirectoryPath + var workingDirectory = path + if !isDirectory.boolValue { + workingDirectory = (path as NSString).deletingLastPathComponent + guard FileManager.default.fileExists(atPath: workingDirectory, isDirectory: &isDirectory), isDirectory.boolValue else { continue } } // Build our config var config = Ghostty.SurfaceConfiguration() - config.workingDirectory = targetDirectoryPath + config.workingDirectory = workingDirectory switch (target) { case .window: From 800054874e9ee6012f84b2714ad9275161e8c555 Mon Sep 17 00:00:00 2001 From: Bryan Lee <38807139+liby@users.noreply.github.com> Date: Fri, 9 May 2025 10:48:58 +0800 Subject: [PATCH 219/642] macOS: switch to using URL instead of String --- .../Features/Services/ServiceProvider.swift | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index 7e45a7b99..a06e7d151 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -36,30 +36,27 @@ class ServiceProvider: NSObject { error.pointee = Self.errorNoString return } - let filePaths = objs.map { $0.path }.compactMap { $0 } + let urlObjects = objs.map { $0 as URL } - openTerminal(filePaths, target: target) + openTerminal(urlObjects, target: target) } - private func openTerminal(_ paths: [String], target: OpenTarget) { + private func openTerminal(_ urls: [URL], target: OpenTarget) { guard let delegateRaw = NSApp.delegate else { return } guard let delegate = delegateRaw as? AppDelegate else { return } let terminalManager = delegate.terminalManager - for path in paths { - // We only open in directories. - var isDirectory = ObjCBool(true) - guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory) else { continue } - - var workingDirectory = path - if !isDirectory.boolValue { - workingDirectory = (path as NSString).deletingLastPathComponent - guard FileManager.default.fileExists(atPath: workingDirectory, isDirectory: &isDirectory), isDirectory.boolValue else { continue } + let uniqueCwds: Set = Set( + urls.map { url -> URL in + // We only open in directories. + url.hasDirectoryPath ? url : url.deletingLastPathComponent() } + ) + for cwd in uniqueCwds { // Build our config var config = Ghostty.SurfaceConfiguration() - config.workingDirectory = workingDirectory + config.workingDirectory = cwd.path(percentEncoded: false) switch (target) { case .window: From 201ea050bd2812748353c8b43bcf9e37c62b3767 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 May 2025 08:27:11 -0700 Subject: [PATCH 220/642] update PACKAGING.md to be explicit about source vs. git Related to #7316 --- PACKAGING.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/PACKAGING.md b/PACKAGING.md index 234a86770..d85f55de7 100644 --- a/PACKAGING.md +++ b/PACKAGING.md @@ -4,13 +4,12 @@ Ghostty relies on downstream package maintainers to distribute Ghostty to end-users. This document provides guidance to package maintainers on how to package Ghostty for distribution. -> [!NOTE] +> [!IMPORTANT] > -> While Ghostty went through an extensive private beta testing period, -> packaging Ghostty is immature and may require additional build script -> tweaks and documentation improvement. I'm extremely motivated to work with -> package maintainers to improve the packaging process. Please open issues -> to discuss any packaging issues you encounter. +> This document is only accurate for the Ghostty source alongside it. +> **Do not use this document for older or newer versions of Ghostty!** If +> you are reading this document in a different version of Ghostty, please +> find the `PACKAGING.md` file alongside that version. ## Source Tarballs @@ -37,6 +36,19 @@ Use the `ghostty-source.tar.gz` asset and _not the GitHub auto-generated source tarball_. These tarballs are generated for every commit to the `main` branch and are not associated with a specific version. +> [!WARNING] +> +> Source tarballs are _not the same_ as a Git checkout. Source tarballs +> contain some preprocessed files that allow building Ghostty with less +> dependencies. If you are building Ghostty from a Git checkout, the +> steps below are the same but they may require additional dependencies +> not listed here. See the `README.md` for more information on building +> from a Git checkout. +> +> For everyone except Ghostty developers, please use the source tarballs. +> We generate tip source tarballs for users following the development +> branch. + ## Zig Version [Zig](https://ziglang.org) is required to build Ghostty. Prior to Zig 1.0, @@ -81,13 +93,6 @@ for system packages which separate a build and install step, since the install step can then be done with a `mv` or `cp` command (from `/tmp/ghostty` to wherever the package manager expects it). -> [!NOTE] -> -> **Version 1.1.1 and 1.1.2 are missing `fetch-zig-cache.sh`.** This was -> an oversight on the release process. You can use the script from version -> 1.1.0 to fetch the Zig cache for these versions. Future versions will -> restore the script. - ### Build Options Ghostty uses the Zig build system. You can see all available build options by From 91d15c89bc2f8533d1f852b388c96988368b09e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 08:21:31 -0700 Subject: [PATCH 221/642] input: key enum is now aligned with W3C keyboard codes --- src/input/key.zig | 376 +++++++++++++++++++++++++++++----------------- 1 file changed, 239 insertions(+), 137 deletions(-) diff --git a/src/input/key.zig b/src/input/key.zig index c0f80e294..a082134a7 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -255,95 +255,144 @@ pub const Action = enum(c_int) { repeat, }; -/// The set of keys that can map to keybindings. These have no fixed enum -/// values because we map platform-specific keys to this set. Note that -/// this only needs to accommodate what maps to a key. If a key is not bound -/// to anything and the key can be mapped to a printable character, then that -/// unicode character is sent directly to the pty. +/// The set of key codes that Ghostty is aware of. These represent +/// physical keys on the keyboard. The logical key (or key string) +/// is the string that is generated by the key event and that is up +/// to the apprt to provide. /// -/// This is backed by a c_int so we can use this as-is for our embedding API. +/// Note that these are layout-independent. For example, the "a" +/// key on a US keyboard is the same as the "ф" key on a Russian +/// keyboard, but both will report the "a" enum value in the key +/// event. These values are based on the W3C standard. See: +/// https://www.w3.org/TR/uievents-code +/// +/// Layout-dependent strings are provided in the KeyEvent struct as +/// UTF-8 and are produced by the associated apprt. Ghostty core has +/// no mechanism to map input events to strings without the apprt. /// /// IMPORTANT: Any changes here update include/ghostty.h pub const Key = enum(c_int) { - invalid, + unidentified, - // a-z - a, - b, - c, - d, - e, - f, - g, - h, - i, - j, - k, - l, - m, - n, - o, - p, - q, - r, - s, - t, - u, - v, - w, - x, - y, - z, - - // numbers - zero, - one, - two, - three, - four, - five, - six, - seven, - eight, - nine, - - // punctuation - semicolon, - space, - apostrophe, + // "Writing System Keys" § 3.1.1 + backquote, + backslash, + bracket_left, + bracket_right, comma, - grave_accent, // ` - period, - slash, - minus, - plus, + digit_0, + digit_1, + digit_2, + digit_3, + digit_4, + digit_5, + digit_6, + digit_7, + digit_8, + digit_9, equal, - left_bracket, // [ - right_bracket, // ] - backslash, // \ + intl_backslash, + intl_ro, + intl_yen, + key_a, + key_b, + key_c, + key_d, + key_e, + key_f, + key_g, + key_h, + key_i, + key_j, + key_k, + key_l, + key_m, + key_n, + key_o, + key_p, + key_q, + key_r, + key_s, + key_t, + key_u, + key_v, + key_w, + key_x, + key_y, + key_z, + minus, + period, + quote, + semicolon, + slash, - // control - up, - down, - right, - left, - home, - end, - insert, - delete, - caps_lock, - scroll_lock, - num_lock, - page_up, - page_down, - escape, - enter, - tab, + // "Functional Keys" § 3.1.2 + alt_left, + alt_right, backspace, - print_screen, - pause, + caps_lock, + context_menu, + control_left, + control_right, + enter, + meta_left, + meta_right, + shift_left, + shift_right, + space, + tab, + convert, + kana_mode, + non_convert, - // function keys + // "Control Pad Section" § 3.2 + delete, + end, + help, + home, + insert, + page_down, + page_up, + + // "Arrow Pad Section" § 3.3 + arrow_down, + arrow_left, + arrow_right, + arrow_up, + + // "Numpad Section" § 3.4 + num_lock, + numpad_0, + numpad_1, + numpad_2, + numpad_3, + numpad_4, + numpad_5, + numpad_6, + numpad_7, + numpad_8, + numpad_9, + numpad_add, + numpad_backspace, + numpad_clear, + numpad_clear_entry, + numpad_comma, + numpad_decimal, + numpad_divide, + numpad_enter, + numpad_equal, + numpad_memory_add, + numpad_memory_clear, + numpad_memory_recall, + numpad_memory_store, + numpad_memory_subtract, + numpad_multiply, + numpad_paren_left, + numpad_paren_right, + numpad_subtract, + + // "Function Section" § 3.5 + escape, f1, f2, f3, @@ -356,66 +405,119 @@ pub const Key = enum(c_int) { f10, f11, f12, - f13, - f14, - f15, - f16, - f17, - f18, - f19, - f20, - f21, - f22, - f23, - f24, - f25, + @"fn", + fn_lock, + print_screen, + scroll_lock, + pause, - // keypad - kp_0, - kp_1, - kp_2, - kp_3, - kp_4, - kp_5, - kp_6, - kp_7, - kp_8, - kp_9, - kp_decimal, - kp_divide, - kp_multiply, - kp_subtract, - kp_add, - kp_enter, - kp_equal, - kp_separator, - kp_left, - kp_right, - kp_up, - kp_down, - kp_page_up, - kp_page_down, - kp_home, - kp_end, - kp_insert, - kp_delete, - kp_begin, + // "Media Keys" § 3.6 + browser_back, + browser_favorites, + browser_forward, + browser_home, + browser_refresh, + browser_search, + browser_stop, + eject, + launch_app_1, + launch_app_2, + launch_mail, + media_play_pause, + media_select, + media_stop, + media_track_next, + media_track_previous, + power, + sleep, + audio_volume_down, + audio_volume_mute, + audio_volume_up, + wake_up, - // special keys - context_menu, - - // modifiers - left_shift, - left_control, - left_alt, - left_super, - right_shift, - right_control, - right_alt, - right_super, - - // To support more keys (there are obviously more!) add them here - // and ensure the mapping is up to date in the Window key handler. + // Backwards compatibility for Ghostty 1.1.x and earlier, we don't + // want to force people to rewrite their configs. + pub const a = .key_a; + pub const b = .key_b; + pub const c = .key_c; + pub const d = .key_d; + pub const e = .key_e; + pub const f = .key_f; + pub const g = .key_g; + pub const h = .key_h; + pub const i = .key_i; + pub const j = .key_j; + pub const k = .key_k; + pub const l = .key_l; + pub const m = .key_m; + pub const n = .key_n; + pub const o = .key_o; + pub const p = .key_p; + pub const q = .key_q; + pub const r = .key_r; + pub const s = .key_s; + pub const t = .key_t; + pub const u = .key_u; + pub const v = .key_v; + pub const w = .key_w; + pub const x = .key_x; + pub const y = .key_y; + pub const z = .key_z; + pub const zero = .digit_0; + pub const one = .digit_1; + pub const two = .digit_2; + pub const three = .digit_3; + pub const four = .digit_4; + pub const five = .digit_5; + pub const six = .digit_6; + pub const seven = .digit_7; + pub const eight = .digit_8; + pub const nine = .digit_9; + pub const apostrophe = .quote; + pub const grave_accent = .backquote; + pub const left_bracket = .bracket_left; + pub const right_bracket = .bracket_right; + pub const up = .arrow_up; + pub const down = .arrow_down; + pub const left = .arrow_left; + pub const right = .arrow_right; + pub const kp_0 = .numpad_0; + pub const kp_1 = .numpad_1; + pub const kp_2 = .numpad_2; + pub const kp_3 = .numpad_3; + pub const kp_4 = .numpad_4; + pub const kp_5 = .numpad_5; + pub const kp_6 = .numpad_6; + pub const kp_7 = .numpad_7; + pub const kp_8 = .numpad_8; + pub const kp_9 = .numpad_9; + pub const kp_decimal = .numpad_decimal; + pub const kp_divide = .numpad_divide; + pub const kp_multiply = .numpad_multiply; + pub const kp_subtract = .numpad_subtract; + pub const kp_add = .numpad_add; + pub const kp_enter = .numpad_enter; + pub const kp_equal = .numpad_equal; + pub const kp_separator = .numpad_separator; + pub const kp_left = .numpad_left; + pub const kp_right = .numpad_right; + pub const kp_up = .numpad_up; + pub const kp_down = .numpad_down; + pub const kp_page_up = .numpad_page_up; + pub const kp_page_down = .numpad_page_down; + pub const kp_home = .numpad_home; + pub const kp_end = .numpad_end; + pub const kp_insert = .numpad_insert; + pub const kp_delete = .numpad_delete; + pub const kp_begin = .numpad_begin; + pub const left_shift = .shift_left; + pub const right_shift = .shift_right; + pub const left_control = .control_left; + pub const right_control = .control_right; + pub const left_alt = .alt_left; + pub const right_alt = .alt_right; + pub const left_super = .meta_left; + pub const right_super = .meta_right; /// Converts an ASCII character to a key, if possible. This returns /// null if the character is unknown. From a3462dd2bd541af6372855917c6ccb5643aeda93 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 08:47:21 -0700 Subject: [PATCH 222/642] input: remove translated --- NOTES.md | 3 + src/config/Config.zig | 222 +++++++++++++------------- src/input/Binding.zig | 253 +++++++++++------------------- src/input/KeyEncoder.zig | 106 ++++++------- src/input/function_keys.zig | 62 ++++---- src/input/key.zig | 301 ++++++++++++++++++------------------ src/input/kitty.zig | 72 ++++----- src/inspector/key.zig | 6 +- src/surface_mouse.zig | 26 ++-- 9 files changed, 481 insertions(+), 570 deletions(-) create mode 100644 NOTES.md diff --git a/NOTES.md b/NOTES.md new file mode 100644 index 000000000..8e4937bd4 --- /dev/null +++ b/NOTES.md @@ -0,0 +1,3 @@ +- key backwards compatibility, e.g. `grave_accent` +- `physical:` backwards compatibility? + diff --git a/src/config/Config.zig b/src/config/Config.zig index 7850fd068..7d2814136 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4343,12 +4343,12 @@ pub const Keybinds = struct { // keybinds for opening and reloading config try self.set.put( alloc, - .{ .key = .{ .translated = .comma }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .{ .unicode = ',' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .reload_config = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .comma }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = ',' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .open_config = {} }, ); @@ -4362,12 +4362,12 @@ pub const Keybinds = struct { if (!builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, - .{ .key = .{ .translated = .insert }, .mods = .{ .ctrl = true } }, + .{ .key = .{ .physical = .insert }, .mods = .{ .ctrl = true } }, .{ .copy_to_clipboard = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .insert }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .insert }, .mods = .{ .shift = true } }, .{ .paste_from_clipboard = {} }, ); } @@ -4381,12 +4381,12 @@ pub const Keybinds = struct { try self.set.put( alloc, - .{ .key = .{ .translated = .c }, .mods = mods }, + .{ .key = .{ .unicode = 'c' }, .mods = mods }, .{ .copy_to_clipboard = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .v }, .mods = mods }, + .{ .key = .{ .unicode = 'v' }, .mods = mods }, .{ .paste_from_clipboard = {} }, ); } @@ -4397,84 +4397,84 @@ pub const Keybinds = struct { // set the expected keybind for the menu. try self.set.put( alloc, - .{ .key = .{ .translated = .plus }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = '+' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .physical = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .minus }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .physical = .minus }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .decrease_font_size = 1 }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .zero }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .physical = .digit_0 }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .reset_font_size = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .write_screen_file = .paste }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .j }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true, .alt = true }) }, + .{ .key = .{ .unicode = 'j' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true, .alt = true }) }, .{ .write_screen_file = .open }, ); // Expand Selection try self.set.putFlags( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .shift = true } }, .{ .adjust_selection = .left }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .shift = true } }, .{ .adjust_selection = .right }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .shift = true } }, .{ .adjust_selection = .up }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .shift = true } }, .{ .adjust_selection = .down }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .page_up }, .mods = .{ .shift = true } }, .{ .adjust_selection = .page_up }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .page_down }, .mods = .{ .shift = true } }, .{ .adjust_selection = .page_down }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .home }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .home }, .mods = .{ .shift = true } }, .{ .adjust_selection = .home }, .{ .performable = true }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .end }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .end }, .mods = .{ .shift = true } }, .{ .adjust_selection = .end }, .{ .performable = true }, ); @@ -4482,12 +4482,12 @@ pub const Keybinds = struct { // Tabs common to all platforms try self.set.put( alloc, - .{ .key = .{ .translated = .tab }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .tab }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .tab }, .mods = .{ .ctrl = true } }, + .{ .key = .{ .physical = .tab }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, ); @@ -4495,174 +4495,174 @@ pub const Keybinds = struct { if (comptime !builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, - .{ .key = .{ .translated = .n }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .n }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_window = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .w }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .w }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_surface = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .q }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .q }, .mods = .{ .ctrl = true, .shift = true } }, .{ .quit = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .f4 }, .mods = .{ .alt = true } }, + .{ .key = .{ .physical = .f4 }, .mods = .{ .alt = true } }, .{ .close_window = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .t }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .t }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .w }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .w }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .next_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_up }, .mods = .{ .ctrl = true } }, + .{ .key = .{ .physical = .page_up }, .mods = .{ .ctrl = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_down }, .mods = .{ .ctrl = true } }, + .{ .key = .{ .physical = .page_down }, .mods = .{ .ctrl = true } }, .{ .next_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .o }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .o }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .right }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .e }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .e }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .down }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left_bracket }, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .physical = .bracket_left }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .previous }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right_bracket }, .mods = .{ .ctrl = true, .super = true } }, + .{ .key = .{ .physical = .bracket_right }, .mods = .{ .ctrl = true, .super = true } }, .{ .goto_split = .next }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .up }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .down }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .left }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .right }, ); // Resizing splits try self.set.put( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .up, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .down, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .left, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .right, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .plus }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .plus }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .equalize_splits = {} }, ); // Viewport scrolling try self.set.put( alloc, - .{ .key = .{ .translated = .home }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .home }, .mods = .{ .shift = true } }, .{ .scroll_to_top = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .end }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .end }, .mods = .{ .shift = true } }, .{ .scroll_to_bottom = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .page_up }, .mods = .{ .shift = true } }, .{ .scroll_page_up = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .page_down }, .mods = .{ .shift = true } }, .{ .scroll_page_down = {} }, ); // Semantic prompts try self.set.put( alloc, - .{ .key = .{ .translated = .page_up }, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .physical = .page_up }, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = -1 }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_down }, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .physical = .page_down }, .mods = .{ .shift = true, .ctrl = true } }, .{ .jump_to_prompt = 1 }, ); // Inspector, matching Chromium try self.set.put( alloc, - .{ .key = .{ .translated = .i }, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .physical = .i }, .mods = .{ .shift = true, .ctrl = true } }, .{ .inspector = .toggle }, ); // Terminal try self.set.put( alloc, - .{ .key = .{ .translated = .a }, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .physical = .a }, .mods = .{ .shift = true, .ctrl = true } }, .{ .select_all = {} }, ); // Selection clipboard paste try self.set.put( alloc, - .{ .key = .{ .translated = .insert }, .mods = .{ .shift = true } }, + .{ .key = .{ .physical = .insert }, .mods = .{ .shift = true } }, .{ .paste_from_selection = {} }, ); } @@ -4675,23 +4675,14 @@ pub const Keybinds = struct { .{ .alt = true }; // Cmd+N for goto tab N - const start = @intFromEnum(inputpkg.Key.one); - const end = @intFromEnum(inputpkg.Key.eight); - var i: usize = start; + const start: u21 = '1'; + const end: u21 = '8'; + var i: u21 = start; while (i <= end) : (i += 1) { try self.set.put( alloc, .{ - // On macOS, we use the physical key for tab changing so - // that this works across all keyboard layouts. This may - // want to be true on other platforms as well but this - // is definitely true on macOS so we just do it here for - // now (#817) - .key = if (comptime builtin.target.os.tag.isDarwin()) - .{ .physical = @enumFromInt(i) } - else - .{ .translated = @enumFromInt(i) }, - + .key = .{ .unicode = i }, .mods = mods, }, .{ .goto_tab = (i - start) + 1 }, @@ -4700,10 +4691,7 @@ pub const Keybinds = struct { try self.set.put( alloc, .{ - .key = if (comptime builtin.target.os.tag.isDarwin()) - .{ .physical = .nine } - else - .{ .translated = .nine }, + .key = .{ .unicode = '9' }, .mods = mods, }, .{ .last_tab = {} }, @@ -4713,14 +4701,14 @@ pub const Keybinds = struct { // Toggle fullscreen try self.set.put( alloc, - .{ .key = .{ .translated = .enter }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .physical = .enter }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .toggle_fullscreen = {} }, ); // Toggle zoom a split try self.set.put( alloc, - .{ .key = .{ .translated = .enter }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .{ .key = .{ .physical = .enter }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, .{ .toggle_split_zoom = {} }, ); @@ -4728,199 +4716,199 @@ pub const Keybinds = struct { if (comptime builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, - .{ .key = .{ .translated = .q }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 'q' }, .mods = .{ .super = true } }, .{ .quit = {} }, ); try self.set.putFlags( alloc, - .{ .key = .{ .translated = .k }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } }, .{ .clear_screen = {} }, .{ .performable = true }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .a }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 'a' }, .mods = .{ .super = true } }, .{ .select_all = {} }, ); // Viewport scrolling try self.set.put( alloc, - .{ .key = .{ .translated = .home }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .home }, .mods = .{ .super = true } }, .{ .scroll_to_top = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .end }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .end }, .mods = .{ .super = true } }, .{ .scroll_to_bottom = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_up }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .page_up }, .mods = .{ .super = true } }, .{ .scroll_page_up = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .page_down }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .page_down }, .mods = .{ .super = true } }, .{ .scroll_page_down = {} }, ); // Semantic prompts try self.set.put( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .shift = true } }, .{ .jump_to_prompt = -1 }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .shift = true } }, .{ .jump_to_prompt = 1 }, ); // Mac windowing try self.set.put( alloc, - .{ .key = .{ .translated = .n }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 'n' }, .mods = .{ .super = true } }, .{ .new_window = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .w }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true } }, .{ .close_surface = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .w }, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } }, .{ .close_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .shift = true } }, .{ .close_window = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .w }, .mods = .{ .super = true, .shift = true, .alt = true } }, + .{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .shift = true, .alt = true } }, .{ .close_all_windows = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .t }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 't' }, .mods = .{ .super = true } }, .{ .new_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left_bracket }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .physical = .bracket_left }, .mods = .{ .super = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .physical = .bracket_right }, .mods = .{ .super = true, .shift = true } }, .{ .next_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .d }, .mods = .{ .super = true } }, + .{ .key = .{ .unicode = 'd' }, .mods = .{ .super = true } }, .{ .new_split = .right }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .d }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = 'd' }, .mods = .{ .super = true, .shift = true } }, .{ .new_split = .down }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left_bracket }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .bracket_left }, .mods = .{ .super = true } }, .{ .goto_split = .previous }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .bracket_right }, .mods = .{ .super = true } }, .{ .goto_split = .next }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .up }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .down }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .left }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .super = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .alt = true } }, .{ .goto_split = .right }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .up, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .down, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .left, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true } }, .{ .resize_split = .{ .right, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .equal }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .physical = .equal }, .mods = .{ .super = true, .ctrl = true } }, .{ .equalize_splits = {} }, ); // Jump to prompt, matches Terminal.app try self.set.put( alloc, - .{ .key = .{ .translated = .up }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true } }, .{ .jump_to_prompt = -1 }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .down }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true } }, .{ .jump_to_prompt = 1 }, ); // Toggle command palette, matches VSCode try self.set.put( alloc, - .{ .key = .{ .translated = .p }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = 'p' }, .mods = .{ .super = true, .shift = true } }, .{ .toggle_command_palette = {} }, ); // Inspector, matching Chromium try self.set.put( alloc, - .{ .key = .{ .translated = .i }, .mods = .{ .alt = true, .super = true } }, + .{ .key = .{ .unicode = 'i' }, .mods = .{ .alt = true, .super = true } }, .{ .inspector = .toggle }, ); // Alternate keybind, common to Mac programs try self.set.put( alloc, - .{ .key = .{ .translated = .f }, .mods = .{ .super = true, .ctrl = true } }, + .{ .key = .{ .unicode = 'f' }, .mods = .{ .super = true, .ctrl = true } }, .{ .toggle_fullscreen = {} }, ); // Selection clipboard paste, matches Terminal.app try self.set.put( alloc, - .{ .key = .{ .translated = .v }, .mods = .{ .super = true, .shift = true } }, + .{ .key = .{ .unicode = 'v' }, .mods = .{ .super = true, .shift = true } }, .{ .paste_from_selection = {} }, ); @@ -4931,27 +4919,27 @@ pub const Keybinds = struct { // the keybinds to `unbind`. try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true } }, .{ .text = "\\x05" }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true } }, .{ .text = "\\x01" }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .backspace }, .mods = .{ .super = true } }, + .{ .key = .{ .physical = .backspace }, .mods = .{ .super = true } }, .{ .text = "\\x15" }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .left }, .mods = .{ .alt = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .alt = true } }, .{ .esc = "b" }, ); try self.set.put( alloc, - .{ .key = .{ .translated = .right }, .mods = .{ .alt = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .alt = true } }, .{ .esc = "f" }, ); } @@ -5138,8 +5126,8 @@ pub const Keybinds = struct { // Note they turn into translated keys because they match // their ASCII mapping. const want = - \\keybind = ctrl+z>two=goto_tab:2 - \\keybind = ctrl+z>one=goto_tab:1 + \\keybind = ctrl+z>2=goto_tab:2 + \\keybind = ctrl+z>1=goto_tab:1 \\ ; try std.testing.expectEqualStrings(want, buf.items); @@ -5163,8 +5151,8 @@ pub const Keybinds = struct { // NB: This does not currently retain the order of the keybinds. const want = - \\a = ctrl+a>ctrl+b>w=close_window \\a = ctrl+a>ctrl+b>n=new_window + \\a = ctrl+a>ctrl+b>w=close_window \\a = ctrl+a>ctrl+c>t=new_tab \\a = ctrl+b>ctrl+d>a=previous_tab \\ diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 6583e1462..30575bc30 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -99,9 +99,12 @@ pub const Parser = struct { if (flags.performable) return Error.InvalidFormat; flags.performable = true; } else { - // If we don't recognize the prefix then we're done. - // There are trigger-specific prefixes like "physical:" so - // this lets us fall into that. + // If we don't recognize the prefix then we're done. We + // let any unknown prefix fallthrough to trigger-specific + // parsing in case there are trigger-specific prefixes + // (none currently but historically there was `physical:` + // at one point). Breaking here lets us always implement new + // prefixes. break; } @@ -202,14 +205,12 @@ pub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool { const lhs_key: c_int = blk: { switch (lhs.trigger.key) { - .translated => break :blk @intFromEnum(lhs.trigger.key.translated), .physical => break :blk @intFromEnum(lhs.trigger.key.physical), .unicode => break :blk @intCast(lhs.trigger.key.unicode), } }; const rhs_key: c_int = blk: { switch (rhs.trigger.key) { - .translated => break :blk @intFromEnum(rhs.trigger.key.translated), .physical => break :blk @intFromEnum(rhs.trigger.key.physical), .unicode => break :blk @intCast(rhs.trigger.key.unicode), } @@ -1065,18 +1066,12 @@ pub const Action = union(enum) { /// This must be kept in sync with include/ghostty.h ghostty_input_trigger_s pub const Trigger = struct { /// The key that has to be pressed for a binding to take action. - key: Trigger.Key = .{ .translated = .invalid }, + key: Trigger.Key = .{ .physical = .unidentified }, /// The key modifiers that must be active for this to match. mods: key.Mods = .{}, pub const Key = union(C.Tag) { - /// key is the translated version of a key. This is the key that - /// a logical keyboard layout at the OS level would translate the - /// physical key to. For example if you use a US hardware keyboard - /// but have a Dvorak layout, the key would be the Dvorak key. - translated: key.Key, - /// key is the "physical" version. This is the same as mapped for /// standard US keyboard layouts. For non-US keyboard layouts, this /// is used to bind to a physical key location rather than a translated @@ -1091,18 +1086,16 @@ pub const Trigger = struct { /// The extern struct used for triggers in the C API. pub const C = extern struct { - tag: Tag = .translated, - key: C.Key = .{ .translated = .invalid }, + tag: Tag = .physical, + key: C.Key = .{ .physical = .unidentified }, mods: key.Mods = .{}, pub const Tag = enum(c_int) { - translated, physical, unicode, }; pub const Key = extern union { - translated: key.Key, physical: key.Key, unicode: u32, }; @@ -1150,24 +1143,16 @@ pub const Trigger = struct { } } - // If the key starts with "physical" then this is an physical key. - const physical_prefix = "physical:"; - const physical = std.mem.startsWith(u8, part, physical_prefix); - const key_part = if (physical) part[physical_prefix.len..] else part; - // Check if its a key const keysInfo = @typeInfo(key.Key).@"enum"; inline for (keysInfo.fields) |field| { - if (!std.mem.eql(u8, field.name, "invalid")) { - if (std.mem.eql(u8, key_part, field.name)) { + if (!std.mem.eql(u8, field.name, "unidentified")) { + if (std.mem.eql(u8, part, field.name)) { // Repeat not allowed if (!result.isKeyUnset()) return Error.InvalidFormat; const keyval = @field(key.Key, field.name); - result.key = if (physical) - .{ .physical = keyval } - else - .{ .translated = keyval }; + result.key = .{ .physical = keyval }; continue :loop; } } @@ -1177,21 +1162,13 @@ pub const Trigger = struct { // character then we can use that as a key. if (result.isKeyUnset()) unicode: { // Invalid UTF8 drops to invalid format - const view = std.unicode.Utf8View.init(key_part) catch break :unicode; + const view = std.unicode.Utf8View.init(part) catch break :unicode; var it = view.iterator(); // No codepoints or multiple codepoints drops to invalid format const cp = it.nextCodepoint() orelse break :unicode; if (it.nextCodepoint() != null) break :unicode; - // If this is ASCII and we have a translated key, set that. - if (std.math.cast(u8, cp)) |ascii| { - if (key.Key.fromASCII(ascii)) |k| { - result.key = .{ .translated = k }; - continue :loop; - } - } - result.key = .{ .unicode = cp }; continue :loop; } @@ -1205,7 +1182,7 @@ pub const Trigger = struct { /// Returns true if this trigger has no key set. pub fn isKeyUnset(self: Trigger) bool { return switch (self.key) { - .translated => |v| v == .invalid, + .physical => |v| v == .unidentified, else => false, }; } @@ -1228,7 +1205,6 @@ pub const Trigger = struct { return .{ .tag = self.key, .key = switch (self.key) { - .translated => |v| .{ .translated = v }, .physical => |v| .{ .physical = v }, .unicode => |v| .{ .unicode = @intCast(v) }, }, @@ -1254,8 +1230,7 @@ pub const Trigger = struct { // Key switch (self.key) { - .translated => |k| try writer.print("{s}", .{@tagName(k)}), - .physical => |k| try writer.print("physical:{s}", .{@tagName(k)}), + .physical => |k| try writer.print("{s}", .{@tagName(k)}), .unicode => |c| try writer.print("{u}", .{c}), } } @@ -1620,13 +1595,10 @@ pub const Set = struct { pub fn getEvent(self: *const Set, event: KeyEvent) ?Entry { var trigger: Trigger = .{ .mods = event.mods.binding(), - .key = .{ .translated = event.key }, + .key = .{ .physical = event.physical_key }, }; if (self.get(trigger)) |v| return v; - trigger.key = .{ .physical = event.physical_key }; - if (self.get(trigger)) |v| return v; - if (event.unshifted_codepoint > 0) { trigger.key = .{ .unicode = event.unshifted_codepoint }; if (self.get(trigger)) |v| return v; @@ -1637,19 +1609,7 @@ pub const Set = struct { /// Remove a binding for a given trigger. pub fn remove(self: *Set, alloc: Allocator, t: Trigger) void { - // Remove whatever this trigger is self.removeExact(alloc, t); - - // If we have a physical we remove translated and vice versa. - const alternate: Trigger.Key = switch (t.key) { - .unicode => return, - .translated => |k| .{ .physical = k }, - .physical => |k| .{ .translated = k }, - }; - - var alt_t: Trigger = t; - alt_t.key = alternate; - self.removeExact(alloc, alt_t); } fn removeExact(self: *Set, alloc: Allocator, t: Trigger) void { @@ -1750,37 +1710,24 @@ test "parse: triggers" { // single character try testing.expectEqual( Binding{ - .trigger = .{ .key = .{ .translated = .a } }, + .trigger = .{ .key = .{ .unicode = 'a' } }, .action = .{ .ignore = {} }, }, try parseSingle("a=ignore"), ); - // unicode keys that map to translated - try testing.expectEqual(Binding{ - .trigger = .{ .key = .{ .translated = .one } }, - .action = .{ .ignore = {} }, - }, try parseSingle("1=ignore")); - try testing.expectEqual(Binding{ - .trigger = .{ - .mods = .{ .super = true }, - .key = .{ .translated = .period }, - }, - .action = .{ .ignore = {} }, - }, try parseSingle("cmd+.=ignore")); - // single modifier try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("shift+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .ctrl = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("ctrl+a=ignore")); @@ -1789,7 +1736,7 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true, .ctrl = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("shift+ctrl+a=ignore")); @@ -1798,7 +1745,7 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("a+shift=ignore")); @@ -1807,10 +1754,10 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .physical = .a }, + .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, - }, try parseSingle("shift+physical:a=ignore")); + }, try parseSingle("shift+key_a=ignore")); // unicode keys try testing.expectEqual(Binding{ @@ -1825,7 +1772,7 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .consumed = false }, @@ -1835,17 +1782,17 @@ test "parse: triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .physical = .a }, + .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, .flags = .{ .consumed = false }, - }, try parseSingle("unconsumed:physical:a+shift=ignore")); + }, try parseSingle("unconsumed:key_a+shift=ignore")); // performable keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .performable = true }, @@ -1868,7 +1815,7 @@ test "parse: global triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .global = true }, @@ -1878,17 +1825,17 @@ test "parse: global triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .physical = .a }, + .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, .flags = .{ .global = true }, - }, try parseSingle("global:physical:a+shift=ignore")); + }, try parseSingle("global:key_a+shift=ignore")); // global unconsumed keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ @@ -1911,7 +1858,7 @@ test "parse: all triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ .all = true }, @@ -1921,17 +1868,17 @@ test "parse: all triggers" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .physical = .a }, + .key = .{ .physical = .key_a }, }, .action = .{ .ignore = {} }, .flags = .{ .all = true }, - }, try parseSingle("all:physical:a+shift=ignore")); + }, try parseSingle("all:key_a+shift=ignore")); // all unconsumed keys try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .shift = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, .flags = .{ @@ -1953,14 +1900,14 @@ test "parse: modifier aliases" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .super = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("cmd+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .super = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("command+a=ignore")); @@ -1968,14 +1915,14 @@ test "parse: modifier aliases" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .alt = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("opt+a=ignore")); try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .alt = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("option+a=ignore")); @@ -1983,7 +1930,7 @@ test "parse: modifier aliases" { try testing.expectEqual(Binding{ .trigger = .{ .mods = .{ .ctrl = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, }, try parseSingle("control+a=ignore")); @@ -2002,7 +1949,7 @@ test "parse: action no parameters" { // no parameters try testing.expectEqual( Binding{ - .trigger = .{ .key = .{ .translated = .a } }, + .trigger = .{ .key = .{ .unicode = 'a' } }, .action = .{ .ignore = {} }, }, try parseSingle("a=ignore"), @@ -2108,15 +2055,15 @@ test "sequence iterator" { // single character { var it: SequenceIterator = .{ .input = "a" }; - try testing.expectEqual(Trigger{ .key = .{ .translated = .a } }, (try it.next()).?); + try testing.expectEqual(Trigger{ .key = .{ .unicode = 'a' } }, (try it.next()).?); try testing.expect(try it.next() == null); } // multi character { var it: SequenceIterator = .{ .input = "a>b" }; - try testing.expectEqual(Trigger{ .key = .{ .translated = .a } }, (try it.next()).?); - try testing.expectEqual(Trigger{ .key = .{ .translated = .b } }, (try it.next()).?); + try testing.expectEqual(Trigger{ .key = .{ .unicode = 'a' } }, (try it.next()).?); + try testing.expectEqual(Trigger{ .key = .{ .unicode = 'b' } }, (try it.next()).?); try testing.expect(try it.next() == null); } @@ -2135,7 +2082,7 @@ test "sequence iterator" { // empty ending sequence { var it: SequenceIterator = .{ .input = "a>" }; - try testing.expectEqual(Trigger{ .key = .{ .translated = .a } }, (try it.next()).?); + try testing.expectEqual(Trigger{ .key = .{ .unicode = 'a' } }, (try it.next()).?); try testing.expectError(Error.InvalidFormat, it.next()); } } @@ -2149,7 +2096,7 @@ test "parse: sequences" { try testing.expectEqual(Parser.Elem{ .binding = .{ .trigger = .{ .mods = .{ .ctrl = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .action = .{ .ignore = {} }, } }, (try p.next()).?); @@ -2160,11 +2107,11 @@ test "parse: sequences" { { var p = try Parser.init("a>b=ignore"); try testing.expectEqual(Parser.Elem{ .leader = .{ - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, } }, (try p.next()).?); try testing.expectEqual(Parser.Elem{ .binding = .{ .trigger = .{ - .key = .{ .translated = .b }, + .key = .{ .unicode = 'b' }, }, .action = .{ .ignore = {} }, } }, (try p.next()).?); @@ -2183,7 +2130,7 @@ test "set: parseAndPut typical binding" { // Creates forward mapping { - const action = s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf; + const action = s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); try testing.expectEqual(Flags{}, action.flags); } @@ -2191,7 +2138,7 @@ test "set: parseAndPut typical binding" { // Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } } @@ -2206,7 +2153,7 @@ test "set: parseAndPut unconsumed binding" { // Creates forward mapping { - const trigger: Trigger = .{ .key = .{ .translated = .a } }; + const trigger: Trigger = .{ .key = .{ .unicode = 'a' } }; const action = s.get(trigger).?.value_ptr.*.leaf; try testing.expect(action.action == .new_window); try testing.expectEqual(Flags{ .consumed = false }, action.flags); @@ -2215,7 +2162,7 @@ test "set: parseAndPut unconsumed binding" { // Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } } @@ -2231,25 +2178,7 @@ test "set: parseAndPut removed binding" { // Creates forward mapping { - const trigger: Trigger = .{ .key = .{ .translated = .a } }; - try testing.expect(s.get(trigger) == null); - } - try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); -} - -test "set: parseAndPut removed physical binding" { - const testing = std.testing; - const alloc = testing.allocator; - - var s: Set = .{}; - defer s.deinit(alloc); - - try s.parseAndPut(alloc, "physical:a=new_window"); - try s.parseAndPut(alloc, "a=unbind"); - - // Creates forward mapping - { - const trigger: Trigger = .{ .key = .{ .physical = .a } }; + const trigger: Trigger = .{ .key = .{ .unicode = 'a' } }; try testing.expect(s.get(trigger) == null); } try testing.expect(s.getTrigger(.{ .new_window = {} }) == null); @@ -2265,13 +2194,13 @@ test "set: parseAndPut sequence" { try s.parseAndPut(alloc, "a>b=new_window"); var current: *Set = &s; { - const t: Trigger = .{ .key = .{ .translated = .a } }; + const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { - const t: Trigger = .{ .key = .{ .translated = .b } }; + const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); @@ -2290,20 +2219,20 @@ test "set: parseAndPut sequence with two actions" { try s.parseAndPut(alloc, "a>c=new_tab"); var current: *Set = &s; { - const t: Trigger = .{ .key = .{ .translated = .a } }; + const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { - const t: Trigger = .{ .key = .{ .translated = .b } }; + const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); try testing.expectEqual(Flags{}, e.leaf.flags); } { - const t: Trigger = .{ .key = .{ .translated = .c } }; + const t: Trigger = .{ .key = .{ .unicode = 'c' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_tab); @@ -2322,13 +2251,13 @@ test "set: parseAndPut overwrite sequence" { try s.parseAndPut(alloc, "a>b=new_window"); var current: *Set = &s; { - const t: Trigger = .{ .key = .{ .translated = .a } }; + const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { - const t: Trigger = .{ .key = .{ .translated = .b } }; + const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); @@ -2347,13 +2276,13 @@ test "set: parseAndPut overwrite leader" { try s.parseAndPut(alloc, "a>b=new_window"); var current: *Set = &s; { - const t: Trigger = .{ .key = .{ .translated = .a } }; + const t: Trigger = .{ .key = .{ .unicode = 'a' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leader); current = e.leader; } { - const t: Trigger = .{ .key = .{ .translated = .b } }; + const t: Trigger = .{ .key = .{ .unicode = 'b' } }; const e = current.get(t).?.value_ptr.*; try testing.expect(e == .leaf); try testing.expect(e.leaf.action == .new_window); @@ -2372,7 +2301,7 @@ test "set: parseAndPut unbind sequence unbinds leader" { try s.parseAndPut(alloc, "a>b=unbind"); var current: *Set = &s; { - const t: Trigger = .{ .key = .{ .translated = .a } }; + const t: Trigger = .{ .key = .{ .unicode = 'a' } }; try testing.expect(current.get(t) == null); } } @@ -2387,7 +2316,7 @@ test "set: parseAndPut unbind sequence unbinds leader if not set" { try s.parseAndPut(alloc, "a>b=unbind"); var current: *Set = &s; { - const t: Trigger = .{ .key = .{ .translated = .a } }; + const t: Trigger = .{ .key = .{ .unicode = 'a' } }; try testing.expect(current.get(t) == null); } } @@ -2405,7 +2334,7 @@ test "set: parseAndPut sequence preserves reverse mapping" { // Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } } @@ -2419,13 +2348,13 @@ test "set: put overwrites sequence" { try s.parseAndPut(alloc, "ctrl+a>b=new_window"); try s.put(alloc, .{ .mods = .{ .ctrl = true }, - .key = .{ .translated = .a }, + .key = .{ .unicode = 'a' }, }, .{ .new_window = {} }); // Creates reverse mapping { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } } @@ -2436,24 +2365,24 @@ test "set: maintains reverse mapping" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } // should be most recent - try s.put(alloc, .{ .key = .{ .translated = .b } }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .b); + try testing.expect(trigger.key.unicode == 'b'); } // removal should replace - s.remove(alloc, .{ .key = .{ .translated = .b } }); + s.remove(alloc, .{ .key = .{ .unicode = 'b' } }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } } @@ -2464,29 +2393,29 @@ test "set: performable is not part of reverse mappings" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } // trigger should be non-performable try s.putFlags( alloc, - .{ .key = .{ .translated = .b } }, + .{ .key = .{ .unicode = 'b' } }, .{ .new_window = {} }, .{ .performable = true }, ); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } // removal of performable should do nothing - s.remove(alloc, .{ .key = .{ .translated = .b } }); + s.remove(alloc, .{ .key = .{ .unicode = 'b' } }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } } @@ -2497,14 +2426,14 @@ test "set: overriding a mapping updates reverse" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }).?; - try testing.expect(trigger.key.translated == .a); + try testing.expect(trigger.key.unicode == 'a'); } // should be most recent - try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_tab = {} }); + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_tab = {} }); { const trigger = s.getTrigger(.{ .new_window = {} }); try testing.expect(trigger == null); @@ -2518,22 +2447,22 @@ test "set: consumed state" { var s: Set = .{}; defer s.deinit(alloc); - try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); - try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf); - try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed); + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.* == .leaf); + try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); try s.putFlags( alloc, - .{ .key = .{ .translated = .a } }, + .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }, .{ .consumed = false }, ); - try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf); - try testing.expect(!s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed); + try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.* == .leaf); + try testing.expect(!s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); - try s.put(alloc, .{ .key = .{ .translated = .a } }, .{ .new_window = {} }); - try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.* == .leaf); - try testing.expect(s.get(.{ .key = .{ .translated = .a } }).?.value_ptr.*.leaf.flags.consumed); + try s.put(alloc, .{ .key = .{ .unicode = 'a' } }, .{ .new_window = {} }); + try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.* == .leaf); + try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); } test "Action: clone" { diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index e79856a94..3d43a4e86 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -1082,7 +1082,7 @@ test "kitty: plain text" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .a, + .key = .key_a, .mods = .{}, .utf8 = "abcd", }, @@ -1098,7 +1098,7 @@ test "kitty: repeat with just disambiguate" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .a, + .key = .key_a, .action = .repeat, .mods = .{}, .utf8 = "a", @@ -1222,7 +1222,7 @@ test "kitty: enter with all flags" { test "kitty: ctrl with all flags" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ - .event = .{ .key = .left_control, .mods = .{ .ctrl = true }, .utf8 = "" }, + .event = .{ .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "" }, .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1240,7 +1240,7 @@ test "kitty: ctrl release with ctrl mod set" { var enc: KeyEncoder = .{ .event = .{ .action = .release, - .key = .left_control, + .key = .control_left, .mods = .{ .ctrl = true }, .utf8 = "", }, @@ -1272,7 +1272,7 @@ test "kitty: composing with no modifier" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .a, + .key = .key_a, .mods = .{ .shift = true }, .composing = true, }, @@ -1287,7 +1287,7 @@ test "kitty: composing with modifier" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .left_shift, + .key = .shift_left, .mods = .{ .shift = true }, .composing = true, }, @@ -1302,7 +1302,7 @@ test "kitty: shift+a on US keyboard" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .a, + .key = .key_a, .mods = .{ .shift = true }, .utf8 = "A", .unshifted_codepoint = 97, // lowercase A @@ -1321,7 +1321,7 @@ test "kitty: matching unshifted codepoint" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .a, + .key = .key_a, .mods = .{ .shift = true }, .utf8 = "A", .unshifted_codepoint = 65, @@ -1344,7 +1344,7 @@ test "kitty: report alternates with caps" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .j, + .key = .key_j, .mods = .{ .caps_lock = true }, .utf8 = "J", .unshifted_codepoint = 106, @@ -1450,7 +1450,7 @@ test "kitty: report alternates with hu layout release" { var enc: KeyEncoder = .{ .event = .{ .action = .release, - .key = .left_bracket, + .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "", .unshifted_codepoint = 337, @@ -1473,7 +1473,7 @@ test "kitty: up arrow with utf8" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .up, + .key = .arrow_up, .mods = .{}, .utf8 = &.{30}, }, @@ -1505,7 +1505,7 @@ test "kitty: left shift" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .left_shift, + .key = .shift_left, .mods = .{}, .utf8 = "", }, @@ -1521,7 +1521,7 @@ test "kitty: left shift with report all" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .left_shift, + .key = .shift_left, .mods = .{}, .utf8 = "", }, @@ -1539,7 +1539,7 @@ test "kitty: report associated with alt text on macOS with option" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .w, + .key = .key_w, .mods = .{ .alt = true }, .utf8 = "∑", .unshifted_codepoint = 119, @@ -1565,7 +1565,7 @@ test "kitty: report associated with alt text on macOS with alt" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .w, + .key = .key_w, .mods = .{ .alt = true }, .utf8 = "∑", .unshifted_codepoint = 119, @@ -1588,7 +1588,7 @@ test "kitty: report associated with alt text on macOS with alt" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .w, + .key = .key_w, .mods = .{}, .utf8 = "∑", .unshifted_codepoint = 119, @@ -1611,7 +1611,7 @@ test "kitty: report associated with modifiers" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .j, + .key = .key_j, .mods = .{ .ctrl = true }, .utf8 = "j", .unshifted_codepoint = 106, @@ -1632,7 +1632,7 @@ test "kitty: report associated" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .j, + .key = .key_j, .mods = .{ .shift = true }, .utf8 = "J", .unshifted_codepoint = 106, @@ -1654,7 +1654,7 @@ test "kitty: report associated on release" { var enc: KeyEncoder = .{ .event = .{ .action = .release, - .key = .j, + .key = .key_j, .mods = .{ .shift = true }, .utf8 = "J", .unshifted_codepoint = 106, @@ -1713,7 +1713,7 @@ test "kitty: enter with utf8 (dead key state)" { test "kitty: keypad number" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ - .event = .{ .key = .kp_1, .mods = .{}, .utf8 = "1" }, + .event = .{ .key = .numpad_1, .mods = .{}, .utf8 = "1" }, .kitty_flags = .{ .disambiguate = true, .report_events = true, @@ -1807,7 +1807,7 @@ test "legacy: ctrl+alt+c" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .c, + .key = .key_c, .mods = .{ .ctrl = true, .alt = true }, .utf8 = "c", }, @@ -1821,7 +1821,7 @@ test "legacy: alt+c" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .c, + .key = .key_c, .utf8 = "c", .mods = .{ .alt = true }, }, @@ -1837,7 +1837,7 @@ test "legacy: alt+e only unshifted" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .e, + .key = .key_e, .unshifted_codepoint = 'e', .mods = .{ .alt = true }, }, @@ -1855,7 +1855,7 @@ test "legacy: alt+x macos" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .c, + .key = .key_c, .utf8 = "≈", .unshifted_codepoint = 'c', .mods = .{ .alt = true }, @@ -1891,7 +1891,7 @@ test "legacy: alt+ф" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .f, + .key = .key_f, .utf8 = "ф", .mods = .{ .alt = true }, }, @@ -1906,7 +1906,7 @@ test "legacy: ctrl+c" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .c, + .key = .key_c, .mods = .{ .ctrl = true }, .utf8 = "c", }, @@ -1947,7 +1947,7 @@ test "legacy: ctrl+shift+char with modify other state 2" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .h, + .key = .key_h, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "H", }, @@ -1962,7 +1962,7 @@ test "legacy: fixterm awkward letters" { var buf: [128]u8 = undefined; { var enc: KeyEncoder = .{ .event = .{ - .key = .i, + .key = .key_i, .mods = .{ .ctrl = true }, .utf8 = "i", } }; @@ -1971,7 +1971,7 @@ test "legacy: fixterm awkward letters" { } { var enc: KeyEncoder = .{ .event = .{ - .key = .m, + .key = .key_m, .mods = .{ .ctrl = true }, .utf8 = "m", } }; @@ -1980,7 +1980,7 @@ test "legacy: fixterm awkward letters" { } { var enc: KeyEncoder = .{ .event = .{ - .key = .left_bracket, + .key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "[", } }; @@ -1989,7 +1989,7 @@ test "legacy: fixterm awkward letters" { } { var enc: KeyEncoder = .{ .event = .{ - .key = .two, + .key = .digit_2, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "@", .unshifted_codepoint = '2', @@ -2005,7 +2005,7 @@ test "legacy: ctrl+shift+letter ascii" { var buf: [128]u8 = undefined; { var enc: KeyEncoder = .{ .event = .{ - .key = .m, + .key = .key_m, .mods = .{ .ctrl = true, .shift = true }, .utf8 = "M", .unshifted_codepoint = 'm', @@ -2019,7 +2019,7 @@ test "legacy: shift+function key should use all mods" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .up, + .key = .arrow_up, .mods = .{ .shift = true }, .consumed_mods = .{ .shift = true }, }, @@ -2033,7 +2033,7 @@ test "legacy: keypad enter" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .kp_enter, + .key = .numpad_enter, .mods = .{}, .consumed_mods = .{}, }, @@ -2047,7 +2047,7 @@ test "legacy: keypad 1" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .kp_1, + .key = .numpad_1, .mods = .{}, .consumed_mods = .{}, .utf8 = "1", @@ -2062,7 +2062,7 @@ test "legacy: keypad 1 with application keypad" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .kp_1, + .key = .numpad_1, .mods = .{}, .consumed_mods = .{}, .utf8 = "1", @@ -2078,7 +2078,7 @@ test "legacy: keypad 1 with application keypad and numlock" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .kp_1, + .key = .numpad_1, .mods = .{ .num_lock = true }, .consumed_mods = .{}, .utf8 = "1", @@ -2094,7 +2094,7 @@ test "legacy: keypad 1 with application keypad and numlock ignore" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .kp_1, + .key = .numpad_1, .mods = .{ .num_lock = false }, .consumed_mods = .{}, .utf8 = "1", @@ -2189,8 +2189,8 @@ test "legacy: hu layout ctrl+ő sends proper codepoint" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .left_bracket, - .physical_key = .left_bracket, + .key = .bracket_left, + .physical_key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "ő", .unshifted_codepoint = 337, @@ -2207,7 +2207,7 @@ test "legacy: super-only on macOS with text" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .b, + .key = .key_b, .utf8 = "b", .mods = .{ .super = true }, }, @@ -2223,7 +2223,7 @@ test "legacy: super and other mods on macOS with text" { var buf: [128]u8 = undefined; var enc: KeyEncoder = .{ .event = .{ - .key = .b, + .key = .key_b, .utf8 = "B", .mods = .{ .super = true, .shift = true }, }, @@ -2234,50 +2234,50 @@ test "legacy: super and other mods on macOS with text" { } test "ctrlseq: normal ctrl c" { - const seq = ctrlSeq(.invalid, "c", 'c', .{ .ctrl = true }); + const seq = ctrlSeq(.unidentified, "c", 'c', .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: normal ctrl c, right control" { - const seq = ctrlSeq(.invalid, "c", 'c', .{ .ctrl = true, .sides = .{ .ctrl = .right } }); + const seq = ctrlSeq(.unidentified, "c", 'c', .{ .ctrl = true, .sides = .{ .ctrl = .right } }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: alt should be allowed" { - const seq = ctrlSeq(.invalid, "c", 'c', .{ .alt = true, .ctrl = true }); + const seq = ctrlSeq(.unidentified, "c", 'c', .{ .alt = true, .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: no ctrl does nothing" { - try testing.expect(ctrlSeq(.invalid, "c", 'c', .{}) == null); + try testing.expect(ctrlSeq(.unidentified, "c", 'c', .{}) == null); } test "ctrlseq: shifted non-character" { - const seq = ctrlSeq(.invalid, "_", '-', .{ .ctrl = true, .shift = true }); + const seq = ctrlSeq(.unidentified, "_", '-', .{ .ctrl = true, .shift = true }); try testing.expectEqual(@as(u8, 0x1F), seq.?); } test "ctrlseq: caps ascii letter" { - const seq = ctrlSeq(.invalid, "C", 'c', .{ .ctrl = true, .caps_lock = true }); + const seq = ctrlSeq(.unidentified, "C", 'c', .{ .ctrl = true, .caps_lock = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: shift does not generate ctrl seq" { - try testing.expect(ctrlSeq(.invalid, "C", 'c', .{ .shift = true }) == null); - try testing.expect(ctrlSeq(.invalid, "C", 'c', .{ .shift = true, .ctrl = true }) == null); + try testing.expect(ctrlSeq(.unidentified, "C", 'c', .{ .shift = true }) == null); + try testing.expect(ctrlSeq(.unidentified, "C", 'c', .{ .shift = true, .ctrl = true }) == null); } test "ctrlseq: russian ctrl c" { - const seq = ctrlSeq(.c, "с", 0x0441, .{ .ctrl = true }); + const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } test "ctrlseq: russian shifted ctrl c" { - const seq = ctrlSeq(.c, "с", 0x0441, .{ .ctrl = true, .shift = true }); + const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true, .shift = true }); try testing.expect(seq == null); } test "ctrlseq: russian alt ctrl c" { - const seq = ctrlSeq(.c, "с", 0x0441, .{ .ctrl = true, .alt = true }); + const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true, .alt = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } diff --git a/src/input/function_keys.zig b/src/input/function_keys.zig index 612112e28..33a5b89c0 100644 --- a/src/input/function_keys.zig +++ b/src/input/function_keys.zig @@ -75,10 +75,10 @@ pub const KeyEntryArray = std.EnumArray(key.Key, []const Entry); pub const keys = keys: { var result = KeyEntryArray.initFill(&.{}); - result.set(.up, pcStyle("\x1b[1;{}A") ++ cursorKey("\x1b[A", "\x1bOA")); - result.set(.down, pcStyle("\x1b[1;{}B") ++ cursorKey("\x1b[B", "\x1bOB")); - result.set(.right, pcStyle("\x1b[1;{}C") ++ cursorKey("\x1b[C", "\x1bOC")); - result.set(.left, pcStyle("\x1b[1;{}D") ++ cursorKey("\x1b[D", "\x1bOD")); + result.set(.arrow_up, pcStyle("\x1b[1;{}A") ++ cursorKey("\x1b[A", "\x1bOA")); + result.set(.arrow_down, pcStyle("\x1b[1;{}B") ++ cursorKey("\x1b[B", "\x1bOB")); + result.set(.arrow_right, pcStyle("\x1b[1;{}C") ++ cursorKey("\x1b[C", "\x1bOC")); + result.set(.arrow_left, pcStyle("\x1b[1;{}D") ++ cursorKey("\x1b[D", "\x1bOD")); result.set(.home, pcStyle("\x1b[1;{}H") ++ cursorKey("\x1b[H", "\x1bOH")); result.set(.end, pcStyle("\x1b[1;{}F") ++ cursorKey("\x1b[F", "\x1bOF")); result.set(.insert, pcStyle("\x1b[2;{}~") ++ .{Entry{ .sequence = "\x1B[2~" }}); @@ -101,33 +101,33 @@ pub const keys = keys: { result.set(.f12, pcStyle("\x1b[24;{}~") ++ .{Entry{ .sequence = "\x1B[24~" }}); // Keypad keys - result.set(.kp_0, kpKeys("p")); - result.set(.kp_1, kpKeys("q")); - result.set(.kp_2, kpKeys("r")); - result.set(.kp_3, kpKeys("s")); - result.set(.kp_4, kpKeys("t")); - result.set(.kp_5, kpKeys("u")); - result.set(.kp_6, kpKeys("v")); - result.set(.kp_7, kpKeys("w")); - result.set(.kp_8, kpKeys("x")); - result.set(.kp_9, kpKeys("y")); - result.set(.kp_decimal, kpKeys("n")); - result.set(.kp_divide, kpKeys("o")); - result.set(.kp_multiply, kpKeys("j")); - result.set(.kp_subtract, kpKeys("m")); - result.set(.kp_add, kpKeys("k")); - result.set(.kp_enter, kpKeys("M") ++ .{Entry{ .sequence = "\r" }}); - result.set(.kp_up, pcStyle("\x1b[1;{}A") ++ cursorKey("\x1b[A", "\x1bOA")); - result.set(.kp_down, pcStyle("\x1b[1;{}B") ++ cursorKey("\x1b[B", "\x1bOB")); - result.set(.kp_right, pcStyle("\x1b[1;{}C") ++ cursorKey("\x1b[C", "\x1bOC")); - result.set(.kp_left, pcStyle("\x1b[1;{}D") ++ cursorKey("\x1b[D", "\x1bOD")); - result.set(.kp_begin, pcStyle("\x1b[1;{}E") ++ cursorKey("\x1b[E", "\x1bOE")); - result.set(.kp_home, pcStyle("\x1b[1;{}H") ++ cursorKey("\x1b[H", "\x1bOH")); - result.set(.kp_end, pcStyle("\x1b[1;{}F") ++ cursorKey("\x1b[F", "\x1bOF")); - result.set(.kp_insert, pcStyle("\x1b[2;{}~") ++ .{Entry{ .sequence = "\x1B[2~" }}); - result.set(.kp_delete, pcStyle("\x1b[3;{}~") ++ .{Entry{ .sequence = "\x1B[3~" }}); - result.set(.kp_page_up, pcStyle("\x1b[5;{}~") ++ .{Entry{ .sequence = "\x1B[5~" }}); - result.set(.kp_page_down, pcStyle("\x1b[6;{}~") ++ .{Entry{ .sequence = "\x1B[6~" }}); + result.set(.numpad_0, kpKeys("p")); + result.set(.numpad_1, kpKeys("q")); + result.set(.numpad_2, kpKeys("r")); + result.set(.numpad_3, kpKeys("s")); + result.set(.numpad_4, kpKeys("t")); + result.set(.numpad_5, kpKeys("u")); + result.set(.numpad_6, kpKeys("v")); + result.set(.numpad_7, kpKeys("w")); + result.set(.numpad_8, kpKeys("x")); + result.set(.numpad_9, kpKeys("y")); + result.set(.numpad_decimal, kpKeys("n")); + result.set(.numpad_divide, kpKeys("o")); + result.set(.numpad_multiply, kpKeys("j")); + result.set(.numpad_subtract, kpKeys("m")); + result.set(.numpad_add, kpKeys("k")); + result.set(.numpad_enter, kpKeys("M") ++ .{Entry{ .sequence = "\r" }}); + result.set(.numpad_up, pcStyle("\x1b[1;{}A") ++ cursorKey("\x1b[A", "\x1bOA")); + result.set(.numpad_down, pcStyle("\x1b[1;{}B") ++ cursorKey("\x1b[B", "\x1bOB")); + result.set(.numpad_right, pcStyle("\x1b[1;{}C") ++ cursorKey("\x1b[C", "\x1bOC")); + result.set(.numpad_left, pcStyle("\x1b[1;{}D") ++ cursorKey("\x1b[D", "\x1bOD")); + result.set(.numpad_begin, pcStyle("\x1b[1;{}E") ++ cursorKey("\x1b[E", "\x1bOE")); + result.set(.numpad_home, pcStyle("\x1b[1;{}H") ++ cursorKey("\x1b[H", "\x1bOH")); + result.set(.numpad_end, pcStyle("\x1b[1;{}F") ++ cursorKey("\x1b[F", "\x1bOF")); + result.set(.numpad_insert, pcStyle("\x1b[2;{}~") ++ .{Entry{ .sequence = "\x1B[2~" }}); + result.set(.numpad_delete, pcStyle("\x1b[3;{}~") ++ .{Entry{ .sequence = "\x1B[3~" }}); + result.set(.numpad_page_up, pcStyle("\x1b[5;{}~") ++ .{Entry{ .sequence = "\x1B[5~" }}); + result.set(.numpad_page_down, pcStyle("\x1b[6;{}~") ++ .{Entry{ .sequence = "\x1B[6~" }}); result.set(.backspace, &.{ // Modify Keys Normal diff --git a/src/input/key.zig b/src/input/key.zig index a082134a7..0609108a1 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -21,7 +21,7 @@ pub const KeyEvent = struct { /// the "i" physical key will be reported as "c". The physical /// key is the key that was physically pressed on the keyboard. key: Key, - physical_key: Key = .invalid, + physical_key: Key = .unidentified, /// Mods are the modifiers that are pressed. mods: Mods = .{}, @@ -391,6 +391,25 @@ pub const Key = enum(c_int) { numpad_paren_right, numpad_subtract, + // > For numpads that provide keys not listed here, a code value string + // > should be created by starting with "Numpad" and appending an + // > appropriate description of the key. + // + // These numpad entries are distinguished by various encoding protocols + // (legacy and Kitty) so we support them here in case the apprt can + // produce them. + numpad_up, + numpad_down, + numpad_right, + numpad_left, + numpad_begin, + numpad_home, + numpad_end, + numpad_insert, + numpad_delete, + numpad_page_up, + numpad_page_down, + // "Function Section" § 3.5 escape, f1, @@ -405,6 +424,19 @@ pub const Key = enum(c_int) { f10, f11, f12, + f13, + f14, + f15, + f16, + f17, + f18, + f19, + f20, + f21, + f22, + f23, + f24, + f25, @"fn", fn_lock, print_screen, @@ -437,87 +469,61 @@ pub const Key = enum(c_int) { // Backwards compatibility for Ghostty 1.1.x and earlier, we don't // want to force people to rewrite their configs. - pub const a = .key_a; - pub const b = .key_b; - pub const c = .key_c; - pub const d = .key_d; - pub const e = .key_e; - pub const f = .key_f; - pub const g = .key_g; - pub const h = .key_h; - pub const i = .key_i; - pub const j = .key_j; - pub const k = .key_k; - pub const l = .key_l; - pub const m = .key_m; - pub const n = .key_n; - pub const o = .key_o; - pub const p = .key_p; - pub const q = .key_q; - pub const r = .key_r; - pub const s = .key_s; - pub const t = .key_t; - pub const u = .key_u; - pub const v = .key_v; - pub const w = .key_w; - pub const x = .key_x; - pub const y = .key_y; - pub const z = .key_z; - pub const zero = .digit_0; - pub const one = .digit_1; - pub const two = .digit_2; - pub const three = .digit_3; - pub const four = .digit_4; - pub const five = .digit_5; - pub const six = .digit_6; - pub const seven = .digit_7; - pub const eight = .digit_8; - pub const nine = .digit_9; - pub const apostrophe = .quote; - pub const grave_accent = .backquote; - pub const left_bracket = .bracket_left; - pub const right_bracket = .bracket_right; - pub const up = .arrow_up; - pub const down = .arrow_down; - pub const left = .arrow_left; - pub const right = .arrow_right; - pub const kp_0 = .numpad_0; - pub const kp_1 = .numpad_1; - pub const kp_2 = .numpad_2; - pub const kp_3 = .numpad_3; - pub const kp_4 = .numpad_4; - pub const kp_5 = .numpad_5; - pub const kp_6 = .numpad_6; - pub const kp_7 = .numpad_7; - pub const kp_8 = .numpad_8; - pub const kp_9 = .numpad_9; - pub const kp_decimal = .numpad_decimal; - pub const kp_divide = .numpad_divide; - pub const kp_multiply = .numpad_multiply; - pub const kp_subtract = .numpad_subtract; - pub const kp_add = .numpad_add; - pub const kp_enter = .numpad_enter; - pub const kp_equal = .numpad_equal; - pub const kp_separator = .numpad_separator; - pub const kp_left = .numpad_left; - pub const kp_right = .numpad_right; - pub const kp_up = .numpad_up; - pub const kp_down = .numpad_down; - pub const kp_page_up = .numpad_page_up; - pub const kp_page_down = .numpad_page_down; - pub const kp_home = .numpad_home; - pub const kp_end = .numpad_end; - pub const kp_insert = .numpad_insert; - pub const kp_delete = .numpad_delete; - pub const kp_begin = .numpad_begin; - pub const left_shift = .shift_left; - pub const right_shift = .shift_right; - pub const left_control = .control_left; - pub const right_control = .control_right; - pub const left_alt = .alt_left; - pub const right_alt = .alt_right; - pub const left_super = .meta_left; - pub const right_super = .meta_right; + // pub const zero = .digit_0; + // pub const one = .digit_1; + // pub const two = .digit_2; + // pub const three = .digit_3; + // pub const four = .digit_4; + // pub const five = .digit_5; + // pub const six = .digit_6; + // pub const seven = .digit_7; + // pub const eight = .digit_8; + // pub const nine = .digit_9; + // pub const apostrophe = .quote; + // pub const grave_accent = .backquote; + // pub const left_bracket = .bracket_left; + // pub const right_bracket = .bracket_right; + // pub const up = .arrow_up; + // pub const down = .arrow_down; + // pub const left = .arrow_left; + // pub const right = .arrow_right; + // pub const kp_0 = .numpad_0; + // pub const kp_1 = .numpad_1; + // pub const kp_2 = .numpad_2; + // pub const kp_3 = .numpad_3; + // pub const kp_4 = .numpad_4; + // pub const kp_5 = .numpad_5; + // pub const kp_6 = .numpad_6; + // pub const kp_7 = .numpad_7; + // pub const kp_8 = .numpad_8; + // pub const kp_9 = .numpad_9; + // pub const kp_decimal = .numpad_decimal; + // pub const kp_divide = .numpad_divide; + // pub const kp_multiply = .numpad_multiply; + // pub const kp_subtract = .numpad_subtract; + // pub const kp_add = .numpad_add; + // pub const kp_enter = .numpad_enter; + // pub const kp_equal = .numpad_equal; + // pub const kp_separator = .numpad_separator; + // pub const kp_left = .numpad_left; + // pub const kp_right = .numpad_right; + // pub const kp_up = .numpad_up; + // pub const kp_down = .numpad_down; + // pub const kp_page_up = .numpad_page_up; + // pub const kp_page_down = .numpad_page_down; + // pub const kp_home = .numpad_home; + // pub const kp_end = .numpad_end; + // pub const kp_insert = .numpad_insert; + // pub const kp_delete = .numpad_delete; + // pub const kp_begin = .numpad_begin; + // pub const left_shift = .shift_left; + // pub const right_shift = .shift_right; + // pub const left_control = .control_left; + // pub const right_control = .control_right; + // pub const left_alt = .alt_left; + // pub const right_alt = .alt_right; + // pub const left_super = .meta_left; + // pub const right_super = .meta_right; /// Converts an ASCII character to a key, if possible. This returns /// null if the character is unknown. @@ -586,7 +592,7 @@ pub const Key = enum(c_int) { return switch (self) { inline else => |tag| { const name = @tagName(tag); - const result = comptime std.mem.startsWith(u8, name, "kp_"); + const result = comptime std.mem.startsWith(u8, name, "numpad_"); return result; }, }; @@ -763,107 +769,106 @@ pub const Key = enum(c_int) { /// or ctrl. pub fn ctrlOrSuper(self: Key) bool { if (comptime builtin.target.os.tag.isDarwin()) { - return self == .left_super or self == .right_super; + return self == .meta_left or self == .meta_right; } - return self == .left_control or self == .right_control; + return self == .control_left or self == .control_right; } /// true if this key is either left or right shift. pub fn leftOrRightShift(self: Key) bool { - return self == .left_shift or self == .right_shift; + return self == .shift_left or self == .shift_right; } /// true if this key is either left or right alt. pub fn leftOrRightAlt(self: Key) bool { - return self == .left_alt or self == .right_alt; + return self == .alt_left or self == .alt_right; } test "fromASCII should not return keypad keys" { const testing = std.testing; - try testing.expect(Key.fromASCII('0').? == .zero); + try testing.expect(Key.fromASCII('0').? == .digit_0); try testing.expect(Key.fromASCII('*') == null); } test "keypad keys" { const testing = std.testing; - try testing.expect(Key.kp_0.keypad()); - try testing.expect(!Key.one.keypad()); + try testing.expect(Key.numpad_0.keypad()); + try testing.expect(!Key.digit_1.keypad()); } const codepoint_map: []const struct { u21, Key } = &.{ - .{ 'a', .a }, - .{ 'b', .b }, - .{ 'c', .c }, - .{ 'd', .d }, - .{ 'e', .e }, - .{ 'f', .f }, - .{ 'g', .g }, - .{ 'h', .h }, - .{ 'i', .i }, - .{ 'j', .j }, - .{ 'k', .k }, - .{ 'l', .l }, - .{ 'm', .m }, - .{ 'n', .n }, - .{ 'o', .o }, - .{ 'p', .p }, - .{ 'q', .q }, - .{ 'r', .r }, - .{ 's', .s }, - .{ 't', .t }, - .{ 'u', .u }, - .{ 'v', .v }, - .{ 'w', .w }, - .{ 'x', .x }, - .{ 'y', .y }, - .{ 'z', .z }, - .{ '0', .zero }, - .{ '1', .one }, - .{ '2', .two }, - .{ '3', .three }, - .{ '4', .four }, - .{ '5', .five }, - .{ '6', .six }, - .{ '7', .seven }, - .{ '8', .eight }, - .{ '9', .nine }, + .{ 'a', .key_a }, + .{ 'b', .key_b }, + .{ 'c', .key_c }, + .{ 'd', .key_d }, + .{ 'e', .key_e }, + .{ 'f', .key_f }, + .{ 'g', .key_g }, + .{ 'h', .key_h }, + .{ 'i', .key_i }, + .{ 'j', .key_j }, + .{ 'k', .key_k }, + .{ 'l', .key_l }, + .{ 'm', .key_m }, + .{ 'n', .key_n }, + .{ 'o', .key_o }, + .{ 'p', .key_p }, + .{ 'q', .key_q }, + .{ 'r', .key_r }, + .{ 's', .key_s }, + .{ 't', .key_t }, + .{ 'u', .key_u }, + .{ 'v', .key_v }, + .{ 'w', .key_w }, + .{ 'x', .key_x }, + .{ 'y', .key_y }, + .{ 'z', .key_z }, + .{ '0', .digit_0 }, + .{ '1', .digit_1 }, + .{ '2', .digit_2 }, + .{ '3', .digit_3 }, + .{ '4', .digit_4 }, + .{ '5', .digit_5 }, + .{ '6', .digit_6 }, + .{ '7', .digit_7 }, + .{ '8', .digit_8 }, + .{ '9', .digit_9 }, .{ ';', .semicolon }, .{ ' ', .space }, - .{ '\'', .apostrophe }, + .{ '\'', .quote }, .{ ',', .comma }, - .{ '`', .grave_accent }, + .{ '`', .backquote }, .{ '.', .period }, .{ '/', .slash }, .{ '-', .minus }, - .{ '+', .plus }, .{ '=', .equal }, - .{ '[', .left_bracket }, - .{ ']', .right_bracket }, + .{ '[', .bracket_left }, + .{ ']', .bracket_right }, .{ '\\', .backslash }, // Control characters .{ '\t', .tab }, - // Keypad entries. We just assume keypad with the kp_ prefix + // Keypad entries. We just assume keypad with the numpad_ prefix // so that has some special meaning. These must also always be last, // so that our `fromASCII` function doesn't accidentally map them // over normal numerics and other keys. - .{ '0', .kp_0 }, - .{ '1', .kp_1 }, - .{ '2', .kp_2 }, - .{ '3', .kp_3 }, - .{ '4', .kp_4 }, - .{ '5', .kp_5 }, - .{ '6', .kp_6 }, - .{ '7', .kp_7 }, - .{ '8', .kp_8 }, - .{ '9', .kp_9 }, - .{ '.', .kp_decimal }, - .{ '/', .kp_divide }, - .{ '*', .kp_multiply }, - .{ '-', .kp_subtract }, - .{ '+', .kp_add }, - .{ '=', .kp_equal }, + .{ '0', .numpad_0 }, + .{ '1', .numpad_1 }, + .{ '2', .numpad_2 }, + .{ '3', .numpad_3 }, + .{ '4', .numpad_4 }, + .{ '5', .numpad_5 }, + .{ '6', .numpad_6 }, + .{ '7', .numpad_7 }, + .{ '8', .numpad_8 }, + .{ '9', .numpad_9 }, + .{ '.', .numpad_decimal }, + .{ '/', .numpad_divide }, + .{ '*', .numpad_multiply }, + .{ '-', .numpad_subtract }, + .{ '+', .numpad_add }, + .{ '=', .numpad_equal }, }; }; diff --git a/src/input/kitty.zig b/src/input/kitty.zig index 6e9cdddf8..be397b84b 100644 --- a/src/input/kitty.zig +++ b/src/input/kitty.zig @@ -49,10 +49,10 @@ const raw_entries: []const RawEntry = &.{ .{ .backspace, 127, 'u', false }, .{ .insert, 2, '~', false }, .{ .delete, 3, '~', false }, - .{ .left, 1, 'D', false }, - .{ .right, 1, 'C', false }, - .{ .up, 1, 'A', false }, - .{ .down, 1, 'B', false }, + .{ .arrow_left, 1, 'D', false }, + .{ .arrow_right, 1, 'C', false }, + .{ .arrow_up, 1, 'A', false }, + .{ .arrow_down, 1, 'B', false }, .{ .page_up, 5, '~', false }, .{ .page_down, 6, '~', false }, .{ .home, 1, 'H', false }, @@ -89,46 +89,32 @@ const raw_entries: []const RawEntry = &.{ .{ .f24, 57387, 'u', false }, .{ .f25, 57388, 'u', false }, - .{ .kp_0, 57399, 'u', false }, - .{ .kp_1, 57400, 'u', false }, - .{ .kp_2, 57401, 'u', false }, - .{ .kp_3, 57402, 'u', false }, - .{ .kp_4, 57403, 'u', false }, - .{ .kp_5, 57404, 'u', false }, - .{ .kp_6, 57405, 'u', false }, - .{ .kp_7, 57406, 'u', false }, - .{ .kp_8, 57407, 'u', false }, - .{ .kp_9, 57408, 'u', false }, - .{ .kp_decimal, 57409, 'u', false }, - .{ .kp_divide, 57410, 'u', false }, - .{ .kp_multiply, 57411, 'u', false }, - .{ .kp_subtract, 57412, 'u', false }, - .{ .kp_add, 57413, 'u', false }, - .{ .kp_enter, 57414, 'u', false }, - .{ .kp_equal, 57415, 'u', false }, - .{ .kp_separator, 57416, 'u', false }, - .{ .kp_left, 57417, 'u', false }, - .{ .kp_right, 57418, 'u', false }, - .{ .kp_up, 57419, 'u', false }, - .{ .kp_down, 57420, 'u', false }, - .{ .kp_page_up, 57421, 'u', false }, - .{ .kp_page_down, 57422, 'u', false }, - .{ .kp_home, 57423, 'u', false }, - .{ .kp_end, 57424, 'u', false }, - .{ .kp_insert, 57425, 'u', false }, - .{ .kp_delete, 57426, 'u', false }, - .{ .kp_begin, 57427, 'u', false }, + .{ .numpad_0, 57399, 'u', false }, + .{ .numpad_1, 57400, 'u', false }, + .{ .numpad_2, 57401, 'u', false }, + .{ .numpad_3, 57402, 'u', false }, + .{ .numpad_4, 57403, 'u', false }, + .{ .numpad_5, 57404, 'u', false }, + .{ .numpad_6, 57405, 'u', false }, + .{ .numpad_7, 57406, 'u', false }, + .{ .numpad_8, 57407, 'u', false }, + .{ .numpad_9, 57408, 'u', false }, + .{ .numpad_decimal, 57409, 'u', false }, + .{ .numpad_divide, 57410, 'u', false }, + .{ .numpad_multiply, 57411, 'u', false }, + .{ .numpad_subtract, 57412, 'u', false }, + .{ .numpad_add, 57413, 'u', false }, + .{ .numpad_enter, 57414, 'u', false }, + .{ .numpad_equal, 57415, 'u', false }, - // TODO: media keys - - .{ .left_shift, 57441, 'u', true }, - .{ .right_shift, 57447, 'u', true }, - .{ .left_control, 57442, 'u', true }, - .{ .right_control, 57448, 'u', true }, - .{ .left_super, 57444, 'u', true }, - .{ .right_super, 57450, 'u', true }, - .{ .left_alt, 57443, 'u', true }, - .{ .right_alt, 57449, 'u', true }, + .{ .shift_left, 57441, 'u', true }, + .{ .shift_right, 57447, 'u', true }, + .{ .control_left, 57442, 'u', true }, + .{ .control_right, 57448, 'u', true }, + .{ .meta_left, 57444, 'u', true }, + .{ .meta_right, 57450, 'u', true }, + .{ .alt_left, 57443, 'u', true }, + .{ .alt_right, 57449, 'u', true }, }; test { diff --git a/src/inspector/key.zig b/src/inspector/key.zig index e28bd5d4a..10626d6bd 100644 --- a/src/inspector/key.zig +++ b/src/inspector/key.zig @@ -56,7 +56,7 @@ pub const Event = struct { // Write our key. If we have an invalid key we attempt to write // the utf8 associated with it if we have it to handle non-ascii. try writer.writeAll(switch (self.event.key) { - .invalid => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(.invalid), + .unidentified => if (self.event.utf8.len > 0) self.event.utf8 else @tagName(self.event.key), else => @tagName(self.event.key), }); @@ -227,9 +227,9 @@ test "event string" { const testing = std.testing; const alloc = testing.allocator; - var event = try Event.init(alloc, .{ .key = .a }); + var event = try Event.init(alloc, .{ .key = .key_a }); defer event.deinit(alloc); var buf: [1024]u8 = undefined; - try testing.expectEqualStrings("Press: a", try event.label(&buf)); + try testing.expectEqualStrings("Press: key_a", try event.label(&buf)); } diff --git a/src/surface_mouse.zig b/src/surface_mouse.zig index ed1e36335..a9702a8fe 100644 --- a/src/surface_mouse.zig +++ b/src/surface_mouse.zig @@ -132,7 +132,7 @@ test "keyToMouseShape" { { // No specific key pressed const m: SurfaceMouse = .{ - .physical_key = .invalid, + .physical_key = .unidentified, .mouse_event = .none, .mouse_shape = .progress, .mods = .{}, @@ -148,7 +148,7 @@ test "keyToMouseShape" { // Over a link. NOTE: This tests that we don't touch the inbound state, // not necessarily if we're over a link. const m: SurfaceMouse = .{ - .physical_key = .left_shift, + .physical_key = .shift_left, .mouse_event = .none, .mouse_shape = .progress, .mods = .{}, @@ -163,7 +163,7 @@ test "keyToMouseShape" { { // Mouse is currently hidden const m: SurfaceMouse = .{ - .physical_key = .left_shift, + .physical_key = .shift_left, .mouse_event = .none, .mouse_shape = .progress, .mods = .{}, @@ -178,7 +178,7 @@ test "keyToMouseShape" { { // default, no mods (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_shift, + .physical_key = .shift_left, .mouse_event = .x10, .mouse_shape = .default, .mods = .{}, @@ -194,7 +194,7 @@ test "keyToMouseShape" { { // default -> crosshair (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_alt, + .physical_key = .alt_left, .mouse_event = .x10, .mouse_shape = .default, .mods = .{ .ctrl = true, .super = true, .alt = true, .shift = true }, @@ -210,7 +210,7 @@ test "keyToMouseShape" { { // default -> text (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_shift, + .physical_key = .shift_left, .mouse_event = .x10, .mouse_shape = .default, .mods = .{ .shift = true }, @@ -226,7 +226,7 @@ test "keyToMouseShape" { { // crosshair -> text (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_alt, + .physical_key = .alt_left, .mouse_event = .x10, .mouse_shape = .crosshair, .mods = .{ .shift = true }, @@ -242,7 +242,7 @@ test "keyToMouseShape" { { // crosshair -> default (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_alt, + .physical_key = .alt_left, .mouse_event = .x10, .mouse_shape = .crosshair, .mods = .{}, @@ -258,7 +258,7 @@ test "keyToMouseShape" { { // text -> crosshair (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_alt, + .physical_key = .alt_left, .mouse_event = .x10, .mouse_shape = .text, .mods = .{ .ctrl = true, .super = true, .alt = true, .shift = true }, @@ -274,7 +274,7 @@ test "keyToMouseShape" { { // text -> default (mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_shift, + .physical_key = .shift_left, .mouse_event = .x10, .mouse_shape = .text, .mods = .{}, @@ -290,7 +290,7 @@ test "keyToMouseShape" { { // text, no mods (no mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_shift, + .physical_key = .shift_left, .mouse_event = .none, .mouse_shape = .text, .mods = .{}, @@ -306,7 +306,7 @@ test "keyToMouseShape" { { // text -> crosshair (no mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_alt, + .physical_key = .alt_left, .mouse_event = .none, .mouse_shape = .text, .mods = .{ .ctrl = true, .super = true, .alt = true }, @@ -322,7 +322,7 @@ test "keyToMouseShape" { { // crosshair -> text (no mouse tracking) const m: SurfaceMouse = .{ - .physical_key = .left_alt, + .physical_key = .alt_left, .mouse_event = .none, .mouse_shape = .crosshair, .mods = .{}, From 24d433333b5a28f5e484dfdc724262992eb67e91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 10:44:49 -0700 Subject: [PATCH 223/642] apprt/glfw: builds --- src/Surface.zig | 17 +++-- src/apprt/glfw.zig | 142 +++++++++++++++++++------------------- src/cli/list_keybinds.zig | 8 +-- src/input/key.zig | 16 ++--- 4 files changed, 93 insertions(+), 90 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0d4c9d984..e173d2d8b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1895,12 +1895,12 @@ pub fn keyCallback( // if we didn't have a previous event and this is a release // event then we just want to set it to null. const prev = self.pressed_key orelse break :event null; - if (prev.key == copy.key) copy.key = .invalid; + if (prev.key == copy.key) copy.key = .unidentified; } // If our key is invalid and we have no mods, then we're done! // This helps catch the state that we naturally released all keys. - if (copy.key == .invalid and copy.mods.empty()) break :event null; + if (copy.key == .unidentified and copy.mods.empty()) break :event null; break :event copy; }; @@ -2295,7 +2295,7 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { pressed_key.action = .release; // Release the full key first - if (pressed_key.key != .invalid) { + if (pressed_key.key != .unidentified) { assert(self.keyCallback(pressed_key) catch |err| err: { log.warn("error releasing key on focus loss err={}", .{err}); break :err .ignored; @@ -2315,8 +2315,15 @@ pub fn focusCallback(self: *Surface, focused: bool) !void { if (@field(pressed_key.mods, key)) { @field(pressed_key.mods, key) = false; inline for (&.{ "right", "left" }) |side| { - const keyname = if (comptime std.mem.eql(u8, key, "ctrl")) "control" else key; - pressed_key.key = @field(input.Key, side ++ "_" ++ keyname); + const keyname = comptime keyname: { + break :keyname if (std.mem.eql(u8, key, "ctrl")) + "control" + else if (std.mem.eql(u8, key, "super")) + "meta" + else + key; + }; + pressed_key.key = @field(input.Key, keyname ++ "_" ++ side); if (pressed_key.key != original_key) { assert(self.keyCallback(pressed_key) catch |err| err: { log.warn("error releasing key on focus loss err={}", .{err}); diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 9d1c8a6b5..763933b91 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -966,46 +966,46 @@ pub const Surface = struct { .repeat => .repeat, }; const key: input.Key = switch (glfw_key) { - .a => .a, - .b => .b, - .c => .c, - .d => .d, - .e => .e, - .f => .f, - .g => .g, - .h => .h, - .i => .i, - .j => .j, - .k => .k, - .l => .l, - .m => .m, - .n => .n, - .o => .o, - .p => .p, - .q => .q, - .r => .r, - .s => .s, - .t => .t, - .u => .u, - .v => .v, - .w => .w, - .x => .x, - .y => .y, - .z => .z, - .zero => .zero, - .one => .one, - .two => .two, - .three => .three, - .four => .four, - .five => .five, - .six => .six, - .seven => .seven, - .eight => .eight, - .nine => .nine, - .up => .up, - .down => .down, - .right => .right, - .left => .left, + .a => .key_a, + .b => .key_b, + .c => .key_c, + .d => .key_d, + .e => .key_e, + .f => .key_f, + .g => .key_g, + .h => .key_h, + .i => .key_i, + .j => .key_j, + .k => .key_k, + .l => .key_l, + .m => .key_m, + .n => .key_n, + .o => .key_o, + .p => .key_p, + .q => .key_q, + .r => .key_r, + .s => .key_s, + .t => .key_t, + .u => .key_u, + .v => .key_v, + .w => .key_w, + .x => .key_x, + .y => .key_y, + .z => .key_z, + .zero => .digit_0, + .one => .digit_1, + .two => .digit_2, + .three => .digit_3, + .four => .digit_4, + .five => .digit_5, + .six => .digit_6, + .seven => .digit_7, + .eight => .digit_8, + .nine => .digit_9, + .up => .arrow_up, + .down => .arrow_down, + .right => .arrow_right, + .left => .arrow_left, .home => .home, .end => .end, .page_up => .page_up, @@ -1036,34 +1036,34 @@ pub const Surface = struct { .F23 => .f23, .F24 => .f24, .F25 => .f25, - .kp_0 => .kp_0, - .kp_1 => .kp_1, - .kp_2 => .kp_2, - .kp_3 => .kp_3, - .kp_4 => .kp_4, - .kp_5 => .kp_5, - .kp_6 => .kp_6, - .kp_7 => .kp_7, - .kp_8 => .kp_8, - .kp_9 => .kp_9, - .kp_decimal => .kp_decimal, - .kp_divide => .kp_divide, - .kp_multiply => .kp_multiply, - .kp_subtract => .kp_subtract, - .kp_add => .kp_add, - .kp_enter => .kp_enter, - .kp_equal => .kp_equal, - .grave_accent => .grave_accent, + .kp_0 => .numpad_0, + .kp_1 => .numpad_1, + .kp_2 => .numpad_2, + .kp_3 => .numpad_3, + .kp_4 => .numpad_4, + .kp_5 => .numpad_5, + .kp_6 => .numpad_6, + .kp_7 => .numpad_7, + .kp_8 => .numpad_8, + .kp_9 => .numpad_9, + .kp_decimal => .numpad_decimal, + .kp_divide => .numpad_divide, + .kp_multiply => .numpad_multiply, + .kp_subtract => .numpad_subtract, + .kp_add => .numpad_add, + .kp_enter => .numpad_enter, + .kp_equal => .numpad_equal, + .grave_accent => .backquote, .minus => .minus, .equal => .equal, .space => .space, .semicolon => .semicolon, - .apostrophe => .apostrophe, + .apostrophe => .quote, .comma => .comma, .period => .period, .slash => .slash, - .left_bracket => .left_bracket, - .right_bracket => .right_bracket, + .left_bracket => .bracket_left, + .right_bracket => .bracket_right, .backslash => .backslash, .enter => .enter, .tab => .tab, @@ -1075,20 +1075,20 @@ pub const Surface = struct { .num_lock => .num_lock, .print_screen => .print_screen, .pause => .pause, - .left_shift => .left_shift, - .left_control => .left_control, - .left_alt => .left_alt, - .left_super => .left_super, - .right_shift => .right_shift, - .right_control => .right_control, - .right_alt => .right_alt, - .right_super => .right_super, + .left_shift => .shift_left, + .left_control => .control_left, + .left_alt => .alt_left, + .left_super => .meta_left, + .right_shift => .shift_right, + .right_control => .control_right, + .right_alt => .alt_right, + .right_super => .meta_right, + .menu => .context_menu, - .menu, .world_1, .world_2, .unknown, - => .invalid, + => .unidentified, }; // This is a hack for GLFW. We require our apprts to send both diff --git a/src/cli/list_keybinds.zig b/src/cli/list_keybinds.zig index 6cd989201..f84d540c3 100644 --- a/src/cli/list_keybinds.zig +++ b/src/cli/list_keybinds.zig @@ -155,14 +155,12 @@ const ChordBinding = struct { while (l_trigger != null and r_trigger != null) { const lhs_key: c_int = blk: { switch (l_trigger.?.data.key) { - .translated => |key| break :blk @intFromEnum(key), .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), } }; const rhs_key: c_int = blk: { switch (r_trigger.?.data.key) { - .translated => |key| break :blk @intFromEnum(key), .physical => |key| break :blk @intFromEnum(key), .unicode => |key| break :blk @intCast(key), } @@ -254,8 +252,7 @@ fn prettyPrint(alloc: Allocator, keybinds: Config.Keybinds) !u8 { result = win.printSegment(.{ .text = " + " }, .{ .col_offset = result.col }); } const key = switch (trigger.data.key) { - .translated => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}), - .physical => |k| try std.fmt.allocPrint(alloc, "physical:{s}", .{@tagName(k)}), + .physical => |k| try std.fmt.allocPrint(alloc, "{s}", .{@tagName(k)}), .unicode => |c| try std.fmt.allocPrint(alloc, "{u}", .{c}), }; result = win.printSegment(.{ .text = key }, .{ .col_offset = result.col }); @@ -297,8 +294,7 @@ fn iterateBindings(alloc: Allocator, iter: anytype, win: *const vaxis.Window) !s if (t.mods.shift) try std.fmt.format(buf.writer(), "shift + ", .{}); switch (t.key) { - .translated => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}), - .physical => |k| try std.fmt.format(buf.writer(), "physical:{s}", .{@tagName(k)}), + .physical => |k| try std.fmt.format(buf.writer(), "{s}", .{@tagName(k)}), .unicode => |c| try std.fmt.format(buf.writer(), "{u}", .{c}), } diff --git a/src/input/key.zig b/src/input/key.zig index 0609108a1..b39c5e5d3 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -573,14 +573,14 @@ pub const Key = enum(c_int) { /// True if this key is a modifier. pub fn modifier(self: Key) bool { return switch (self) { - .left_shift, - .left_control, - .left_alt, - .left_super, - .right_shift, - .right_control, - .right_alt, - .right_super, + .shift_left, + .control_left, + .alt_left, + .meta_left, + .shift_right, + .control_right, + .alt_right, + .meta_right, => true, else => false, From b991d36343e19ebff9026c331351b0dbe5b88fc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 10:58:41 -0700 Subject: [PATCH 224/642] macOS: build --- include/ghostty.h | 221 +++++++++++-------- macos/Sources/Ghostty/Ghostty.Input.swift | 250 +++++----------------- src/apprt/embedded.zig | 33 +-- src/input/key.zig | 211 ++++++++++-------- src/input/keycodes.zig | 140 ++++++------ 5 files changed, 379 insertions(+), 476 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 9409fa7c6..600396a84 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -104,9 +104,28 @@ typedef enum { } ghostty_input_action_e; typedef enum { - GHOSTTY_KEY_INVALID, + GHOSTTY_KEY_UNIDENTIFIED, - // a-z + // "Writing System Keys" § 3.1.1 + GHOSTTY_KEY_BACKQUOTE, + GHOSTTY_KEY_BACKSLASH, + GHOSTTY_KEY_BRACKET_LEFT, + GHOSTTY_KEY_BRACKET_RIGHT, + GHOSTTY_KEY_COMMA, + GHOSTTY_KEY_DIGIT_0, + GHOSTTY_KEY_DIGIT_1, + GHOSTTY_KEY_DIGIT_2, + GHOSTTY_KEY_DIGIT_3, + GHOSTTY_KEY_DIGIT_4, + GHOSTTY_KEY_DIGIT_5, + GHOSTTY_KEY_DIGIT_6, + GHOSTTY_KEY_DIGIT_7, + GHOSTTY_KEY_DIGIT_8, + GHOSTTY_KEY_DIGIT_9, + GHOSTTY_KEY_EQUAL, + GHOSTTY_KEY_INTL_BACKSLASH, + GHOSTTY_KEY_INTL_RO, + GHOSTTY_KEY_INTL_YEN, GHOSTTY_KEY_A, GHOSTTY_KEY_B, GHOSTTY_KEY_C, @@ -133,56 +152,90 @@ typedef enum { GHOSTTY_KEY_X, GHOSTTY_KEY_Y, GHOSTTY_KEY_Z, - - // numbers - GHOSTTY_KEY_ZERO, - GHOSTTY_KEY_ONE, - GHOSTTY_KEY_TWO, - GHOSTTY_KEY_THREE, - GHOSTTY_KEY_FOUR, - GHOSTTY_KEY_FIVE, - GHOSTTY_KEY_SIX, - GHOSTTY_KEY_SEVEN, - GHOSTTY_KEY_EIGHT, - GHOSTTY_KEY_NINE, - - // puncuation - GHOSTTY_KEY_SEMICOLON, - GHOSTTY_KEY_SPACE, - GHOSTTY_KEY_APOSTROPHE, - GHOSTTY_KEY_COMMA, - GHOSTTY_KEY_GRAVE_ACCENT, // ` - GHOSTTY_KEY_PERIOD, - GHOSTTY_KEY_SLASH, GHOSTTY_KEY_MINUS, - GHOSTTY_KEY_PLUS, - GHOSTTY_KEY_EQUAL, - GHOSTTY_KEY_LEFT_BRACKET, // [ - GHOSTTY_KEY_RIGHT_BRACKET, // ] - GHOSTTY_KEY_BACKSLASH, // \ + GHOSTTY_KEY_PERIOD, + GHOSTTY_KEY_QUOTE, + GHOSTTY_KEY_SEMICOLON, + GHOSTTY_KEY_SLASH, - // control - GHOSTTY_KEY_UP, - GHOSTTY_KEY_DOWN, - GHOSTTY_KEY_RIGHT, - GHOSTTY_KEY_LEFT, - GHOSTTY_KEY_HOME, - GHOSTTY_KEY_END, - GHOSTTY_KEY_INSERT, - GHOSTTY_KEY_DELETE, - GHOSTTY_KEY_CAPS_LOCK, - GHOSTTY_KEY_SCROLL_LOCK, - GHOSTTY_KEY_NUM_LOCK, - GHOSTTY_KEY_PAGE_UP, - GHOSTTY_KEY_PAGE_DOWN, - GHOSTTY_KEY_ESCAPE, - GHOSTTY_KEY_ENTER, - GHOSTTY_KEY_TAB, + // "Functional Keys" § 3.1.2 + GHOSTTY_KEY_ALT_LEFT, + GHOSTTY_KEY_ALT_RIGHT, GHOSTTY_KEY_BACKSPACE, - GHOSTTY_KEY_PRINT_SCREEN, - GHOSTTY_KEY_PAUSE, + GHOSTTY_KEY_CAPS_LOCK, + GHOSTTY_KEY_CONTEXT_MENU, + GHOSTTY_KEY_CONTROL_LEFT, + GHOSTTY_KEY_CONTROL_RIGHT, + GHOSTTY_KEY_ENTER, + GHOSTTY_KEY_META_LEFT, + GHOSTTY_KEY_META_RIGHT, + GHOSTTY_KEY_SHIFT_LEFT, + GHOSTTY_KEY_SHIFT_RIGHT, + GHOSTTY_KEY_SPACE, + GHOSTTY_KEY_TAB, + GHOSTTY_KEY_CONVERT, + GHOSTTY_KEY_KANA_MODE, + GHOSTTY_KEY_NON_CONVERT, - // function keys + // "Control Pad Section" § 3.2 + GHOSTTY_KEY_DELETE, + GHOSTTY_KEY_END, + GHOSTTY_KEY_HELP, + GHOSTTY_KEY_HOME, + GHOSTTY_KEY_INSERT, + GHOSTTY_KEY_PAGE_DOWN, + GHOSTTY_KEY_PAGE_UP, + + // "Arrow Pad Section" § 3.3 + GHOSTTY_KEY_ARROW_DOWN, + GHOSTTY_KEY_ARROW_LEFT, + GHOSTTY_KEY_ARROW_RIGHT, + GHOSTTY_KEY_ARROW_UP, + + // "Numpad Section" § 3.4 + GHOSTTY_KEY_NUM_LOCK, + GHOSTTY_KEY_NUMPAD_0, + GHOSTTY_KEY_NUMPAD_1, + GHOSTTY_KEY_NUMPAD_2, + GHOSTTY_KEY_NUMPAD_3, + GHOSTTY_KEY_NUMPAD_4, + GHOSTTY_KEY_NUMPAD_5, + GHOSTTY_KEY_NUMPAD_6, + GHOSTTY_KEY_NUMPAD_7, + GHOSTTY_KEY_NUMPAD_8, + GHOSTTY_KEY_NUMPAD_9, + GHOSTTY_KEY_NUMPAD_ADD, + GHOSTTY_KEY_NUMPAD_BACKSPACE, + GHOSTTY_KEY_NUMPAD_CLEAR, + GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY, + GHOSTTY_KEY_NUMPAD_COMMA, + GHOSTTY_KEY_NUMPAD_DECIMAL, + GHOSTTY_KEY_NUMPAD_DIVIDE, + GHOSTTY_KEY_NUMPAD_ENTER, + GHOSTTY_KEY_NUMPAD_EQUAL, + GHOSTTY_KEY_NUMPAD_MEMORY_ADD, + GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR, + GHOSTTY_KEY_NUMPAD_MEMORY_RECALL, + GHOSTTY_KEY_NUMPAD_MEMORY_STORE, + GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT, + GHOSTTY_KEY_NUMPAD_MULTIPLY, + GHOSTTY_KEY_NUMPAD_PAREN_LEFT, + GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, + GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_UP, + GHOSTTY_KEY_NUMPAD_DOWN, + GHOSTTY_KEY_NUMPAD_RIGHT, + GHOSTTY_KEY_NUMPAD_LEFT, + GHOSTTY_KEY_NUMPAD_BEGIN, + GHOSTTY_KEY_NUMPAD_HOME, + GHOSTTY_KEY_NUMPAD_END, + GHOSTTY_KEY_NUMPAD_INSERT, + GHOSTTY_KEY_NUMPAD_DELETE, + GHOSTTY_KEY_NUMPAD_PAGE_UP, + GHOSTTY_KEY_NUMPAD_PAGE_DOWN, + + // "Function Section" § 3.5 + GHOSTTY_KEY_ESCAPE, GHOSTTY_KEY_F1, GHOSTTY_KEY_F2, GHOSTTY_KEY_F3, @@ -208,50 +261,35 @@ typedef enum { GHOSTTY_KEY_F23, GHOSTTY_KEY_F24, GHOSTTY_KEY_F25, + GHOSTTY_KEY_FN, + GHOSTTY_KEY_FN_LOCK, + GHOSTTY_KEY_PRINT_SCREEN, + GHOSTTY_KEY_SCROLL_LOCK, + GHOSTTY_KEY_PAUSE, - // keypad - GHOSTTY_KEY_KP_0, - GHOSTTY_KEY_KP_1, - GHOSTTY_KEY_KP_2, - GHOSTTY_KEY_KP_3, - GHOSTTY_KEY_KP_4, - GHOSTTY_KEY_KP_5, - GHOSTTY_KEY_KP_6, - GHOSTTY_KEY_KP_7, - GHOSTTY_KEY_KP_8, - GHOSTTY_KEY_KP_9, - GHOSTTY_KEY_KP_DECIMAL, - GHOSTTY_KEY_KP_DIVIDE, - GHOSTTY_KEY_KP_MULTIPLY, - GHOSTTY_KEY_KP_SUBTRACT, - GHOSTTY_KEY_KP_ADD, - GHOSTTY_KEY_KP_ENTER, - GHOSTTY_KEY_KP_EQUAL, - GHOSTTY_KEY_KP_SEPARATOR, - GHOSTTY_KEY_KP_LEFT, - GHOSTTY_KEY_KP_RIGHT, - GHOSTTY_KEY_KP_UP, - GHOSTTY_KEY_KP_DOWN, - GHOSTTY_KEY_KP_PAGE_UP, - GHOSTTY_KEY_KP_PAGE_DOWN, - GHOSTTY_KEY_KP_HOME, - GHOSTTY_KEY_KP_END, - GHOSTTY_KEY_KP_INSERT, - GHOSTTY_KEY_KP_DELETE, - GHOSTTY_KEY_KP_BEGIN, - - // special keys - GHOSTTY_KEY_CONTEXT_MENU, - - // modifiers - GHOSTTY_KEY_LEFT_SHIFT, - GHOSTTY_KEY_LEFT_CONTROL, - GHOSTTY_KEY_LEFT_ALT, - GHOSTTY_KEY_LEFT_SUPER, - GHOSTTY_KEY_RIGHT_SHIFT, - GHOSTTY_KEY_RIGHT_CONTROL, - GHOSTTY_KEY_RIGHT_ALT, - GHOSTTY_KEY_RIGHT_SUPER, + // "Media Keys" § 3.6 + GHOSTTY_KEY_BROWSER_BACK, + GHOSTTY_KEY_BROWSER_FAVORITES, + GHOSTTY_KEY_BROWSER_FORWARD, + GHOSTTY_KEY_BROWSER_HOME, + GHOSTTY_KEY_BROWSER_REFRESH, + GHOSTTY_KEY_BROWSER_SEARCH, + GHOSTTY_KEY_BROWSER_STOP, + GHOSTTY_KEY_EJECT, + GHOSTTY_KEY_LAUNCH_APP_1, + GHOSTTY_KEY_LAUNCH_APP_2, + GHOSTTY_KEY_LAUNCH_MAIL, + GHOSTTY_KEY_MEDIA_PLAY_PAUSE, + GHOSTTY_KEY_MEDIA_SELECT, + GHOSTTY_KEY_MEDIA_STOP, + GHOSTTY_KEY_MEDIA_TRACK_NEXT, + GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS, + GHOSTTY_KEY_POWER, + GHOSTTY_KEY_SLEEP, + GHOSTTY_KEY_AUDIO_VOLUME_DOWN, + GHOSTTY_KEY_AUDIO_VOLUME_MUTE, + GHOSTTY_KEY_AUDIO_VOLUME_UP, + GHOSTTY_KEY_WAKE_UP, } ghostty_input_key_e; typedef struct { @@ -265,7 +303,6 @@ typedef struct { } ghostty_input_key_s; typedef enum { - GHOSTTY_TRIGGER_TRANSLATED, GHOSTTY_TRIGGER_PHYSICAL, GHOSTTY_TRIGGER_UNICODE, } ghostty_input_trigger_tag_e; diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 0be579122..942ca5973 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -5,12 +5,6 @@ import GhosttyKit extension Ghostty { // MARK: Keyboard Shortcuts - /// Returns the SwiftUI KeyEquivalent for a given key. Note that not all keys known by - /// Ghostty have a macOS equivalent since macOS doesn't allow all keys as equivalents. - static func keyEquivalent(key: ghostty_input_key_e) -> KeyEquivalent? { - return Self.keyToEquivalent[key] - } - /// Return the key equivalent for the given trigger. /// /// Returns nil if the trigger doesn't have an equivalent KeyboardShortcut. This is possible @@ -22,16 +16,11 @@ extension Ghostty { static func keyboardShortcut(for trigger: ghostty_input_trigger_s) -> KeyboardShortcut? { let key: KeyEquivalent switch (trigger.tag) { - case GHOSTTY_TRIGGER_TRANSLATED: - if let v = Ghostty.keyEquivalent(key: trigger.key.translated) { - key = v - } else { - return nil - } - case GHOSTTY_TRIGGER_PHYSICAL: - if let v = Ghostty.keyEquivalent(key: trigger.key.physical) { - key = v + // Only functional keys can be converted to a KeyboardShortcut. Other physical + // mappings cannot because KeyboardShortcut in Swift is inherently layout-dependent. + if let equiv = Self.keyToEquivalent[trigger.key.physical] { + key = equiv } else { return nil } @@ -86,64 +75,11 @@ extension Ghostty { /// not all ghostty key enum values are represented here because not all of them can be /// mapped to a KeyEquivalent. static let keyToEquivalent: [ghostty_input_key_e : KeyEquivalent] = [ - // 0-9 - GHOSTTY_KEY_ZERO: "0", - GHOSTTY_KEY_ONE: "1", - GHOSTTY_KEY_TWO: "2", - GHOSTTY_KEY_THREE: "3", - GHOSTTY_KEY_FOUR: "4", - GHOSTTY_KEY_FIVE: "5", - GHOSTTY_KEY_SIX: "6", - GHOSTTY_KEY_SEVEN: "7", - GHOSTTY_KEY_EIGHT: "8", - GHOSTTY_KEY_NINE: "9", - - // a-z - GHOSTTY_KEY_A: "a", - GHOSTTY_KEY_B: "b", - GHOSTTY_KEY_C: "c", - GHOSTTY_KEY_D: "d", - GHOSTTY_KEY_E: "e", - GHOSTTY_KEY_F: "f", - GHOSTTY_KEY_G: "g", - GHOSTTY_KEY_H: "h", - GHOSTTY_KEY_I: "i", - GHOSTTY_KEY_J: "j", - GHOSTTY_KEY_K: "k", - GHOSTTY_KEY_L: "l", - GHOSTTY_KEY_M: "m", - GHOSTTY_KEY_N: "n", - GHOSTTY_KEY_O: "o", - GHOSTTY_KEY_P: "p", - GHOSTTY_KEY_Q: "q", - GHOSTTY_KEY_R: "r", - GHOSTTY_KEY_S: "s", - GHOSTTY_KEY_T: "t", - GHOSTTY_KEY_U: "u", - GHOSTTY_KEY_V: "v", - GHOSTTY_KEY_W: "w", - GHOSTTY_KEY_X: "x", - GHOSTTY_KEY_Y: "y", - GHOSTTY_KEY_Z: "z", - - // Symbols - GHOSTTY_KEY_APOSTROPHE: "'", - GHOSTTY_KEY_BACKSLASH: "\\", - GHOSTTY_KEY_COMMA: ",", - GHOSTTY_KEY_EQUAL: "=", - GHOSTTY_KEY_GRAVE_ACCENT: "`", - GHOSTTY_KEY_LEFT_BRACKET: "[", - GHOSTTY_KEY_MINUS: "-", - GHOSTTY_KEY_PERIOD: ".", - GHOSTTY_KEY_RIGHT_BRACKET: "]", - GHOSTTY_KEY_SEMICOLON: ";", - GHOSTTY_KEY_SLASH: "/", - // Function keys - GHOSTTY_KEY_UP: .upArrow, - GHOSTTY_KEY_DOWN: .downArrow, - GHOSTTY_KEY_LEFT: .leftArrow, - GHOSTTY_KEY_RIGHT: .rightArrow, + GHOSTTY_KEY_ARROW_UP: .upArrow, + GHOSTTY_KEY_ARROW_DOWN: .downArrow, + GHOSTTY_KEY_ARROW_LEFT: .leftArrow, + GHOSTTY_KEY_ARROW_RIGHT: .rightArrow, GHOSTTY_KEY_HOME: .home, GHOSTTY_KEY_END: .end, GHOSTTY_KEY_DELETE: .delete, @@ -153,104 +89,22 @@ extension Ghostty { GHOSTTY_KEY_ENTER: .return, GHOSTTY_KEY_TAB: .tab, GHOSTTY_KEY_BACKSPACE: .delete, - ] - - static let asciiToKey: [UInt8 : ghostty_input_key_e] = [ - // 0-9 - 0x30: GHOSTTY_KEY_ZERO, - 0x31: GHOSTTY_KEY_ONE, - 0x32: GHOSTTY_KEY_TWO, - 0x33: GHOSTTY_KEY_THREE, - 0x34: GHOSTTY_KEY_FOUR, - 0x35: GHOSTTY_KEY_FIVE, - 0x36: GHOSTTY_KEY_SIX, - 0x37: GHOSTTY_KEY_SEVEN, - 0x38: GHOSTTY_KEY_EIGHT, - 0x39: GHOSTTY_KEY_NINE, - - // A-Z - 0x41: GHOSTTY_KEY_A, - 0x42: GHOSTTY_KEY_B, - 0x43: GHOSTTY_KEY_C, - 0x44: GHOSTTY_KEY_D, - 0x45: GHOSTTY_KEY_E, - 0x46: GHOSTTY_KEY_F, - 0x47: GHOSTTY_KEY_G, - 0x48: GHOSTTY_KEY_H, - 0x49: GHOSTTY_KEY_I, - 0x4A: GHOSTTY_KEY_J, - 0x4B: GHOSTTY_KEY_K, - 0x4C: GHOSTTY_KEY_L, - 0x4D: GHOSTTY_KEY_M, - 0x4E: GHOSTTY_KEY_N, - 0x4F: GHOSTTY_KEY_O, - 0x50: GHOSTTY_KEY_P, - 0x51: GHOSTTY_KEY_Q, - 0x52: GHOSTTY_KEY_R, - 0x53: GHOSTTY_KEY_S, - 0x54: GHOSTTY_KEY_T, - 0x55: GHOSTTY_KEY_U, - 0x56: GHOSTTY_KEY_V, - 0x57: GHOSTTY_KEY_W, - 0x58: GHOSTTY_KEY_X, - 0x59: GHOSTTY_KEY_Y, - 0x5A: GHOSTTY_KEY_Z, - - // a-z - 0x61: GHOSTTY_KEY_A, - 0x62: GHOSTTY_KEY_B, - 0x63: GHOSTTY_KEY_C, - 0x64: GHOSTTY_KEY_D, - 0x65: GHOSTTY_KEY_E, - 0x66: GHOSTTY_KEY_F, - 0x67: GHOSTTY_KEY_G, - 0x68: GHOSTTY_KEY_H, - 0x69: GHOSTTY_KEY_I, - 0x6A: GHOSTTY_KEY_J, - 0x6B: GHOSTTY_KEY_K, - 0x6C: GHOSTTY_KEY_L, - 0x6D: GHOSTTY_KEY_M, - 0x6E: GHOSTTY_KEY_N, - 0x6F: GHOSTTY_KEY_O, - 0x70: GHOSTTY_KEY_P, - 0x71: GHOSTTY_KEY_Q, - 0x72: GHOSTTY_KEY_R, - 0x73: GHOSTTY_KEY_S, - 0x74: GHOSTTY_KEY_T, - 0x75: GHOSTTY_KEY_U, - 0x76: GHOSTTY_KEY_V, - 0x77: GHOSTTY_KEY_W, - 0x78: GHOSTTY_KEY_X, - 0x79: GHOSTTY_KEY_Y, - 0x7A: GHOSTTY_KEY_Z, - - // Symbols - 0x27: GHOSTTY_KEY_APOSTROPHE, - 0x5C: GHOSTTY_KEY_BACKSLASH, - 0x2C: GHOSTTY_KEY_COMMA, - 0x3D: GHOSTTY_KEY_EQUAL, - 0x60: GHOSTTY_KEY_GRAVE_ACCENT, - 0x5B: GHOSTTY_KEY_LEFT_BRACKET, - 0x2D: GHOSTTY_KEY_MINUS, - 0x2E: GHOSTTY_KEY_PERIOD, - 0x5D: GHOSTTY_KEY_RIGHT_BRACKET, - 0x3B: GHOSTTY_KEY_SEMICOLON, - 0x2F: GHOSTTY_KEY_SLASH, + GHOSTTY_KEY_SPACE: .space, ] // Mapping of event keyCode to ghostty input key values. This is cribbed from // glfw mostly since we started as a glfw-based app way back in the day! static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [ - 0x1D: GHOSTTY_KEY_ZERO, - 0x12: GHOSTTY_KEY_ONE, - 0x13: GHOSTTY_KEY_TWO, - 0x14: GHOSTTY_KEY_THREE, - 0x15: GHOSTTY_KEY_FOUR, - 0x17: GHOSTTY_KEY_FIVE, - 0x16: GHOSTTY_KEY_SIX, - 0x1A: GHOSTTY_KEY_SEVEN, - 0x1C: GHOSTTY_KEY_EIGHT, - 0x19: GHOSTTY_KEY_NINE, + 0x1D: GHOSTTY_KEY_DIGIT_0, + 0x12: GHOSTTY_KEY_DIGIT_1, + 0x13: GHOSTTY_KEY_DIGIT_2, + 0x14: GHOSTTY_KEY_DIGIT_3, + 0x15: GHOSTTY_KEY_DIGIT_4, + 0x17: GHOSTTY_KEY_DIGIT_5, + 0x16: GHOSTTY_KEY_DIGIT_6, + 0x1A: GHOSTTY_KEY_DIGIT_7, + 0x1C: GHOSTTY_KEY_DIGIT_8, + 0x19: GHOSTTY_KEY_DIGIT_9, 0x00: GHOSTTY_KEY_A, 0x0B: GHOSTTY_KEY_B, 0x08: GHOSTTY_KEY_C, @@ -278,22 +132,22 @@ extension Ghostty { 0x10: GHOSTTY_KEY_Y, 0x06: GHOSTTY_KEY_Z, - 0x27: GHOSTTY_KEY_APOSTROPHE, + 0x27: GHOSTTY_KEY_QUOTE, 0x2A: GHOSTTY_KEY_BACKSLASH, 0x2B: GHOSTTY_KEY_COMMA, 0x18: GHOSTTY_KEY_EQUAL, - 0x32: GHOSTTY_KEY_GRAVE_ACCENT, - 0x21: GHOSTTY_KEY_LEFT_BRACKET, + 0x32: GHOSTTY_KEY_BACKQUOTE, + 0x21: GHOSTTY_KEY_BRACKET_LEFT, 0x1B: GHOSTTY_KEY_MINUS, 0x2F: GHOSTTY_KEY_PERIOD, - 0x1E: GHOSTTY_KEY_RIGHT_BRACKET, + 0x1E: GHOSTTY_KEY_BRACKET_RIGHT, 0x29: GHOSTTY_KEY_SEMICOLON, 0x2C: GHOSTTY_KEY_SLASH, 0x33: GHOSTTY_KEY_BACKSPACE, 0x39: GHOSTTY_KEY_CAPS_LOCK, 0x75: GHOSTTY_KEY_DELETE, - 0x7D: GHOSTTY_KEY_DOWN, + 0x7D: GHOSTTY_KEY_ARROW_DOWN, 0x77: GHOSTTY_KEY_END, 0x24: GHOSTTY_KEY_ENTER, 0x35: GHOSTTY_KEY_ESCAPE, @@ -319,39 +173,39 @@ extension Ghostty { 0x5A: GHOSTTY_KEY_F20, 0x73: GHOSTTY_KEY_HOME, 0x72: GHOSTTY_KEY_INSERT, - 0x7B: GHOSTTY_KEY_LEFT, - 0x3A: GHOSTTY_KEY_LEFT_ALT, - 0x3B: GHOSTTY_KEY_LEFT_CONTROL, - 0x38: GHOSTTY_KEY_LEFT_SHIFT, - 0x37: GHOSTTY_KEY_LEFT_SUPER, + 0x7B: GHOSTTY_KEY_ARROW_LEFT, + 0x3A: GHOSTTY_KEY_ALT_LEFT, + 0x3B: GHOSTTY_KEY_CONTROL_LEFT, + 0x38: GHOSTTY_KEY_SHIFT_LEFT, + 0x37: GHOSTTY_KEY_META_LEFT, 0x47: GHOSTTY_KEY_NUM_LOCK, 0x79: GHOSTTY_KEY_PAGE_DOWN, 0x74: GHOSTTY_KEY_PAGE_UP, - 0x7C: GHOSTTY_KEY_RIGHT, - 0x3D: GHOSTTY_KEY_RIGHT_ALT, - 0x3E: GHOSTTY_KEY_RIGHT_CONTROL, - 0x3C: GHOSTTY_KEY_RIGHT_SHIFT, - 0x36: GHOSTTY_KEY_RIGHT_SUPER, + 0x7C: GHOSTTY_KEY_ARROW_RIGHT, + 0x3D: GHOSTTY_KEY_ALT_RIGHT, + 0x3E: GHOSTTY_KEY_CONTROL_RIGHT, + 0x3C: GHOSTTY_KEY_SHIFT_RIGHT, + 0x36: GHOSTTY_KEY_META_RIGHT, 0x31: GHOSTTY_KEY_SPACE, 0x30: GHOSTTY_KEY_TAB, - 0x7E: GHOSTTY_KEY_UP, + 0x7E: GHOSTTY_KEY_ARROW_UP, - 0x52: GHOSTTY_KEY_KP_0, - 0x53: GHOSTTY_KEY_KP_1, - 0x54: GHOSTTY_KEY_KP_2, - 0x55: GHOSTTY_KEY_KP_3, - 0x56: GHOSTTY_KEY_KP_4, - 0x57: GHOSTTY_KEY_KP_5, - 0x58: GHOSTTY_KEY_KP_6, - 0x59: GHOSTTY_KEY_KP_7, - 0x5B: GHOSTTY_KEY_KP_8, - 0x5C: GHOSTTY_KEY_KP_9, - 0x45: GHOSTTY_KEY_KP_ADD, - 0x41: GHOSTTY_KEY_KP_DECIMAL, - 0x4B: GHOSTTY_KEY_KP_DIVIDE, - 0x4C: GHOSTTY_KEY_KP_ENTER, - 0x51: GHOSTTY_KEY_KP_EQUAL, - 0x43: GHOSTTY_KEY_KP_MULTIPLY, - 0x4E: GHOSTTY_KEY_KP_SUBTRACT, + 0x52: GHOSTTY_KEY_NUMPAD_0, + 0x53: GHOSTTY_KEY_NUMPAD_1, + 0x54: GHOSTTY_KEY_NUMPAD_2, + 0x55: GHOSTTY_KEY_NUMPAD_3, + 0x56: GHOSTTY_KEY_NUMPAD_4, + 0x57: GHOSTTY_KEY_NUMPAD_5, + 0x58: GHOSTTY_KEY_NUMPAD_6, + 0x59: GHOSTTY_KEY_NUMPAD_7, + 0x5B: GHOSTTY_KEY_NUMPAD_8, + 0x5C: GHOSTTY_KEY_NUMPAD_9, + 0x45: GHOSTTY_KEY_NUMPAD_ADD, + 0x41: GHOSTTY_KEY_NUMPAD_DECIMAL, + 0x4B: GHOSTTY_KEY_NUMPAD_DIVIDE, + 0x4C: GHOSTTY_KEY_NUMPAD_ENTER, + 0x51: GHOSTTY_KEY_NUMPAD_EQUAL, + 0x43: GHOSTTY_KEY_NUMPAD_MULTIPLY, + 0x4E: GHOSTTY_KEY_NUMPAD_SUBTRACT, ]; } diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index c953300cd..cf3f73a55 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -92,41 +92,12 @@ pub const App = struct { // We want to get the physical unmapped key to process keybinds. const physical_key = keycode: for (input.keycodes.entries) |entry| { if (entry.native == self.keycode) break :keycode entry.key; - } else .invalid; - - // If the resulting text has length 1 then we can take its key - // and attempt to translate it to a key enum and call the key callback. - // If the length is greater than 1 then we're going to call the - // charCallback. - // - // We also only do key translation if this is not a dead key. - const key = if (!self.composing) key: { - // If our physical key is a keypad key, we use that. - if (physical_key.keypad()) break :key physical_key; - - // A completed key. If the length of the key is one then we can - // attempt to translate it to a key enum and call the key - // callback. First try plain ASCII. - if (text.len > 0) { - if (input.Key.fromASCII(text[0])) |key| { - break :key key; - } - } - - // If the above doesn't work, we use the unmodified value. - if (std.math.cast(u8, unshifted_codepoint)) |ascii| { - if (input.Key.fromASCII(ascii)) |key| { - break :key key; - } - } - - break :key physical_key; - } else .invalid; + } else .unidentified; // Build our final key event return .{ .action = self.action, - .key = key, + .key = physical_key, .physical_key = physical_key, .mods = self.mods, .consumed_mods = self.consumed_mods, diff --git a/src/input/key.zig b/src/input/key.zig index b39c5e5d3..c0deea25c 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -270,7 +270,7 @@ pub const Action = enum(c_int) { /// UTF-8 and are produced by the associated apprt. Ghostty core has /// no mechanism to map input events to strings without the apprt. /// -/// IMPORTANT: Any changes here update include/ghostty.h +/// IMPORTANT: Any changes here update include/ghostty.h ghostty_input_key_e pub const Key = enum(c_int) { unidentified, @@ -618,61 +618,61 @@ pub const Key = enum(c_int) { /// Returns the cimgui key constant for this key. pub fn imguiKey(self: Key) ?c_uint { return switch (self) { - .a => cimgui.c.ImGuiKey_A, - .b => cimgui.c.ImGuiKey_B, - .c => cimgui.c.ImGuiKey_C, - .d => cimgui.c.ImGuiKey_D, - .e => cimgui.c.ImGuiKey_E, - .f => cimgui.c.ImGuiKey_F, - .g => cimgui.c.ImGuiKey_G, - .h => cimgui.c.ImGuiKey_H, - .i => cimgui.c.ImGuiKey_I, - .j => cimgui.c.ImGuiKey_J, - .k => cimgui.c.ImGuiKey_K, - .l => cimgui.c.ImGuiKey_L, - .m => cimgui.c.ImGuiKey_M, - .n => cimgui.c.ImGuiKey_N, - .o => cimgui.c.ImGuiKey_O, - .p => cimgui.c.ImGuiKey_P, - .q => cimgui.c.ImGuiKey_Q, - .r => cimgui.c.ImGuiKey_R, - .s => cimgui.c.ImGuiKey_S, - .t => cimgui.c.ImGuiKey_T, - .u => cimgui.c.ImGuiKey_U, - .v => cimgui.c.ImGuiKey_V, - .w => cimgui.c.ImGuiKey_W, - .x => cimgui.c.ImGuiKey_X, - .y => cimgui.c.ImGuiKey_Y, - .z => cimgui.c.ImGuiKey_Z, + .key_a => cimgui.c.ImGuiKey_A, + .key_b => cimgui.c.ImGuiKey_B, + .key_c => cimgui.c.ImGuiKey_C, + .key_d => cimgui.c.ImGuiKey_D, + .key_e => cimgui.c.ImGuiKey_E, + .key_f => cimgui.c.ImGuiKey_F, + .key_g => cimgui.c.ImGuiKey_G, + .key_h => cimgui.c.ImGuiKey_H, + .key_i => cimgui.c.ImGuiKey_I, + .key_j => cimgui.c.ImGuiKey_J, + .key_k => cimgui.c.ImGuiKey_K, + .key_l => cimgui.c.ImGuiKey_L, + .key_m => cimgui.c.ImGuiKey_M, + .key_n => cimgui.c.ImGuiKey_N, + .key_o => cimgui.c.ImGuiKey_O, + .key_p => cimgui.c.ImGuiKey_P, + .key_q => cimgui.c.ImGuiKey_Q, + .key_r => cimgui.c.ImGuiKey_R, + .key_s => cimgui.c.ImGuiKey_S, + .key_t => cimgui.c.ImGuiKey_T, + .key_u => cimgui.c.ImGuiKey_U, + .key_v => cimgui.c.ImGuiKey_V, + .key_w => cimgui.c.ImGuiKey_W, + .key_x => cimgui.c.ImGuiKey_X, + .key_y => cimgui.c.ImGuiKey_Y, + .key_z => cimgui.c.ImGuiKey_Z, - .zero => cimgui.c.ImGuiKey_0, - .one => cimgui.c.ImGuiKey_1, - .two => cimgui.c.ImGuiKey_2, - .three => cimgui.c.ImGuiKey_3, - .four => cimgui.c.ImGuiKey_4, - .five => cimgui.c.ImGuiKey_5, - .six => cimgui.c.ImGuiKey_6, - .seven => cimgui.c.ImGuiKey_7, - .eight => cimgui.c.ImGuiKey_8, - .nine => cimgui.c.ImGuiKey_9, + .digit_0 => cimgui.c.ImGuiKey_0, + .digit_1 => cimgui.c.ImGuiKey_1, + .digit_2 => cimgui.c.ImGuiKey_2, + .digit_3 => cimgui.c.ImGuiKey_3, + .digit_4 => cimgui.c.ImGuiKey_4, + .digit_5 => cimgui.c.ImGuiKey_5, + .digit_6 => cimgui.c.ImGuiKey_6, + .digit_7 => cimgui.c.ImGuiKey_7, + .digit_8 => cimgui.c.ImGuiKey_8, + .digit_9 => cimgui.c.ImGuiKey_9, .semicolon => cimgui.c.ImGuiKey_Semicolon, .space => cimgui.c.ImGuiKey_Space, - .apostrophe => cimgui.c.ImGuiKey_Apostrophe, + .quote => cimgui.c.ImGuiKey_Apostrophe, .comma => cimgui.c.ImGuiKey_Comma, - .grave_accent => cimgui.c.ImGuiKey_GraveAccent, + .backquote => cimgui.c.ImGuiKey_GraveAccent, .period => cimgui.c.ImGuiKey_Period, .slash => cimgui.c.ImGuiKey_Slash, .minus => cimgui.c.ImGuiKey_Minus, .equal => cimgui.c.ImGuiKey_Equal, - .left_bracket => cimgui.c.ImGuiKey_LeftBracket, - .right_bracket => cimgui.c.ImGuiKey_RightBracket, + .bracket_left => cimgui.c.ImGuiKey_LeftBracket, + .bracket_right => cimgui.c.ImGuiKey_RightBracket, .backslash => cimgui.c.ImGuiKey_Backslash, - .up => cimgui.c.ImGuiKey_UpArrow, - .down => cimgui.c.ImGuiKey_DownArrow, - .left => cimgui.c.ImGuiKey_LeftArrow, - .right => cimgui.c.ImGuiKey_RightArrow, + .arrow_up => cimgui.c.ImGuiKey_UpArrow, + .arrow_down => cimgui.c.ImGuiKey_DownArrow, + .arrow_left => cimgui.c.ImGuiKey_LeftArrow, + .arrow_right => cimgui.c.ImGuiKey_RightArrow, .home => cimgui.c.ImGuiKey_Home, .end => cimgui.c.ImGuiKey_End, .insert => cimgui.c.ImGuiKey_Insert, @@ -703,48 +703,47 @@ pub const Key = enum(c_int) { .f11 => cimgui.c.ImGuiKey_F11, .f12 => cimgui.c.ImGuiKey_F12, - .kp_0 => cimgui.c.ImGuiKey_Keypad0, - .kp_1 => cimgui.c.ImGuiKey_Keypad1, - .kp_2 => cimgui.c.ImGuiKey_Keypad2, - .kp_3 => cimgui.c.ImGuiKey_Keypad3, - .kp_4 => cimgui.c.ImGuiKey_Keypad4, - .kp_5 => cimgui.c.ImGuiKey_Keypad5, - .kp_6 => cimgui.c.ImGuiKey_Keypad6, - .kp_7 => cimgui.c.ImGuiKey_Keypad7, - .kp_8 => cimgui.c.ImGuiKey_Keypad8, - .kp_9 => cimgui.c.ImGuiKey_Keypad9, - .kp_decimal => cimgui.c.ImGuiKey_KeypadDecimal, - .kp_divide => cimgui.c.ImGuiKey_KeypadDivide, - .kp_multiply => cimgui.c.ImGuiKey_KeypadMultiply, - .kp_subtract => cimgui.c.ImGuiKey_KeypadSubtract, - .kp_add => cimgui.c.ImGuiKey_KeypadAdd, - .kp_enter => cimgui.c.ImGuiKey_KeypadEnter, - .kp_equal => cimgui.c.ImGuiKey_KeypadEqual, + .numpad_0 => cimgui.c.ImGuiKey_Keypad0, + .numpad_1 => cimgui.c.ImGuiKey_Keypad1, + .numpad_2 => cimgui.c.ImGuiKey_Keypad2, + .numpad_3 => cimgui.c.ImGuiKey_Keypad3, + .numpad_4 => cimgui.c.ImGuiKey_Keypad4, + .numpad_5 => cimgui.c.ImGuiKey_Keypad5, + .numpad_6 => cimgui.c.ImGuiKey_Keypad6, + .numpad_7 => cimgui.c.ImGuiKey_Keypad7, + .numpad_8 => cimgui.c.ImGuiKey_Keypad8, + .numpad_9 => cimgui.c.ImGuiKey_Keypad9, + .numpad_decimal => cimgui.c.ImGuiKey_KeypadDecimal, + .numpad_divide => cimgui.c.ImGuiKey_KeypadDivide, + .numpad_multiply => cimgui.c.ImGuiKey_KeypadMultiply, + .numpad_subtract => cimgui.c.ImGuiKey_KeypadSubtract, + .numpad_add => cimgui.c.ImGuiKey_KeypadAdd, + .numpad_enter => cimgui.c.ImGuiKey_KeypadEnter, + .numpad_equal => cimgui.c.ImGuiKey_KeypadEqual, // We map KP_SEPARATOR to Comma because traditionally a numpad would // have a numeric separator key. Most modern numpads do not - .kp_separator => cimgui.c.ImGuiKey_Comma, - .kp_left => cimgui.c.ImGuiKey_LeftArrow, - .kp_right => cimgui.c.ImGuiKey_RightArrow, - .kp_up => cimgui.c.ImGuiKey_UpArrow, - .kp_down => cimgui.c.ImGuiKey_DownArrow, - .kp_page_up => cimgui.c.ImGuiKey_PageUp, - .kp_page_down => cimgui.c.ImGuiKey_PageUp, - .kp_home => cimgui.c.ImGuiKey_Home, - .kp_end => cimgui.c.ImGuiKey_End, - .kp_insert => cimgui.c.ImGuiKey_Insert, - .kp_delete => cimgui.c.ImGuiKey_Delete, - .kp_begin => cimgui.c.ImGuiKey_NamedKey_BEGIN, + .numpad_left => cimgui.c.ImGuiKey_LeftArrow, + .numpad_right => cimgui.c.ImGuiKey_RightArrow, + .numpad_up => cimgui.c.ImGuiKey_UpArrow, + .numpad_down => cimgui.c.ImGuiKey_DownArrow, + .numpad_page_up => cimgui.c.ImGuiKey_PageUp, + .numpad_page_down => cimgui.c.ImGuiKey_PageUp, + .numpad_home => cimgui.c.ImGuiKey_Home, + .numpad_end => cimgui.c.ImGuiKey_End, + .numpad_insert => cimgui.c.ImGuiKey_Insert, + .numpad_delete => cimgui.c.ImGuiKey_Delete, + .numpad_begin => cimgui.c.ImGuiKey_NamedKey_BEGIN, - .left_shift => cimgui.c.ImGuiKey_LeftShift, - .left_control => cimgui.c.ImGuiKey_LeftCtrl, - .left_alt => cimgui.c.ImGuiKey_LeftAlt, - .left_super => cimgui.c.ImGuiKey_LeftSuper, - .right_shift => cimgui.c.ImGuiKey_RightShift, - .right_control => cimgui.c.ImGuiKey_RightCtrl, - .right_alt => cimgui.c.ImGuiKey_RightAlt, - .right_super => cimgui.c.ImGuiKey_RightSuper, + .shift_left => cimgui.c.ImGuiKey_LeftShift, + .control_left => cimgui.c.ImGuiKey_LeftCtrl, + .alt_left => cimgui.c.ImGuiKey_LeftAlt, + .meta_left => cimgui.c.ImGuiKey_LeftSuper, + .shift_right => cimgui.c.ImGuiKey_RightShift, + .control_right => cimgui.c.ImGuiKey_RightCtrl, + .alt_right => cimgui.c.ImGuiKey_RightAlt, + .meta_right => cimgui.c.ImGuiKey_RightSuper, - .invalid, + // These keys aren't represented in cimgui .f13, .f14, .f15, @@ -758,9 +757,51 @@ pub const Key = enum(c_int) { .f23, .f24, .f25, + .intl_backslash, + .intl_ro, + .intl_yen, + .convert, + .kana_mode, + .non_convert, + .numpad_backspace, + .numpad_clear, + .numpad_clear_entry, + .numpad_comma, + .numpad_memory_add, + .numpad_memory_clear, + .numpad_memory_recall, + .numpad_memory_store, + .numpad_memory_subtract, + .numpad_paren_left, + .numpad_paren_right, + .@"fn", + .fn_lock, + .browser_back, + .browser_favorites, + .browser_forward, + .browser_home, + .browser_refresh, + .browser_search, + .browser_stop, + .eject, + .launch_app_1, + .launch_app_2, + .launch_mail, + .media_play_pause, + .media_select, + .media_stop, + .media_track_next, + .media_track_previous, + .power, + .sleep, + .audio_volume_down, + .audio_volume_mute, + .audio_volume_up, + .wake_up, + .help, + => null, - // These keys aren't represented in cimgui - .plus, + .unidentified, => null, }; } diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig index e9adbc156..fa95ff206 100644 --- a/src/input/keycodes.zig +++ b/src/input/keycodes.zig @@ -19,7 +19,7 @@ pub const entries: []const Entry = entries: { for (raw_entries, 0..) |raw, i| { @setEvalBranchQuota(10000); result[i] = .{ - .key = code_to_key.get(raw[5]) orelse .invalid, + .key = code_to_key.get(raw[5]) orelse .unidentified, .usb = raw[0], .code = raw[5], .native = raw[native_idx], @@ -45,42 +45,42 @@ pub const Entry = struct { const code_to_key = code_to_key: { @setEvalBranchQuota(5000); break :code_to_key std.StaticStringMap(Key).initComptime(.{ - .{ "KeyA", .a }, - .{ "KeyB", .b }, - .{ "KeyC", .c }, - .{ "KeyD", .d }, - .{ "KeyE", .e }, - .{ "KeyF", .f }, - .{ "KeyG", .g }, - .{ "KeyH", .h }, - .{ "KeyI", .i }, - .{ "KeyJ", .j }, - .{ "KeyK", .k }, - .{ "KeyL", .l }, - .{ "KeyM", .m }, - .{ "KeyN", .n }, - .{ "KeyO", .o }, - .{ "KeyP", .p }, - .{ "KeyQ", .q }, - .{ "KeyR", .r }, - .{ "KeyS", .s }, - .{ "KeyT", .t }, - .{ "KeyU", .u }, - .{ "KeyV", .v }, - .{ "KeyW", .w }, - .{ "KeyX", .x }, - .{ "KeyY", .y }, - .{ "KeyZ", .z }, - .{ "Digit1", .one }, - .{ "Digit2", .two }, - .{ "Digit3", .three }, - .{ "Digit4", .four }, - .{ "Digit5", .five }, - .{ "Digit6", .six }, - .{ "Digit7", .seven }, - .{ "Digit8", .eight }, - .{ "Digit9", .nine }, - .{ "Digit0", .zero }, + .{ "KeyA", .key_a }, + .{ "KeyB", .key_b }, + .{ "KeyC", .key_c }, + .{ "KeyD", .key_d }, + .{ "KeyE", .key_e }, + .{ "KeyF", .key_f }, + .{ "KeyG", .key_g }, + .{ "KeyH", .key_h }, + .{ "KeyI", .key_i }, + .{ "KeyJ", .key_j }, + .{ "KeyK", .key_k }, + .{ "KeyL", .key_l }, + .{ "KeyM", .key_m }, + .{ "KeyN", .key_n }, + .{ "KeyO", .key_o }, + .{ "KeyP", .key_p }, + .{ "KeyQ", .key_q }, + .{ "KeyR", .key_r }, + .{ "KeyS", .key_s }, + .{ "KeyT", .key_t }, + .{ "KeyU", .key_u }, + .{ "KeyV", .key_v }, + .{ "KeyW", .key_w }, + .{ "KeyX", .key_x }, + .{ "KeyY", .key_y }, + .{ "KeyZ", .key_z }, + .{ "Digit1", .digit_1 }, + .{ "Digit2", .digit_2 }, + .{ "Digit3", .digit_3 }, + .{ "Digit4", .digit_4 }, + .{ "Digit5", .digit_5 }, + .{ "Digit6", .digit_6 }, + .{ "Digit7", .digit_7 }, + .{ "Digit8", .digit_8 }, + .{ "Digit9", .digit_9 }, + .{ "Digit0", .digit_0 }, .{ "Enter", .enter }, .{ "Escape", .escape }, .{ "Backspace", .backspace }, @@ -88,12 +88,12 @@ const code_to_key = code_to_key: { .{ "Space", .space }, .{ "Minus", .minus }, .{ "Equal", .equal }, - .{ "BracketLeft", .left_bracket }, - .{ "BracketRight", .right_bracket }, + .{ "BracketLeft", .bracket_left }, + .{ "BracketRight", .bracket_left }, .{ "Backslash", .backslash }, .{ "Semicolon", .semicolon }, - .{ "Quote", .apostrophe }, - .{ "Backquote", .grave_accent }, + .{ "Quote", .quote }, + .{ "Backquote", .backquote }, .{ "Comma", .comma }, .{ "Period", .period }, .{ "Slash", .slash }, @@ -131,37 +131,37 @@ const code_to_key = code_to_key: { .{ "Delete", .delete }, .{ "End", .end }, .{ "PageDown", .page_down }, - .{ "ArrowRight", .right }, - .{ "ArrowLeft", .left }, - .{ "ArrowDown", .down }, - .{ "ArrowUp", .up }, + .{ "ArrowRight", .arrow_right }, + .{ "ArrowLeft", .arrow_left }, + .{ "ArrowDown", .arrow_down }, + .{ "ArrowUp", .arrow_up }, .{ "NumLock", .num_lock }, - .{ "NumpadDivide", .kp_divide }, - .{ "NumpadMultiply", .kp_multiply }, - .{ "NumpadSubtract", .kp_subtract }, - .{ "NumpadAdd", .kp_add }, - .{ "NumpadEnter", .kp_enter }, - .{ "Numpad1", .kp_1 }, - .{ "Numpad2", .kp_2 }, - .{ "Numpad3", .kp_3 }, - .{ "Numpad4", .kp_4 }, - .{ "Numpad5", .kp_5 }, - .{ "Numpad6", .kp_6 }, - .{ "Numpad7", .kp_7 }, - .{ "Numpad8", .kp_8 }, - .{ "Numpad9", .kp_9 }, - .{ "Numpad0", .kp_0 }, - .{ "NumpadDecimal", .kp_decimal }, - .{ "NumpadEqual", .kp_equal }, + .{ "NumpadDivide", .numpad_divide }, + .{ "NumpadMultiply", .numpad_multiply }, + .{ "NumpadSubtract", .numpad_subtract }, + .{ "NumpadAdd", .numpad_add }, + .{ "NumpadEnter", .numpad_enter }, + .{ "Numpad1", .numpad_1 }, + .{ "Numpad2", .numpad_2 }, + .{ "Numpad3", .numpad_3 }, + .{ "Numpad4", .numpad_4 }, + .{ "Numpad5", .numpad_5 }, + .{ "Numpad6", .numpad_6 }, + .{ "Numpad7", .numpad_7 }, + .{ "Numpad8", .numpad_8 }, + .{ "Numpad9", .numpad_9 }, + .{ "Numpad0", .numpad_0 }, + .{ "NumpadDecimal", .numpad_decimal }, + .{ "NumpadEqual", .numpad_equal }, .{ "ContextMenu", .context_menu }, - .{ "ControlLeft", .left_control }, - .{ "ShiftLeft", .left_shift }, - .{ "AltLeft", .left_alt }, - .{ "MetaLeft", .left_super }, - .{ "ControlRight", .right_control }, - .{ "ShiftRight", .right_shift }, - .{ "AltRight", .right_alt }, - .{ "MetaRight", .right_super }, + .{ "ControlLeft", .control_left }, + .{ "ShiftLeft", .shift_left }, + .{ "AltLeft", .alt_left }, + .{ "MetaLeft", .meta_left }, + .{ "ControlRight", .control_right }, + .{ "ShiftRight", .shift_right }, + .{ "AltRight", .alt_right }, + .{ "MetaRight", .meta_right }, }); }; From ffdf86374a964dba28ca2998e914c0eece13a394 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 11:18:03 -0700 Subject: [PATCH 225/642] apprt/gtk: build --- include/ghostty.h | 1 + src/apprt/gtk/Surface.zig | 54 +---------- src/apprt/gtk/key.zig | 184 +++++++++++++++++++------------------- src/config/Config.zig | 43 ++++----- src/input/key.zig | 2 + 5 files changed, 118 insertions(+), 166 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 600396a84..2734fc368 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -222,6 +222,7 @@ typedef enum { GHOSTTY_KEY_NUMPAD_PAREN_LEFT, GHOSTTY_KEY_NUMPAD_PAREN_RIGHT, GHOSTTY_KEY_NUMPAD_SUBTRACT, + GHOSTTY_KEY_NUMPAD_SEPARATOR, GHOSTTY_KEY_NUMPAD_UP, GHOSTTY_KEY_NUMPAD_DOWN, GHOSTTY_KEY_NUMPAD_RIGHT, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 7ff96480e..a04372494 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1840,7 +1840,7 @@ pub fn keyEvent( // (These are keybinds explicitly marked as requesting physical mapping). const physical_key = keycode: for (input.keycodes.entries) |entry| { if (entry.native == keycode) break :keycode entry.key; - } else .invalid; + } else .unidentified; // Get our modifier for the event const mods: input.Mods = gtk_key.eventMods( @@ -1861,52 +1861,6 @@ pub fn keyEvent( break :consumed gtk_key.translateMods(@bitCast(masked)); }; - // If we're not in a dead key state, we want to translate our text - // to some input.Key. - const key = if (!self.im_composing) key: { - // First, try to convert the keyval directly to a key. This allows the - // use of key remapping and identification of keypad numerics (as - // opposed to their ASCII counterparts) - if (gtk_key.keyFromKeyval(keyval)) |key| { - break :key key; - } - - // A completed key. If the length of the key is one then we can - // attempt to translate it to a key enum and call the key - // callback. First try plain ASCII. - if (self.im_len > 0) { - if (input.Key.fromASCII(self.im_buf[0])) |key| { - break :key key; - } - } - - // If that doesn't work then we try to translate the kevval.. - if (keyval_unicode != 0) { - if (std.math.cast(u8, keyval_unicode)) |byte| { - if (input.Key.fromASCII(byte)) |key| { - break :key key; - } - } - } - - // If that doesn't work we use the unshifted value... - if (std.math.cast(u8, keyval_unicode_unshifted)) |ascii| { - if (input.Key.fromASCII(ascii)) |key| { - break :key key; - } - } - - // If we have im text then this is invalid. This means that - // the keypress generated some character that we don't know about - // in our key enum. We don't want to use the physical key because - // it can be simply wrong. For example on "Turkish Q" the "i" key - // on a US layout results in "ı" which is not the same as "i" so - // we shouldn't use the physical key. - if (self.im_len > 0 or keyval_unicode_unshifted != 0) break :key .invalid; - - break :key physical_key; - } else .invalid; - // log.debug("key pressed key={} keyval={x} physical_key={} composing={} text_len={} mods={}", .{ // key, // keyval, @@ -1936,7 +1890,7 @@ pub fn keyEvent( // Invoke the core Ghostty logic to handle this input. const effect = self.core_surface.keyCallback(.{ .action = action, - .key = key, + .key = physical_key, .physical_key = physical_key, .mods = mods, .consumed_mods = consumed_mods, @@ -2088,8 +2042,8 @@ fn gtkInputCommit( // invalid key, which should produce no PTY encoding). _ = self.core_surface.keyCallback(.{ .action = .press, - .key = .invalid, - .physical_key = .invalid, + .key = .unidentified, + .physical_key = .unidentified, .mods = .{}, .consumed_mods = .{}, .composing = false, diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 2e00552a6..b3330eb40 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -21,7 +21,7 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u // Write our key switch (trigger.key) { - .physical, .translated => |k| { + .physical => |k| { const keyval = keyvalFromKey(k) orelse return null; try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return null)); }, @@ -122,42 +122,42 @@ pub fn eventMods( // if only the modifier key is pressed, but our core logic // relies on it. switch (physical_key) { - .left_shift => { + .shift_left => { mods.shift = action != .release; mods.sides.shift = .left; }, - .right_shift => { + .shift_right => { mods.shift = action != .release; mods.sides.shift = .right; }, - .left_control => { + .control_left => { mods.ctrl = action != .release; mods.sides.ctrl = .left; }, - .right_control => { + .control_right => { mods.ctrl = action != .release; mods.sides.ctrl = .right; }, - .left_alt => { + .alt_left => { mods.alt = action != .release; mods.sides.alt = .left; }, - .right_alt => { + .alt_right => { mods.alt = action != .release; mods.sides.alt = .right; }, - .left_super => { + .meta_left => { mods.super = action != .release; mods.sides.super = .left; }, - .right_super => { + .meta_right => { mods.super = action != .release; mods.sides.super = .right; }, @@ -182,7 +182,7 @@ pub fn keyvalFromKey(key: input.Key) ?c_uint { switch (key) { inline else => |key_comptime| { return comptime value: { - @setEvalBranchQuota(10_000); + @setEvalBranchQuota(50_000); for (keymap) |entry| { if (entry[1] == key_comptime) break :value entry[0]; } @@ -199,7 +199,7 @@ test "accelFromTrigger" { try testing.expectEqualStrings("q", (try accelFromTrigger(&buf, .{ .mods = .{ .super = true }, - .key = .{ .translated = .q }, + .key = .{ .unicode = 'q' }, })).?); try testing.expectEqualStrings("backslash", (try accelFromTrigger(&buf, .{ @@ -213,61 +213,61 @@ test "accelFromTrigger" { const RawEntry = struct { c_uint, input.Key }; const keymap: []const RawEntry = &.{ - .{ gdk.KEY_a, .a }, - .{ gdk.KEY_b, .b }, - .{ gdk.KEY_c, .c }, - .{ gdk.KEY_d, .d }, - .{ gdk.KEY_e, .e }, - .{ gdk.KEY_f, .f }, - .{ gdk.KEY_g, .g }, - .{ gdk.KEY_h, .h }, - .{ gdk.KEY_i, .i }, - .{ gdk.KEY_j, .j }, - .{ gdk.KEY_k, .k }, - .{ gdk.KEY_l, .l }, - .{ gdk.KEY_m, .m }, - .{ gdk.KEY_n, .n }, - .{ gdk.KEY_o, .o }, - .{ gdk.KEY_p, .p }, - .{ gdk.KEY_q, .q }, - .{ gdk.KEY_r, .r }, - .{ gdk.KEY_s, .s }, - .{ gdk.KEY_t, .t }, - .{ gdk.KEY_u, .u }, - .{ gdk.KEY_v, .v }, - .{ gdk.KEY_w, .w }, - .{ gdk.KEY_x, .x }, - .{ gdk.KEY_y, .y }, - .{ gdk.KEY_z, .z }, + .{ gdk.KEY_a, .key_a }, + .{ gdk.KEY_b, .key_b }, + .{ gdk.KEY_c, .key_c }, + .{ gdk.KEY_d, .key_d }, + .{ gdk.KEY_e, .key_e }, + .{ gdk.KEY_f, .key_f }, + .{ gdk.KEY_g, .key_g }, + .{ gdk.KEY_h, .key_h }, + .{ gdk.KEY_i, .key_i }, + .{ gdk.KEY_j, .key_j }, + .{ gdk.KEY_k, .key_k }, + .{ gdk.KEY_l, .key_l }, + .{ gdk.KEY_m, .key_m }, + .{ gdk.KEY_n, .key_n }, + .{ gdk.KEY_o, .key_o }, + .{ gdk.KEY_p, .key_p }, + .{ gdk.KEY_q, .key_q }, + .{ gdk.KEY_r, .key_r }, + .{ gdk.KEY_s, .key_s }, + .{ gdk.KEY_t, .key_t }, + .{ gdk.KEY_u, .key_u }, + .{ gdk.KEY_v, .key_v }, + .{ gdk.KEY_w, .key_w }, + .{ gdk.KEY_x, .key_x }, + .{ gdk.KEY_y, .key_y }, + .{ gdk.KEY_z, .key_z }, - .{ gdk.KEY_0, .zero }, - .{ gdk.KEY_1, .one }, - .{ gdk.KEY_2, .two }, - .{ gdk.KEY_3, .three }, - .{ gdk.KEY_4, .four }, - .{ gdk.KEY_5, .five }, - .{ gdk.KEY_6, .six }, - .{ gdk.KEY_7, .seven }, - .{ gdk.KEY_8, .eight }, - .{ gdk.KEY_9, .nine }, + .{ gdk.KEY_0, .digit_0 }, + .{ gdk.KEY_1, .digit_1 }, + .{ gdk.KEY_2, .digit_2 }, + .{ gdk.KEY_3, .digit_3 }, + .{ gdk.KEY_4, .digit_4 }, + .{ gdk.KEY_5, .digit_5 }, + .{ gdk.KEY_6, .digit_6 }, + .{ gdk.KEY_7, .digit_7 }, + .{ gdk.KEY_8, .digit_8 }, + .{ gdk.KEY_9, .digit_9 }, .{ gdk.KEY_semicolon, .semicolon }, .{ gdk.KEY_space, .space }, - .{ gdk.KEY_apostrophe, .apostrophe }, + .{ gdk.KEY_apostrophe, .quote }, .{ gdk.KEY_comma, .comma }, - .{ gdk.KEY_grave, .grave_accent }, + .{ gdk.KEY_grave, .backquote }, .{ gdk.KEY_period, .period }, .{ gdk.KEY_slash, .slash }, .{ gdk.KEY_minus, .minus }, .{ gdk.KEY_equal, .equal }, - .{ gdk.KEY_bracketleft, .left_bracket }, - .{ gdk.KEY_bracketright, .right_bracket }, + .{ gdk.KEY_bracketleft, .bracket_left }, + .{ gdk.KEY_bracketright, .bracket_right }, .{ gdk.KEY_backslash, .backslash }, - .{ gdk.KEY_Up, .up }, - .{ gdk.KEY_Down, .down }, - .{ gdk.KEY_Right, .right }, - .{ gdk.KEY_Left, .left }, + .{ gdk.KEY_Up, .arrow_up }, + .{ gdk.KEY_Down, .arrow_down }, + .{ gdk.KEY_Right, .arrow_right }, + .{ gdk.KEY_Left, .arrow_left }, .{ gdk.KEY_Home, .home }, .{ gdk.KEY_End, .end }, .{ gdk.KEY_Insert, .insert }, @@ -310,45 +310,45 @@ const keymap: []const RawEntry = &.{ .{ gdk.KEY_F24, .f24 }, .{ gdk.KEY_F25, .f25 }, - .{ gdk.KEY_KP_0, .kp_0 }, - .{ gdk.KEY_KP_1, .kp_1 }, - .{ gdk.KEY_KP_2, .kp_2 }, - .{ gdk.KEY_KP_3, .kp_3 }, - .{ gdk.KEY_KP_4, .kp_4 }, - .{ gdk.KEY_KP_5, .kp_5 }, - .{ gdk.KEY_KP_6, .kp_6 }, - .{ gdk.KEY_KP_7, .kp_7 }, - .{ gdk.KEY_KP_8, .kp_8 }, - .{ gdk.KEY_KP_9, .kp_9 }, - .{ gdk.KEY_KP_Decimal, .kp_decimal }, - .{ gdk.KEY_KP_Divide, .kp_divide }, - .{ gdk.KEY_KP_Multiply, .kp_multiply }, - .{ gdk.KEY_KP_Subtract, .kp_subtract }, - .{ gdk.KEY_KP_Add, .kp_add }, - .{ gdk.KEY_KP_Enter, .kp_enter }, - .{ gdk.KEY_KP_Equal, .kp_equal }, + .{ gdk.KEY_KP_0, .numpad_0 }, + .{ gdk.KEY_KP_1, .numpad_1 }, + .{ gdk.KEY_KP_2, .numpad_2 }, + .{ gdk.KEY_KP_3, .numpad_3 }, + .{ gdk.KEY_KP_4, .numpad_4 }, + .{ gdk.KEY_KP_5, .numpad_5 }, + .{ gdk.KEY_KP_6, .numpad_6 }, + .{ gdk.KEY_KP_7, .numpad_7 }, + .{ gdk.KEY_KP_8, .numpad_8 }, + .{ gdk.KEY_KP_9, .numpad_9 }, + .{ gdk.KEY_KP_Decimal, .numpad_decimal }, + .{ gdk.KEY_KP_Divide, .numpad_divide }, + .{ gdk.KEY_KP_Multiply, .numpad_multiply }, + .{ gdk.KEY_KP_Subtract, .numpad_subtract }, + .{ gdk.KEY_KP_Add, .numpad_add }, + .{ gdk.KEY_KP_Enter, .numpad_enter }, + .{ gdk.KEY_KP_Equal, .numpad_equal }, - .{ gdk.KEY_KP_Separator, .kp_separator }, - .{ gdk.KEY_KP_Left, .kp_left }, - .{ gdk.KEY_KP_Right, .kp_right }, - .{ gdk.KEY_KP_Up, .kp_up }, - .{ gdk.KEY_KP_Down, .kp_down }, - .{ gdk.KEY_KP_Page_Up, .kp_page_up }, - .{ gdk.KEY_KP_Page_Down, .kp_page_down }, - .{ gdk.KEY_KP_Home, .kp_home }, - .{ gdk.KEY_KP_End, .kp_end }, - .{ gdk.KEY_KP_Insert, .kp_insert }, - .{ gdk.KEY_KP_Delete, .kp_delete }, - .{ gdk.KEY_KP_Begin, .kp_begin }, + .{ gdk.KEY_KP_Separator, .numpad_separator }, + .{ gdk.KEY_KP_Left, .numpad_left }, + .{ gdk.KEY_KP_Right, .numpad_right }, + .{ gdk.KEY_KP_Up, .numpad_up }, + .{ gdk.KEY_KP_Down, .numpad_down }, + .{ gdk.KEY_KP_Page_Up, .numpad_page_up }, + .{ gdk.KEY_KP_Page_Down, .numpad_page_down }, + .{ gdk.KEY_KP_Home, .numpad_home }, + .{ gdk.KEY_KP_End, .numpad_end }, + .{ gdk.KEY_KP_Insert, .numpad_insert }, + .{ gdk.KEY_KP_Delete, .numpad_delete }, + .{ gdk.KEY_KP_Begin, .numpad_begin }, - .{ gdk.KEY_Shift_L, .left_shift }, - .{ gdk.KEY_Control_L, .left_control }, - .{ gdk.KEY_Alt_L, .left_alt }, - .{ gdk.KEY_Super_L, .left_super }, - .{ gdk.KEY_Shift_R, .right_shift }, - .{ gdk.KEY_Control_R, .right_control }, - .{ gdk.KEY_Alt_R, .right_alt }, - .{ gdk.KEY_Super_R, .right_super }, + .{ gdk.KEY_Shift_L, .shift_left }, + .{ gdk.KEY_Control_L, .control_left }, + .{ gdk.KEY_Alt_L, .alt_left }, + .{ gdk.KEY_Super_L, .meta_left }, + .{ gdk.KEY_Shift_R, .shift_right }, + .{ gdk.KEY_Control_R, .control_right }, + .{ gdk.KEY_Alt_R, .alt_right }, + .{ gdk.KEY_Super_R, .meta_right }, // TODO: media keys }; diff --git a/src/config/Config.zig b/src/config/Config.zig index 7d2814136..2a34f9c80 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4495,17 +4495,17 @@ pub const Keybinds = struct { if (comptime !builtin.target.os.tag.isDarwin()) { try self.set.put( alloc, - .{ .key = .{ .physical = .n }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 'n' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_window = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .w }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_surface = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .q }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 'q' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .quit = {} }, ); try self.set.put( @@ -4515,22 +4515,22 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .t }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 't' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .w }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .close_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .left }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .shift = true } }, .{ .previous_tab = {} }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .right }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .shift = true } }, .{ .next_tab = {} }, ); try self.set.put( @@ -4545,12 +4545,12 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .o }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 'o' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .right }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .e }, .mods = .{ .ctrl = true, .shift = true } }, + .{ .key = .{ .unicode = 'e' }, .mods = .{ .ctrl = true, .shift = true } }, .{ .new_split = .down }, ); try self.set.put( @@ -4565,51 +4565,46 @@ pub const Keybinds = struct { ); try self.set.put( alloc, - .{ .key = .{ .physical = .up }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .up }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .down }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .down }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .left }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .left }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .right }, .mods = .{ .ctrl = true, .alt = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .ctrl = true, .alt = true } }, .{ .goto_split = .right }, ); // Resizing splits try self.set.put( alloc, - .{ .key = .{ .physical = .up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_up }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .up, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_down }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .down, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_left }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .left, 10 } }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, + .{ .key = .{ .physical = .arrow_right }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, .{ .resize_split = .{ .right, 10 } }, ); - try self.set.put( - alloc, - .{ .key = .{ .physical = .plus }, .mods = .{ .super = true, .ctrl = true, .shift = true } }, - .{ .equalize_splits = {} }, - ); // Viewport scrolling try self.set.put( @@ -4648,14 +4643,14 @@ pub const Keybinds = struct { // Inspector, matching Chromium try self.set.put( alloc, - .{ .key = .{ .physical = .i }, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .unicode = 'i' }, .mods = .{ .shift = true, .ctrl = true } }, .{ .inspector = .toggle }, ); // Terminal try self.set.put( alloc, - .{ .key = .{ .physical = .a }, .mods = .{ .shift = true, .ctrl = true } }, + .{ .key = .{ .unicode = 'a' }, .mods = .{ .shift = true, .ctrl = true } }, .{ .select_all = {} }, ); diff --git a/src/input/key.zig b/src/input/key.zig index c0deea25c..7e770b332 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -398,6 +398,7 @@ pub const Key = enum(c_int) { // These numpad entries are distinguished by various encoding protocols // (legacy and Kitty) so we support them here in case the apprt can // produce them. + numpad_separator, numpad_up, numpad_down, numpad_right, @@ -763,6 +764,7 @@ pub const Key = enum(c_int) { .convert, .kana_mode, .non_convert, + .numpad_separator, .numpad_backspace, .numpad_clear, .numpad_clear_entry, From 7983e0d62ce2a57cdcfc2c88659f019e999ffde5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 12:30:00 -0700 Subject: [PATCH 226/642] input: backwards compatibility --- NOTES.md | 3 - src/input/Binding.zig | 179 +++++++++++++++++++++++++++++++++++++++++- src/input/key.zig | 58 -------------- 3 files changed, 176 insertions(+), 64 deletions(-) delete mode 100644 NOTES.md diff --git a/NOTES.md b/NOTES.md deleted file mode 100644 index 8e4937bd4..000000000 --- a/NOTES.md +++ /dev/null @@ -1,3 +0,0 @@ -- key backwards compatibility, e.g. `grave_accent` -- `physical:` backwards compatibility? - diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 30575bc30..805a3726a 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1143,14 +1143,15 @@ pub const Trigger = struct { } } + // Anything after this point is a key and we only support + // single keys. + if (!result.isKeyUnset()) return Error.InvalidFormat; + // Check if its a key const keysInfo = @typeInfo(key.Key).@"enum"; inline for (keysInfo.fields) |field| { if (!std.mem.eql(u8, field.name, "unidentified")) { if (std.mem.eql(u8, part, field.name)) { - // Repeat not allowed - if (!result.isKeyUnset()) return Error.InvalidFormat; - const keyval = @field(key.Key, field.name); result.key = .{ .physical = keyval }; continue :loop; @@ -1173,12 +1174,141 @@ pub const Trigger = struct { continue :loop; } + // If we're still unset then we look for backwards compatible + // keys with Ghostty 1.1.x. We do this last so its least likely + // to impact performance for modern users. + if (backwards_compatible_keys.get(part)) |old_key| { + result.key = old_key; + continue :loop; + } + // We didn't recognize this value return Error.InvalidFormat; } return result; } + + /// The values that are backwards compatible with Ghostty 1.1.x. + /// Ghostty 1.2+ doesn't support these anymore since we moved to + /// W3C key codes. + const backwards_compatible_keys = std.StaticStringMap(Key).initComptime(.{ + .{ "zero", Key{ .unicode = '0' } }, + .{ "one", Key{ .unicode = '1' } }, + .{ "two", Key{ .unicode = '2' } }, + .{ "three", Key{ .unicode = '3' } }, + .{ "four", Key{ .unicode = '4' } }, + .{ "five", Key{ .unicode = '5' } }, + .{ "six", Key{ .unicode = '6' } }, + .{ "seven", Key{ .unicode = '7' } }, + .{ "eight", Key{ .unicode = '8' } }, + .{ "nine", Key{ .unicode = '9' } }, + .{ "apostrophe", Key{ .unicode = '\'' } }, + .{ "grave_accent", Key{ .physical = .backquote } }, + .{ "left_bracket", Key{ .physical = .bracket_left } }, + .{ "right_bracket", Key{ .physical = .bracket_right } }, + .{ "up", Key{ .physical = .arrow_up } }, + .{ "down", Key{ .physical = .arrow_down } }, + .{ "left", Key{ .physical = .arrow_left } }, + .{ "right", Key{ .physical = .arrow_right } }, + .{ "kp_0", Key{ .physical = .numpad_0 } }, + .{ "kp_1", Key{ .physical = .numpad_1 } }, + .{ "kp_2", Key{ .physical = .numpad_2 } }, + .{ "kp_3", Key{ .physical = .numpad_3 } }, + .{ "kp_4", Key{ .physical = .numpad_4 } }, + .{ "kp_5", Key{ .physical = .numpad_5 } }, + .{ "kp_6", Key{ .physical = .numpad_6 } }, + .{ "kp_7", Key{ .physical = .numpad_7 } }, + .{ "kp_8", Key{ .physical = .numpad_8 } }, + .{ "kp_9", Key{ .physical = .numpad_9 } }, + .{ "kp_add", Key{ .physical = .numpad_add } }, + .{ "kp_subtract", Key{ .physical = .numpad_subtract } }, + .{ "kp_multiply", Key{ .physical = .numpad_multiply } }, + .{ "kp_divide", Key{ .physical = .numpad_divide } }, + .{ "kp_decimal", Key{ .physical = .numpad_decimal } }, + .{ "kp_enter", Key{ .physical = .numpad_enter } }, + .{ "kp_equal", Key{ .physical = .numpad_equal } }, + .{ "kp_separator", Key{ .physical = .numpad_separator } }, + .{ "kp_left", Key{ .physical = .numpad_left } }, + .{ "kp_right", Key{ .physical = .numpad_right } }, + .{ "kp_up", Key{ .physical = .numpad_up } }, + .{ "kp_down", Key{ .physical = .numpad_down } }, + .{ "kp_page_up", Key{ .physical = .numpad_page_up } }, + .{ "kp_page_down", Key{ .physical = .numpad_page_down } }, + .{ "kp_home", Key{ .physical = .numpad_home } }, + .{ "kp_end", Key{ .physical = .numpad_end } }, + .{ "kp_insert", Key{ .physical = .numpad_insert } }, + .{ "kp_delete", Key{ .physical = .numpad_delete } }, + .{ "kp_begin", Key{ .physical = .numpad_begin } }, + .{ "left_shift", Key{ .physical = .shift_left } }, + .{ "right_shift", Key{ .physical = .shift_right } }, + .{ "left_control", Key{ .physical = .control_left } }, + .{ "right_control", Key{ .physical = .control_right } }, + .{ "left_alt", Key{ .physical = .alt_left } }, + .{ "right_alt", Key{ .physical = .alt_right } }, + .{ "left_super", Key{ .physical = .meta_left } }, + .{ "right_super", Key{ .physical = .meta_right } }, + + // Physical variants. This is a blunt approach to this but its + // glue for backwards compatibility so I'm not too worried about + // making this super nice. + .{ "physical:zero", Key{ .physical = .digit_0 } }, + .{ "physical:one", Key{ .physical = .digit_1 } }, + .{ "physical:two", Key{ .physical = .digit_2 } }, + .{ "physical:three", Key{ .physical = .digit_3 } }, + .{ "physical:four", Key{ .physical = .digit_4 } }, + .{ "physical:five", Key{ .physical = .digit_5 } }, + .{ "physical:six", Key{ .physical = .digit_6 } }, + .{ "physical:seven", Key{ .physical = .digit_7 } }, + .{ "physical:eight", Key{ .physical = .digit_8 } }, + .{ "physical:nine", Key{ .physical = .digit_9 } }, + .{ "physical:apostrophe", Key{ .physical = .quote } }, + .{ "physical:grave_accent", Key{ .physical = .backquote } }, + .{ "physical:left_bracket", Key{ .physical = .bracket_left } }, + .{ "physical:right_bracket", Key{ .physical = .bracket_right } }, + .{ "physical:up", Key{ .physical = .arrow_up } }, + .{ "physical:down", Key{ .physical = .arrow_down } }, + .{ "physical:left", Key{ .physical = .arrow_left } }, + .{ "physical:right", Key{ .physical = .arrow_right } }, + .{ "physical:kp_0", Key{ .physical = .numpad_0 } }, + .{ "physical:kp_1", Key{ .physical = .numpad_1 } }, + .{ "physical:kp_2", Key{ .physical = .numpad_2 } }, + .{ "physical:kp_3", Key{ .physical = .numpad_3 } }, + .{ "physical:kp_4", Key{ .physical = .numpad_4 } }, + .{ "physical:kp_5", Key{ .physical = .numpad_5 } }, + .{ "physical:kp_6", Key{ .physical = .numpad_6 } }, + .{ "physical:kp_7", Key{ .physical = .numpad_7 } }, + .{ "physical:kp_8", Key{ .physical = .numpad_8 } }, + .{ "physical:kp_9", Key{ .physical = .numpad_9 } }, + .{ "physical:kp_add", Key{ .physical = .numpad_add } }, + .{ "physical:kp_subtract", Key{ .physical = .numpad_subtract } }, + .{ "physical:kp_multiply", Key{ .physical = .numpad_multiply } }, + .{ "physical:kp_divide", Key{ .physical = .numpad_divide } }, + .{ "physical:kp_decimal", Key{ .physical = .numpad_decimal } }, + .{ "physical:kp_enter", Key{ .physical = .numpad_enter } }, + .{ "physical:kp_equal", Key{ .physical = .numpad_equal } }, + .{ "physical:kp_separator", Key{ .physical = .numpad_separator } }, + .{ "physical:kp_left", Key{ .physical = .numpad_left } }, + .{ "physical:kp_right", Key{ .physical = .numpad_right } }, + .{ "physical:kp_up", Key{ .physical = .numpad_up } }, + .{ "physical:kp_down", Key{ .physical = .numpad_down } }, + .{ "physical:kp_page_up", Key{ .physical = .numpad_page_up } }, + .{ "physical:kp_page_down", Key{ .physical = .numpad_page_down } }, + .{ "physical:kp_home", Key{ .physical = .numpad_home } }, + .{ "physical:kp_end", Key{ .physical = .numpad_end } }, + .{ "physical:kp_insert", Key{ .physical = .numpad_insert } }, + .{ "physical:kp_delete", Key{ .physical = .numpad_delete } }, + .{ "physical:kp_begin", Key{ .physical = .numpad_begin } }, + .{ "physical:left_shift", Key{ .physical = .shift_left } }, + .{ "physical:right_shift", Key{ .physical = .shift_right } }, + .{ "physical:left_control", Key{ .physical = .control_left } }, + .{ "physical:right_control", Key{ .physical = .control_right } }, + .{ "physical:left_alt", Key{ .physical = .alt_left } }, + .{ "physical:right_alt", Key{ .physical = .alt_right } }, + .{ "physical:left_super", Key{ .physical = .meta_left } }, + .{ "physical:right_super", Key{ .physical = .meta_right } }, + }); + /// Returns true if this trigger has no key set. pub fn isKeyUnset(self: Trigger) bool { return switch (self.key) { @@ -1808,6 +1938,49 @@ test "parse: triggers" { try testing.expectError(Error.InvalidFormat, parseSingle("a+b=ignore")); } +// For Ghostty 1.2+ we changed our key names to match the W3C and removed +// `physical:`. This tests the backwards compatibility with the old format. +// Note that our backwards compatibility isn't 100% perfect since triggers +// like `a` now map to unicode instead of "translated" (which was also +// removed). But we did our best here with what was unambiguous. +test "parse: backwards compatibility with <= 1.1.x" { + const testing = std.testing; + + // simple, for sanity + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .{ .unicode = '0' } }, + .action = .{ .ignore = {} }, + }, + try parseSingle("zero=ignore"), + ); + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .{ .physical = .digit_0 } }, + .action = .{ .ignore = {} }, + }, + try parseSingle("physical:zero=ignore"), + ); + + // duplicates + try testing.expectError(Error.InvalidFormat, parseSingle("zero+one=ignore")); + + // test our full map + for ( + Trigger.backwards_compatible_keys.keys(), + Trigger.backwards_compatible_keys.values(), + ) |k, v| { + var buf: [128]u8 = undefined; + try testing.expectEqual( + Binding{ + .trigger = .{ .key = v }, + .action = .{ .ignore = {} }, + }, + try parseSingle(try std.fmt.bufPrint(&buf, "{s}=ignore", .{k})), + ); + } +} + test "parse: global triggers" { const testing = std.testing; diff --git a/src/input/key.zig b/src/input/key.zig index 7e770b332..961d4cefe 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -468,64 +468,6 @@ pub const Key = enum(c_int) { audio_volume_up, wake_up, - // Backwards compatibility for Ghostty 1.1.x and earlier, we don't - // want to force people to rewrite their configs. - // pub const zero = .digit_0; - // pub const one = .digit_1; - // pub const two = .digit_2; - // pub const three = .digit_3; - // pub const four = .digit_4; - // pub const five = .digit_5; - // pub const six = .digit_6; - // pub const seven = .digit_7; - // pub const eight = .digit_8; - // pub const nine = .digit_9; - // pub const apostrophe = .quote; - // pub const grave_accent = .backquote; - // pub const left_bracket = .bracket_left; - // pub const right_bracket = .bracket_right; - // pub const up = .arrow_up; - // pub const down = .arrow_down; - // pub const left = .arrow_left; - // pub const right = .arrow_right; - // pub const kp_0 = .numpad_0; - // pub const kp_1 = .numpad_1; - // pub const kp_2 = .numpad_2; - // pub const kp_3 = .numpad_3; - // pub const kp_4 = .numpad_4; - // pub const kp_5 = .numpad_5; - // pub const kp_6 = .numpad_6; - // pub const kp_7 = .numpad_7; - // pub const kp_8 = .numpad_8; - // pub const kp_9 = .numpad_9; - // pub const kp_decimal = .numpad_decimal; - // pub const kp_divide = .numpad_divide; - // pub const kp_multiply = .numpad_multiply; - // pub const kp_subtract = .numpad_subtract; - // pub const kp_add = .numpad_add; - // pub const kp_enter = .numpad_enter; - // pub const kp_equal = .numpad_equal; - // pub const kp_separator = .numpad_separator; - // pub const kp_left = .numpad_left; - // pub const kp_right = .numpad_right; - // pub const kp_up = .numpad_up; - // pub const kp_down = .numpad_down; - // pub const kp_page_up = .numpad_page_up; - // pub const kp_page_down = .numpad_page_down; - // pub const kp_home = .numpad_home; - // pub const kp_end = .numpad_end; - // pub const kp_insert = .numpad_insert; - // pub const kp_delete = .numpad_delete; - // pub const kp_begin = .numpad_begin; - // pub const left_shift = .shift_left; - // pub const right_shift = .shift_right; - // pub const left_control = .control_left; - // pub const right_control = .control_right; - // pub const left_alt = .alt_left; - // pub const right_alt = .alt_right; - // pub const left_super = .meta_left; - // pub const right_super = .meta_right; - /// Converts an ASCII character to a key, if possible. This returns /// null if the character is unknown. /// From 1e76222f19eceae25e9a496324a6fb6afb36e8d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 12:44:34 -0700 Subject: [PATCH 227/642] update docs --- src/config/Config.zig | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 2a34f9c80..d154109a6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -929,12 +929,23 @@ class: ?[:0]const u8 = null, /// Trigger: `+`-separated list of keys and modifiers. Example: `ctrl+a`, /// `ctrl+shift+b`, `up`. /// -/// Valid keys are currently only listed in the -/// [Ghostty source code](https://github.com/ghostty-org/ghostty/blob/d6e76858164d52cff460fedc61ddf2e560912d71/src/input/key.zig#L255). -/// This is a documentation limitation and we will improve this in the future. -/// A common gotcha is that numeric keys are written as words: e.g. `one`, -/// `two`, `three`, etc. and not `1`, `2`, `3`. This will also be improved in -/// the future. +/// If the key is a single Unicode codepoint, the trigger will match +/// any presses that produce that codepoint. These are impacted by +/// keyboard layouts. For example, `a` will match the `a` key on a +/// QWERTY keyboard, but will match the `q` key on a AZERTY keyboard +/// (assuming US physical layout). +/// +/// Physical key codes can be specified by using any of the key codes +/// as specified by the [W3C specification](https://www.w3.org/TR/uievents-code/). +/// For example, `KeyA` will match the physical `a` key on a US standard +/// keyboard regardless of the keyboard layout. +/// +/// Function keys such as `insert`, `up`, `f5`, etc. are also specified +/// using the keys as specified by the previously linked W3C specification. +/// +/// Physical keys always match with a higher priority than Unicode codepoints, +/// so if you specify both `a` and `KeyA`, the physical key will always be used +/// regardless of what order they are configured. /// /// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`, /// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier @@ -954,11 +965,6 @@ class: ?[:0]const u8 = null, /// /// * only a single key input is allowed, `ctrl+a+b` is invalid. /// -/// * the key input can be prefixed with `physical:` to specify a -/// physical key mapping rather than a logical one. A physical key -/// mapping responds to the hardware keycode and not the keycode -/// translated by any system keyboard layouts. Example: "ctrl+physical:a" -/// /// You may also specify multiple triggers separated by `>` to require a /// sequence of triggers to activate the action. For example, /// `ctrl+a>n=new_window` will only trigger the `new_window` action if the From cc748305fb7364594baf1e81aa5c7cf63c3a17b6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 14:14:09 -0700 Subject: [PATCH 228/642] input: w3c names for keys --- src/input/Binding.zig | 22 ++++++++++++ src/input/key.zig | 80 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 805a3726a..e6347ab9d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1174,6 +1174,12 @@ pub const Trigger = struct { continue :loop; } + // Look for a matching w3c name next. + if (key.Key.fromW3C(part)) |w3c_key| { + result.key = .{ .physical = w3c_key }; + continue :loop; + } + // If we're still unset then we look for backwards compatible // keys with Ghostty 1.1.x. We do this last so its least likely // to impact performance for modern users. @@ -1938,6 +1944,22 @@ test "parse: triggers" { try testing.expectError(Error.InvalidFormat, parseSingle("a+b=ignore")); } +test "parse: w3c key names" { + const testing = std.testing; + + // Exact match + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .{ .physical = .key_a } }, + .action = .{ .ignore = {} }, + }, + try parseSingle("KeyA=ignore"), + ); + + // Case-sensitive + try testing.expectError(Error.InvalidFormat, parseSingle("Keya=ignore")); +} + // For Ghostty 1.2+ we changed our key names to match the W3C and removed // `physical:`. This tests the backwards compatibility with the old format. // Note that our backwards compatibility isn't 100% perfect since triggers diff --git a/src/input/key.zig b/src/input/key.zig index 961d4cefe..d9ef284d1 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -497,6 +497,74 @@ pub const Key = enum(c_int) { }; } + /// Converts a W3C key code to a Ghostty key enum value. + /// + /// All required W3C key codes are supported, but there are a number of + /// non-standard key codes that are not supported. In the case the value is + /// invalid or unsupported, this function will return null. + pub fn fromW3C(code: []const u8) ?Key { + var result: [128]u8 = undefined; + + // If the code is bigger than our buffer it can't possibly match. + if (code.len > result.len) return null; + + // First just check the whole thing lowercased, this is the simple case + if (std.meta.stringToEnum( + Key, + std.ascii.lowerString(&result, code), + )) |key| return key; + + // We need to convert FooBar to foo_bar + var fbs = std.io.fixedBufferStream(&result); + const w = fbs.writer(); + for (code, 0..) |ch, i| switch (ch) { + 'a'...'z' => w.writeByte(ch) catch return null, + + // Caps and numbers trigger underscores + 'A'...'Z', '0'...'9' => { + if (i > 0) w.writeByte('_') catch return null; + w.writeByte(std.ascii.toLower(ch)) catch return null; + }, + + // We don't know of any key codes that aren't alphanumeric. + else => return null, + }; + + return std.meta.stringToEnum(Key, fbs.getWritten()); + } + + /// Converts a Ghostty key enum value to a W3C key code. + pub fn w3c(self: Key) []const u8 { + return switch (self) { + inline else => |tag| comptime w3c: { + @setEvalBranchQuota(50_000); + + const name = @tagName(tag); + + var buf: [128]u8 = undefined; + var fbs = std.io.fixedBufferStream(&buf); + const w = fbs.writer(); + var i: usize = 0; + while (i < name.len) { + if (i == 0) { + w.writeByte(std.ascii.toUpper(name[i])) catch unreachable; + } else if (name[i] == '_') { + i += 1; + w.writeByte(std.ascii.toUpper(name[i])) catch unreachable; + } else { + w.writeByte(name[i]) catch unreachable; + } + + i += 1; + } + + const written = buf; + const result = written[0..fbs.getWritten().len]; + break :w3c result; + }, + }; + } + /// True if this key represents a printable character. pub fn printable(self: Key) bool { return switch (self) { @@ -781,6 +849,18 @@ pub const Key = enum(c_int) { try testing.expect(!Key.digit_1.keypad()); } + test "w3c" { + // All our keys should convert to and from the W3C format. + // We don't support every key in the W3C spec, so we only + // check the enum fields. + const testing = std.testing; + inline for (@typeInfo(Key).@"enum".fields) |field| { + const key = @field(Key, field.name); + const w3c_name = key.w3c(); + try testing.expectEqual(key, Key.fromW3C(w3c_name).?); + } + } + const codepoint_map: []const struct { u21, Key } = &.{ .{ 'a', .key_a }, .{ 'b', .key_b }, From 11a623aa17536d3dd48abb223e3e8639a65d0b12 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 14:32:38 -0700 Subject: [PATCH 229/642] docs --- src/config/Config.zig | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index d154109a6..251dca147 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -938,7 +938,14 @@ class: ?[:0]const u8 = null, /// Physical key codes can be specified by using any of the key codes /// as specified by the [W3C specification](https://www.w3.org/TR/uievents-code/). /// For example, `KeyA` will match the physical `a` key on a US standard -/// keyboard regardless of the keyboard layout. +/// keyboard regardless of the keyboard layout. These are case-sensitive. +/// +/// For aesthetic reasons, the w3c codes also support snake case. For +/// example, `key_a` is equivalent to `KeyA`. The only exceptions are +/// function keys, e.g. `F1` is `f1` (no underscore). This is a consequence +/// of our internal code using snake case but is purposely supported +/// and tested so it is safe to use. It allows an all-lowercase binding +/// which I find more aesthetically pleasing. /// /// Function keys such as `insert`, `up`, `f5`, etc. are also specified /// using the keys as specified by the previously linked W3C specification. From 5962696c3b0f42f863b91f6995d2d41e35dcc550 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 20:53:09 -0700 Subject: [PATCH 230/642] input: remove `physical_key` from the key event (all keys are physical) --- src/Surface.zig | 2 +- src/apprt/embedded.zig | 1 - src/apprt/glfw.zig | 1 - src/apprt/gtk/Surface.zig | 2 -- src/input/Binding.zig | 2 +- src/input/KeyEncoder.zig | 1 - src/input/key.zig | 11 ++++------- src/inspector/key.zig | 7 ------- 8 files changed, 6 insertions(+), 21 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index e173d2d8b..a71c180ff 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1870,7 +1870,7 @@ pub fn keyCallback( // Process the cursor state logic. This will update the cursor shape if // needed, depending on the key state. if ((SurfaceMouse{ - .physical_key = event.physical_key, + .physical_key = event.key, .mouse_event = self.io.terminal.flags.mouse_event, .mouse_shape = self.io.terminal.mouse_shape, .mods = self.mouse.mods, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index cf3f73a55..7bc84bcad 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -98,7 +98,6 @@ pub const App = struct { return .{ .action = self.action, .key = physical_key, - .physical_key = physical_key, .mods = self.mods, .consumed_mods = self.consumed_mods, .composing = self.composing, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 763933b91..e416d5645 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -1108,7 +1108,6 @@ pub const Surface = struct { const key_event: input.KeyEvent = .{ .action = action, .key = key, - .physical_key = key, .mods = mods, .consumed_mods = .{}, .composing = false, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index a04372494..0a9f644b7 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1891,7 +1891,6 @@ pub fn keyEvent( const effect = self.core_surface.keyCallback(.{ .action = action, .key = physical_key, - .physical_key = physical_key, .mods = mods, .consumed_mods = consumed_mods, .composing = self.im_composing, @@ -2043,7 +2042,6 @@ fn gtkInputCommit( _ = self.core_surface.keyCallback(.{ .action = .press, .key = .unidentified, - .physical_key = .unidentified, .mods = .{}, .consumed_mods = .{}, .composing = false, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index e6347ab9d..2c53fb49f 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1731,7 +1731,7 @@ pub const Set = struct { pub fn getEvent(self: *const Set, event: KeyEvent) ?Entry { var trigger: Trigger = .{ .mods = event.mods.binding(), - .key = .{ .physical = event.physical_key }, + .key = .{ .physical = event.key }, }; if (self.get(trigger)) |v| return v; diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 3d43a4e86..7f9972779 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -2190,7 +2190,6 @@ test "legacy: hu layout ctrl+ő sends proper codepoint" { var enc: KeyEncoder = .{ .event = .{ .key = .bracket_left, - .physical_key = .bracket_left, .mods = .{ .ctrl = true }, .utf8 = "ő", .unshifted_codepoint = 337, diff --git a/src/input/key.zig b/src/input/key.zig index d9ef284d1..831c9f07f 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -16,12 +16,10 @@ pub const KeyEvent = struct { /// The action: press, release, etc. action: Action = .press, - /// "key" is the logical key that was pressed. For example, if - /// a Dvorak keyboard layout is being used on a US keyboard, - /// the "i" physical key will be reported as "c". The physical - /// key is the key that was physically pressed on the keyboard. - key: Key, - physical_key: Key = .unidentified, + /// The keycode of the physical key that was pressed. This is agnostic + /// to the layout. Layout-dependent matching can only be done via the + /// UTF-8 or unshifted codepoint. + key: Key = .unidentified, /// Mods are the modifiers that are pressed. mods: Mods = .{}, @@ -63,7 +61,6 @@ pub const KeyEvent = struct { // These are all the fields that are explicitly part of Trigger. std.hash.autoHash(&hasher, self.key); - std.hash.autoHash(&hasher, self.physical_key); std.hash.autoHash(&hasher, self.unshifted_codepoint); std.hash.autoHash(&hasher, self.mods.binding()); diff --git a/src/inspector/key.zig b/src/inspector/key.zig index 10626d6bd..dbccb47a8 100644 --- a/src/inspector/key.zig +++ b/src/inspector/key.zig @@ -117,13 +117,6 @@ pub const Event = struct { _ = cimgui.c.igTableSetColumnIndex(1); cimgui.c.igText("%s", @tagName(self.event.key).ptr); } - if (self.event.physical_key != self.event.key) { - cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); - _ = cimgui.c.igTableSetColumnIndex(0); - cimgui.c.igText("Physical Key"); - _ = cimgui.c.igTableSetColumnIndex(1); - cimgui.c.igText("%s", @tagName(self.event.physical_key).ptr); - } if (!self.event.mods.empty()) { cimgui.c.igTableNextRow(cimgui.c.ImGuiTableRowFlags_None, 0); _ = cimgui.c.igTableSetColumnIndex(0); From d015efc87d59f873e2ae7a5df8fa432cd5db2bdc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 21:08:00 -0700 Subject: [PATCH 231/642] clean up bindings so that they match macOS menus --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 16 ++++++++++------ src/Surface.zig | 2 ++ src/config/Config.zig | 8 ++++---- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 921c32c8b..af9895c35 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1043,12 +1043,16 @@ extension Ghostty { } // If this event as-is would result in a key binding then we send it. - if let surface, - ghostty_surface_key_is_binding( - surface, - event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) { - self.keyDown(with: event) - return true + if let surface { + var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) + let match = (event.characters ?? "").withCString { ptr in + ghosttyEvent.text = ptr + return ghostty_surface_key_is_binding(surface, ghosttyEvent) + } + if match { + self.keyDown(with: event) + return true + } } let equivalent: String diff --git a/src/Surface.zig b/src/Surface.zig index a71c180ff..138cd4839 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1761,6 +1761,8 @@ pub fn keyEventIsBinding( // sequences) or the root set. const set = self.keyboard.bindings orelse &self.config.keybind.set; + // log.warn("text keyEventIsBinding event={} match={}", .{ event, set.getEvent(event) != null }); + // If we have a keybinding for this event then we return true. return set.getEvent(event) != null; } diff --git a/src/config/Config.zig b/src/config/Config.zig index 251dca147..0ec61d4c5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4410,23 +4410,23 @@ pub const Keybinds = struct { // set the expected keybind for the menu. try self.set.put( alloc, - .{ .key = .{ .unicode = '+' }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .physical = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .equal }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = '+' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .increase_font_size = 1 }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .minus }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = '-' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .decrease_font_size = 1 }, ); try self.set.put( alloc, - .{ .key = .{ .physical = .digit_0 }, .mods = inputpkg.ctrlOrSuper(.{}) }, + .{ .key = .{ .unicode = '0' }, .mods = inputpkg.ctrlOrSuper(.{}) }, .{ .reset_font_size = {} }, ); From 5dc88bda6a71d952209ff0fc02c4a1bae2e1384a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 8 May 2025 21:22:26 -0700 Subject: [PATCH 232/642] macOS: send proper UTF-8 text for more key events --- macos/Sources/Ghostty/NSEvent+Extension.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index 058e7aace..a5aec3870 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -65,6 +65,6 @@ extension NSEvent { return self.characters(byApplyingModifiers: modifierFlags.subtracting(.control)) } - return nil + return characters } } From 54bd701ba973bee77163408b6438d97e98b5ff5e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 May 2025 07:22:04 -0700 Subject: [PATCH 233/642] input: bindings should match on single-codepoint utf-8 text too --- src/input/Binding.zig | 87 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 2c53fb49f..d1fcabb1b 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1735,6 +1735,23 @@ pub const Set = struct { }; if (self.get(trigger)) |v| return v; + // If our UTF-8 text is exactly one codepoint, we try to match that. + if (event.utf8.len > 0) unicode: { + const view = std.unicode.Utf8View.init(event.utf8) catch break :unicode; + var it = view.iterator(); + + // No codepoints or multiple codepoints drops to invalid format + const cp = it.nextCodepoint() orelse break :unicode; + if (it.nextCodepoint() != null) break :unicode; + + trigger.key = .{ .unicode = cp }; + if (self.get(trigger)) |v| return v; + } + + // Finally fallback to the full unshifted codepoint if we have one. + // Question: should we be doing this if we have UTF-8 text? I + // suspect "no" but we don't currently have any failing scenarios + // to verify this. if (event.unshifted_codepoint > 0) { trigger.key = .{ .unicode = event.unshifted_codepoint }; if (self.get(trigger)) |v| return v; @@ -2660,6 +2677,76 @@ test "set: consumed state" { try testing.expect(s.get(.{ .key = .{ .unicode = 'a' } }).?.value_ptr.*.leaf.flags.consumed); } +test "set: getEvent physical" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "ctrl+quote=new_window"); + + // Physical matches on physical + { + const action = s.getEvent(.{ + .key = .quote, + .mods = .{ .ctrl = true }, + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } + + // Physical does not match on UTF8/codepoint + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + .utf8 = "'", + .unshifted_codepoint = '\'', + }); + try testing.expect(action == null); + } +} + +test "set: getEvent codepoint" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "ctrl+'=new_window"); + + // Matches on codepoint + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + .utf8 = "", + .unshifted_codepoint = '\'', + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } + + // Matches on UTF-8 + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + .utf8 = "'", + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } + + // Doesn't match on physical + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + }); + try testing.expect(action == null); + } +} + test "Action: clone" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); From 293a67cd01a1fdb8a131c4591c078f49aa6dd3d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 May 2025 10:16:10 -0700 Subject: [PATCH 234/642] input: control-encode right control properly --- src/input/KeyEncoder.zig | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 7f9972779..5dfcf7ff5 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -571,7 +571,9 @@ fn ctrlSeq( if (!mods.ctrl) return null; const char, const unset_mods = unset_mods: { - var unset_mods = mods; + // We need to only get binding modifiers so we strip lock + // keys, sides, etc. + var unset_mods = mods.binding(); // Remove alt from our modifiers because it does not impact whether // we are generating a ctrl sequence and we handle the ESC-prefix @@ -640,7 +642,7 @@ fn ctrlSeq( // only matches Kitty in behavior. But I believe this is a // justified divergence because it's a useful distinction. - break :unset_mods .{ char, unset_mods.binding() }; + break :unset_mods .{ char, unset_mods }; }; // After unsetting, we only continue if we have ONLY control set. @@ -2280,3 +2282,11 @@ test "ctrlseq: russian alt ctrl c" { const seq = ctrlSeq(.key_c, "с", 0x0441, .{ .ctrl = true, .alt = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); } + +test "ctrlseq: right ctrl c" { + const seq = ctrlSeq(.key_c, "с", 'c', .{ + .ctrl = true, + .sides = .{ .ctrl = .right }, + }); + try testing.expectEqual(@as(u8, 0x03), seq.?); +} From a26310e83f13f8f0db9d4c3321e2927a3b8be9e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 May 2025 10:23:50 -0700 Subject: [PATCH 235/642] macOS: app key is binding check should include utf-8 chars --- macos/Sources/App/macOS/AppDelegate.swift | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index a3a3185d9..c5d63f55d 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -469,17 +469,22 @@ class AppDelegate: NSObject, guard NSApp.mainWindow == nil else { return event } // If this event as-is would result in a key binding then we send it. - if let app = ghostty.app, - ghostty_app_key_is_binding( - app, - event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) { + if let app = ghostty.app { + var ghosttyEvent = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) + let match = (event.characters ?? "").withCString { ptr in + ghosttyEvent.text = ptr + if !ghostty_app_key_is_binding(app, ghosttyEvent) { + return false + } + + return ghostty_app_key(app, ghosttyEvent) + } + // If the key was handled by Ghostty we stop the event chain. If // the key wasn't handled then we let it fall through and continue // processing. This is important because some bindings may have no // affect at this scope. - if (ghostty_app_key( - app, - event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) { + if match { return nil } } From ebabdb322c911f190e547473b681b3daccbb7373 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 May 2025 10:58:50 -0700 Subject: [PATCH 236/642] input: ignore control characters for backspace/enter/escape special case --- src/input/KeyEncoder.zig | 46 ++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 5dfcf7ff5..9e68dae2d 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -103,11 +103,15 @@ fn kitty( // and UTF8 text we just send it directly since we assume that is // whats happening. See legacy()'s similar logic for more details // on how to verify this. - if (self.event.utf8.len > 0) { + if (self.event.utf8.len > 0) utf8: { switch (self.event.key) { - .enter => return try copyToBuf(buf, self.event.utf8), - .backspace => return "", else => {}, + inline .enter, .backspace => |tag| { + // See legacy for why we handle this this way. + if (isControlUtf8(self.event.utf8)) break :utf8; + if (comptime tag == .backspace) return ""; + return try copyToBuf(buf, self.event.utf8); + }, } } @@ -272,11 +276,21 @@ fn legacy( // - Korean: escape commits the dead key state // - Korean: backspace should delete a single preedit char // - if (self.event.utf8.len > 0) { + if (self.event.utf8.len > 0) utf8: { switch (self.event.key) { else => {}, - .backspace => return "", - .enter, .escape => break :pc_style, + inline .backspace, .enter, .escape => |tag| { + // We want to ignore control characters. This is because + // some apprts (macOS) will send control characters as + // UTF-8 encodings and we handle that manually. + if (isControlUtf8(self.event.utf8)) break :utf8; + + // Backspace encodes nothing because we modified IME. + // Enter/escape don't encode the PC-style encoding + // because we want to encode committed text. + if (comptime tag == .backspace) return ""; + break :pc_style; + }, } } @@ -712,6 +726,12 @@ fn isControl(cp: u21) bool { return cp < 0x20 or cp == 0x7F; } +/// Returns true if this string is comprised of a single +/// control character. This returns false for multi-byte strings. +fn isControlUtf8(str: []const u8) bool { + return str.len == 1 and isControl(@intCast(str[0])); +} + /// This is the bitmask for fixterm CSI u modifiers. const CsiUMods = packed struct(u3) { shift: bool = false, @@ -2234,6 +2254,20 @@ test "legacy: super and other mods on macOS with text" { try testing.expectEqualStrings("", actual); } +test "legacy: backspace with DEL utf8" { + var buf: [128]u8 = undefined; + var enc: KeyEncoder = .{ + .event = .{ + .key = .backspace, + .utf8 = &.{0x7F}, + .unshifted_codepoint = 0x08, + }, + }; + + const actual = try enc.legacy(&buf); + try testing.expectEqualStrings("\x7F", actual); +} + test "ctrlseq: normal ctrl c" { const seq = ctrlSeq(.unidentified, "c", 'c', .{ .ctrl = true }); try testing.expectEqual(@as(u8, 0x03), seq.?); From ca2ead9647b0c5cc5fad59033ae192714b2aae89 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 9 May 2025 11:09:22 -0700 Subject: [PATCH 237/642] input: kitty add missing numpad keycodes since we support those now --- src/input/kitty.zig | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/input/kitty.zig b/src/input/kitty.zig index be397b84b..7ebbd7757 100644 --- a/src/input/kitty.zig +++ b/src/input/kitty.zig @@ -106,6 +106,18 @@ const raw_entries: []const RawEntry = &.{ .{ .numpad_add, 57413, 'u', false }, .{ .numpad_enter, 57414, 'u', false }, .{ .numpad_equal, 57415, 'u', false }, + .{ .numpad_separator, 57416, 'u', false }, + .{ .numpad_left, 57417, 'u', false }, + .{ .numpad_right, 57418, 'u', false }, + .{ .numpad_up, 57419, 'u', false }, + .{ .numpad_down, 57420, 'u', false }, + .{ .numpad_page_up, 57421, 'u', false }, + .{ .numpad_page_down, 57422, 'u', false }, + .{ .numpad_home, 57423, 'u', false }, + .{ .numpad_end, 57424, 'u', false }, + .{ .numpad_insert, 57425, 'u', false }, + .{ .numpad_delete, 57426, 'u', false }, + .{ .numpad_begin, 57427, 'u', false }, .{ .shift_left, 57441, 'u', true }, .{ .shift_right, 57447, 'u', true }, From 1752edd9ebc42ead4ad20b922993412e9d3e9038 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 May 2025 07:22:20 -0700 Subject: [PATCH 238/642] input: implement case folding for binding matching --- src/input/Binding.zig | 65 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index d1fcabb1b..31ce8b554 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -5,6 +5,7 @@ const Binding = @This(); const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; +const ziglyph = @import("ziglyph"); const key = @import("key.zig"); const KeyEvent = key.KeyEvent; @@ -1332,10 +1333,32 @@ pub const Trigger = struct { /// Hash the trigger into the given hasher. fn hashIncremental(self: Trigger, hasher: anytype) void { - std.hash.autoHash(hasher, self.key); + std.hash.autoHash(hasher, std.meta.activeTag(self.key)); + switch (self.key) { + .physical => |v| std.hash.autoHash(hasher, v), + .unicode => |cp| std.hash.autoHash( + hasher, + foldedCodepoint(cp), + ), + } std.hash.autoHash(hasher, self.mods.binding()); } + /// The codepoint we use for comparisons. Case folding can result + /// in more codepoints so we need to use a 3 element array. + fn foldedCodepoint(cp: u21) [3]u21 { + // ASCII fast path + if (ziglyph.letter.isAsciiLetter(cp)) { + return .{ ziglyph.letter.toLower(cp), 0, 0 }; + } + + // Unicode slow path. Case folding can resultin more codepoints. + // If more codepoints are produced then we return the codepoint + // as-is which isn't correct but until we have a failing test + // then I don't want to handle this. + return ziglyph.letter.toCaseFold(cp); + } + /// Convert the trigger to a C API compatible trigger. pub fn cval(self: Trigger) C { return .{ @@ -2747,6 +2770,46 @@ test "set: getEvent codepoint" { } } +test "set: getEvent codepoint case folding" { + const testing = std.testing; + const alloc = testing.allocator; + + var s: Set = .{}; + defer s.deinit(alloc); + + try s.parseAndPut(alloc, "ctrl+A=new_window"); + + // Lowercase codepoint + { + const action = s.getEvent(.{ + .key = .key_j, + .mods = .{ .ctrl = true }, + .utf8 = "", + .unshifted_codepoint = 'a', + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } + + // Uppercase codepoint + { + const action = s.getEvent(.{ + .key = .key_a, + .mods = .{ .ctrl = true }, + .utf8 = "", + .unshifted_codepoint = 'A', + }).?.value_ptr.*.leaf; + try testing.expect(action.action == .new_window); + } + + // Negative case for sanity + { + const action = s.getEvent(.{ + .key = .key_j, + .mods = .{ .ctrl = true }, + }); + try testing.expect(action == null); + } +} test "Action: clone" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(testing.allocator); From ed1194cd7571c681df17909dfff69311c22731b2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 May 2025 08:51:03 -0700 Subject: [PATCH 239/642] fix tests --- include/ghostty.h | 1 + src/config/Config.zig | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 2734fc368..72f23b22b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -103,6 +103,7 @@ typedef enum { GHOSTTY_ACTION_REPEAT, } ghostty_input_action_e; +// Based on: https://www.w3.org/TR/uievents-code/ typedef enum { GHOSTTY_KEY_UNIDENTIFIED, diff --git a/src/config/Config.zig b/src/config/Config.zig index 0ec61d4c5..7c93ac845 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5159,9 +5159,9 @@ pub const Keybinds = struct { // NB: This does not currently retain the order of the keybinds. const want = - \\a = ctrl+a>ctrl+b>n=new_window - \\a = ctrl+a>ctrl+b>w=close_window \\a = ctrl+a>ctrl+c>t=new_tab + \\a = ctrl+a>ctrl+b>w=close_window + \\a = ctrl+a>ctrl+b>n=new_window \\a = ctrl+b>ctrl+d>a=previous_tab \\ ; From db1608ff1674f3d5338180ae5dbc42e0726e89d0 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 11 May 2025 00:14:21 +0000 Subject: [PATCH 240/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 7c5ff8ffc..187c67531 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz", - .hash = "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz", + .hash = "N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 513ee0dcd..8b29ff0c3 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A": { + "N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz", - "hash": "sha256-xpDitXpZrdU/EcgLyG4G0cEiT4r42viy+DJALmy2sQE=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz", + "hash": "sha256-YIlb2eSviWrRc+hbwgsAHLeCY3JgbYWjd9ZbOpXe1Qg=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 46cf07cc9..056c7b75e 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A"; + name = "N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz"; - hash = "sha256-xpDitXpZrdU/EcgLyG4G0cEiT4r42viy+DJALmy2sQE="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz"; + hash = "sha256-YIlb2eSviWrRc+hbwgsAHLeCY3JgbYWjd9ZbOpXe1Qg="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 5f06418a7..dd38069c4 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index bc3b6cd0c..9dbd2c18d 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/1e4957e65005908993250f8f07be3f70e805195e.tar.gz", - "dest": "vendor/p/N-V-__8AAHOASAQuLADcCSHLHEJiKFVLZiCD9Aq2rh5GT01A", - "sha256": "c690e2b57a59add53f11c80bc86e06d1c1224f8af8daf8b2f832402e6cb6b101" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz", + "dest": "vendor/p/N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs", + "sha256": "60895bd9e4af896ad173e85bc20b001cb7826372606d85a377d65b3a95ded508" }, { "type": "archive", From c4f1c78fcf254bd23196eff3c00bb86d071fe0fd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 10 May 2025 14:50:56 -0700 Subject: [PATCH 241/642] macOS: treat C-/ specially again to prevent beep Fixes #7310 --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 10 ++++++++++ src/input/key.zig | 11 ----------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index af9895c35..8e8838471 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1066,6 +1066,16 @@ extension Ghostty { equivalent = "\r" + case "/": + // Treat C-/ as C-_. We do this because C-/ makes macOS make a beep + // sound and we don't like the beep sound. + if (!event.modifierFlags.contains(.control) || + !event.modifierFlags.isDisjoint(with: [.shift, .command, .option])) { + return false + } + + equivalent = "_" + default: // It looks like some part of AppKit sometimes generates synthetic NSEvents // with a zero timestamp. We never process these at this point. Concretely, diff --git a/src/input/key.zig b/src/input/key.zig index 831c9f07f..9dad37d78 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -149,9 +149,6 @@ pub const Mods = packed struct(Mods.Backing) { pub fn translation(self: Mods, option_as_alt: config.OptionAsAlt) Mods { var result = self; - // Control is never used for translation. - result.ctrl = false; - // macos-option-as-alt for darwin if (comptime builtin.target.os.tag.isDarwin()) alt: { // Alt has to be set only on the correct side @@ -187,14 +184,6 @@ pub const Mods = packed struct(Mods.Backing) { ); } - test "translation removes control" { - const testing = std.testing; - - const mods: Mods = .{ .ctrl = true }; - const result = mods.translation(.true); - try testing.expectEqual(Mods{}, result); - } - test "translation macos-option-as-alt" { if (comptime !builtin.target.os.tag.isDarwin()) return error.SkipZigTest; From 8f40d1331e322066a6c466f0eb895af3a4d721fe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 May 2025 09:07:53 -0700 Subject: [PATCH 242/642] ensure `ctrl++` parses, clarify case folding docs --- src/config/Config.zig | 16 ++++++++++++++++ src/input/Binding.zig | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 7c93ac845..ca330f8f6 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -935,6 +935,22 @@ class: ?[:0]const u8 = null, /// QWERTY keyboard, but will match the `q` key on a AZERTY keyboard /// (assuming US physical layout). /// +/// For Unicode codepoints, matching is done by comparing the set of +/// modifiers with the unmodified codepoint. The unmodified codepoint is +/// sometimes called an "unshifted character" in other software, but all +/// modifiers are considered, not only shift. For example, `ctrl+a` will match +/// `a` but not `ctrl+shift+a` (which is `A` on a US keyboard). +/// +/// Further, codepoint matching is case-insensitive and the unmodified +/// codepoint is always case folded for comparison. As a result, +/// `ctrl+A` configured will match when `ctrl+a` is pressed. Note that +/// this means some key combinations are impossible depending on keyboard +/// layout. For example, `ctrl+_` is impossible on a US keyboard because +/// `_` is `shift+-` and `ctrl+shift+-` is not equal to `ctrl+_` (because +/// the modifiers don't match!). More details on impossible key combinations +/// can be found at this excellent source written by Qt developers: +/// https://doc.qt.io/qt-6/qkeysequence.html#keyboard-layout-issues +/// /// Physical key codes can be specified by using any of the key codes /// as specified by the [W3C specification](https://www.w3.org/TR/uievents-code/). /// For example, `KeyA` will match the physical `a` key on a US standard diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 31ce8b554..d02a58078 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1109,10 +1109,11 @@ pub const Trigger = struct { pub fn parse(input: []const u8) !Trigger { if (input.len == 0) return Error.InvalidFormat; var result: Trigger = .{}; - var iter = std.mem.tokenizeScalar(u8, input, '+'); - loop: while (iter.next()) |part| { - // All parts must be non-empty - if (part.len == 0) return Error.InvalidFormat; + var rem: []const u8 = input; + loop: while (rem.len > 0) { + const idx = std.mem.indexOfScalar(u8, rem, '+') orelse rem.len; + const part = rem[0..idx]; + rem = if (idx >= rem.len) "" else rem[idx + 1 ..]; // Check if its a modifier const modsInfo = @typeInfo(key.Mods).@"struct"; @@ -1148,6 +1149,13 @@ pub const Trigger = struct { // single keys. if (!result.isKeyUnset()) return Error.InvalidFormat; + // If the part is empty it means that it is actually + // a literal `+`, which we treat as a Unicode character. + if (part.len == 0) { + result.key = .{ .unicode = '+' }; + continue :loop; + } + // Check if its a key const keysInfo = @typeInfo(key.Key).@"enum"; inline for (keysInfo.fields) |field| { @@ -2000,6 +2008,32 @@ test "parse: w3c key names" { try testing.expectError(Error.InvalidFormat, parseSingle("Keya=ignore")); } +test "parse: plus sign" { + const testing = std.testing; + + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .{ .unicode = '+' } }, + .action = .{ .ignore = {} }, + }, + try parseSingle("+=ignore"), + ); + + // Modifier + try testing.expectEqual( + Binding{ + .trigger = .{ + .key = .{ .unicode = '+' }, + .mods = .{ .ctrl = true }, + }, + .action = .{ .ignore = {} }, + }, + try parseSingle("ctrl++=ignore"), + ); + + try testing.expectError(Error.InvalidFormat, parseSingle("++=ignore")); +} + // For Ghostty 1.2+ we changed our key names to match the W3C and removed // `physical:`. This tests the backwards compatibility with the old format. // Note that our backwards compatibility isn't 100% perfect since triggers From 6c6cdf4c4f615fd3c882679191784f5907956af2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 May 2025 09:56:58 -0700 Subject: [PATCH 243/642] input: bracket right was mapped to left, a typo --- src/input/keycodes.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig index fa95ff206..b4004088e 100644 --- a/src/input/keycodes.zig +++ b/src/input/keycodes.zig @@ -89,7 +89,7 @@ const code_to_key = code_to_key: { .{ "Minus", .minus }, .{ "Equal", .equal }, .{ "BracketLeft", .bracket_left }, - .{ "BracketRight", .bracket_left }, + .{ "BracketRight", .bracket_right }, .{ "Backslash", .backslash }, .{ "Semicolon", .semicolon }, .{ "Quote", .quote }, From ecda5ec327288bae5e27c8d2ec1e7ac3a99b3087 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 May 2025 11:12:04 -0700 Subject: [PATCH 244/642] macos: do not send UTF-8 PUA codepoints to key events Fixes #7337 AppKit encodes functional keys as PUA codepoints. We don't want to send that down as valid text encoding for a key event because KKP uses that in particular to change the encoding with associated text. I think there may be a more specific solution to this by only doing this within the KKP encoding part of KeyEncoder but that was filled with edge cases and I didn't want to risk breaking anything else. --- macos/Sources/Ghostty/NSEvent+Extension.swift | 19 +++++++++++++------ src/input/KeyEncoder.zig | 8 ++++++-- src/terminal/kitty/key.zig | 9 +++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/macos/Sources/Ghostty/NSEvent+Extension.swift b/macos/Sources/Ghostty/NSEvent+Extension.swift index a5aec3870..b67c1932e 100644 --- a/macos/Sources/Ghostty/NSEvent+Extension.swift +++ b/macos/Sources/Ghostty/NSEvent+Extension.swift @@ -56,13 +56,20 @@ extension NSEvent { // If we have no characters associated with this event we do nothing. guard let characters else { return nil } - // If we have a single control character, then we return the characters - // without control pressed. We do this because we handle control character - // encoding directly within Ghostty's KeyEncoder. if characters.count == 1, - let scalar = characters.unicodeScalars.first, - scalar.value < 0x20 { - return self.characters(byApplyingModifiers: modifierFlags.subtracting(.control)) + let scalar = characters.unicodeScalars.first { + // If we have a single control character, then we return the characters + // without control pressed. We do this because we handle control character + // encoding directly within Ghostty's KeyEncoder. + if scalar.value < 0x20 { + return self.characters(byApplyingModifiers: modifierFlags.subtracting(.control)) + } + + // If we have a single value in the PUA, then it's a function key and + // we don't want to send PUA ranges down to Ghostty. + if scalar.value >= 0xF700 && scalar.value <= 0xF8FF { + return nil + } } return characters diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 9e68dae2d..41634f2f1 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -146,7 +146,9 @@ fn kitty( // the real world issue is usually control characters. const view = try std.unicode.Utf8View.init(self.event.utf8); var it = view.iterator(); - while (it.nextCodepoint()) |cp| if (isControl(cp)) break :plain_text; + while (it.nextCodepoint()) |cp| { + if (isControl(cp)) break :plain_text; + } return try copyToBuf(buf, self.event.utf8); } @@ -212,7 +214,9 @@ fn kitty( } } - if (self.kitty_flags.report_associated and seq.event != .release) associated: { + if (self.kitty_flags.report_associated and + seq.event != .release) + associated: { // Determine if the Alt modifier should be treated as an actual // modifier (in which case it prevents associated text) or as // the macOS Option key, which does not prevent associated text. diff --git a/src/terminal/kitty/key.zig b/src/terminal/kitty/key.zig index a04bd181a..8bafcb7dc 100644 --- a/src/terminal/kitty/key.zig +++ b/src/terminal/kitty/key.zig @@ -83,6 +83,15 @@ pub const Flags = packed struct(u5) { report_all: bool = false, report_associated: bool = false, + /// Sets all modes on. + pub const @"true": Flags = .{ + .disambiguate = true, + .report_events = true, + .report_alternates = true, + .report_all = true, + .report_associated = true, + }; + pub fn int(self: Flags) u5 { return @bitCast(self); } From e2daf04cbad11c1e207334e826810f61b0d88695 Mon Sep 17 00:00:00 2001 From: Ken VanDine Date: Mon, 12 May 2025 17:40:48 -0400 Subject: [PATCH 245/642] snap: Build with cpu=baseline as documented in PACKAGING.md --- snap/snapcraft.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 8f1a7180a..b57411a6c 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -80,7 +80,7 @@ parts: - gettext override-build: | craftctl set version=$(cat VERSION) - $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast + $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline cp -rp zig-out/* $CRAFT_PART_INSTALL/ sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop From 507e808a7caec36c5f852be54a943fe6f3b7bdfe Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 12 May 2025 15:32:27 -0700 Subject: [PATCH 246/642] input: add backwards compatible alias for `plus` to `+` From #7320 Discussion #7340 There isn't a `physical` alias because there is no physical plus key defined for the W3C keycode spec. --- src/input/Binding.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index d02a58078..89c5e4352 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -1218,6 +1218,7 @@ pub const Trigger = struct { .{ "seven", Key{ .unicode = '7' } }, .{ "eight", Key{ .unicode = '8' } }, .{ "nine", Key{ .unicode = '9' } }, + .{ "plus", Key{ .unicode = '+' } }, .{ "apostrophe", Key{ .unicode = '\'' } }, .{ "grave_accent", Key{ .physical = .backquote } }, .{ "left_bracket", Key{ .physical = .bracket_left } }, From 8d0c3c7b7c4b4aaba59d5562fe43701d2d9e566a Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 23 Jan 2025 13:57:36 -0600 Subject: [PATCH 247/642] gtk: implement custom audio for bell --- nix/build-support/build-inputs.nix | 3 ++ nix/devShell.nix | 4 ++ nix/package.nix | 4 ++ src/apprt/gtk/Surface.zig | 65 ++++++++++++++++++++++++++++++ src/config/Config.zig | 30 ++++++++++++-- 5 files changed, 102 insertions(+), 4 deletions(-) diff --git a/nix/build-support/build-inputs.nix b/nix/build-support/build-inputs.nix index 5886cfe30..7c9258675 100644 --- a/nix/build-support/build-inputs.nix +++ b/nix/build-support/build-inputs.nix @@ -28,6 +28,9 @@ pkgs.glib pkgs.gobject-introspection pkgs.gsettings-desktop-schemas + pkgs.gst_all_1.gst-plugins-base + pkgs.gst_all_1.gst-plugins-good + pkgs.gst_all_1.gstreamer pkgs.gtk4 pkgs.libadwaita ] diff --git a/nix/devShell.nix b/nix/devShell.nix index 498102ef4..b87c23dd1 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -35,6 +35,7 @@ gtk4, gtk4-layer-shell, gobject-introspection, + gst_all_1, libadwaita, blueprint-compiler, gettext, @@ -166,6 +167,9 @@ in wayland wayland-scanner wayland-protocols + gst_all_1.gstreamer + gst_all_1.gst-plugins-base + gst_all_1.gst-plugins-good ]; # This should be set onto the rpath of the ghostty binary if you want diff --git a/nix/package.nix b/nix/package.nix index 9368b2cde..a39f5b835 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -127,6 +127,10 @@ in mv $out/share/vim/vimfiles "$vim" ln -sf "$vim" "$out/share/vim/vimfiles" echo "$vim" >> "$out/nix-support/propagated-user-env-packages" + + echo "gst_all_1.gstreamer" >> "$out/nix-support/propagated-user-env-packages" + echo "gst_all_1.gst-plugins-base" >> "$out/nix-support/propagated-user-env-packages" + echo "gst_all_1.gst-plugins-good" >> "$out/nix-support/propagated-user-env-packages" ''; meta = { diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 0a9f644b7..e47316ac3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2405,6 +2405,47 @@ pub fn ringBell(self: *Surface) !void { surface.beep(); } + if (features.audio) audio: { + // Play a user-specified audio file. + + const pathname, const optional = switch (self.app.config.@"bell-audio-path" orelse break :audio) { + .optional => |path| .{ path, true }, + .required => |path| .{ path, false }, + }; + + const volume: f64 = @min( + @max( + 0.0, + self.app.config.@"bell-audio-volume", + ), + 1.0, + ); + + std.debug.assert(std.fs.path.isAbsolute(pathname)); + const media_file = gtk.MediaFile.newForFilename(pathname); + + if (!optional) { + _ = gobject.Object.signals.notify.connect( + media_file, + ?*anyopaque, + gtkStreamError, + null, + .{ .detail = "error" }, + ); + } + _ = gobject.Object.signals.notify.connect( + media_file, + ?*anyopaque, + gtkStreamEnded, + null, + .{ .detail = "ended" }, + ); + + const media_stream = media_file.as(gtk.MediaStream); + media_stream.setVolume(volume); + media_stream.play(); + } + // Mark tab as needing attention if (self.container.tab()) |tab| tab: { const page = window.notebook.getTabPage(tab) orelse break :tab; @@ -2413,3 +2454,27 @@ pub fn ringBell(self: *Surface) !void { if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true)); } } + +/// Handle a stream that is in an error state. +fn gtkStreamError(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void { + const path = path: { + const file = media_file.getFile() orelse break :path null; + break :path file.getPath(); + }; + defer if (path) |p| glib.free(p); + + const media_stream = media_file.as(gtk.MediaStream); + const err = media_stream.getError() orelse return; + + log.warn("error playing bell from {s}: {s} {d} {s}", .{ + path orelse "<>", + glib.quarkToString(err.f_domain), + err.f_code, + err.f_message orelse "", + }); +} + +/// Stream is finished, release the memory. +fn gtkStreamEnded(media_file: *gtk.MediaFile, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void { + media_file.unref(); +} diff --git a/src/config/Config.zig b/src/config/Config.zig index ca330f8f6..b51f053cd 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1890,8 +1890,10 @@ keybind: Keybinds = .{}, /// open terminals. @"custom-shader-animation": CustomShaderAnimation = .true, -/// The list of enabled features that are activated after encountering -/// a bell character. +/// Bell features to enable if bell support is available in your runtime. Not +/// all features are available on all runtimes. The format of this is a list of +/// features to enable separated by commas. If you prefix a feature with `no-` +/// then it is disabled. If you omit a feature, its default value is used. /// /// Valid values are: /// @@ -1901,17 +1903,36 @@ keybind: Keybinds = .{}, /// This could result in an audiovisual effect, a notification, or something /// else entirely. Changing these effects require altering system settings: /// for instance under the "Sound > Alert Sound" setting in GNOME, -/// or the "Accessibility > System Bell" settings in KDE Plasma. +/// or the "Accessibility > System Bell" settings in KDE Plasma. (GTK only) /// -/// On macOS this has no affect. +/// * `audio` +/// +/// Play a custom sound. (GTK only) +/// +/// Example: `audio`, `no-audio`, `system`, `no-system`: /// /// On macOS, if the app is unfocused, it will bounce the app icon in the dock /// once. Additionally, the title of the window with the alerted terminal /// surface will contain a bell emoji (🔔) until the terminal is focused /// or a key is pressed. These are not currently configurable since they're /// considered unobtrusive. +/// +/// By default, no bell features are enabled. @"bell-features": BellFeatures = .{}, +/// If `audio` is an enabled bell feature, this is a path to an audio file. If +/// the path is not absolute, it is considered relative to the directory of the +/// 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) +@"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) +@"bell-audio-volume": f64 = 0.5, + /// Control the in-app notifications that Ghostty shows. /// /// On Linux (GTK), in-app notifications show up as toasts. Toasts appear @@ -5765,6 +5786,7 @@ pub const AppNotifications = packed struct { /// See bell-features pub const BellFeatures = packed struct { system: bool = false, + audio: bool = false, }; /// See mouse-shift-capture From 0e8b266662d35f1df3abd181f2251e54dc474957 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 16 Apr 2025 11:06:31 -0500 Subject: [PATCH 248/642] Use `std.math.clamp` Co-authored-by: Leah Amelia Chen --- src/apprt/gtk/Surface.zig | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e47316ac3..d756925b3 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2413,13 +2413,7 @@ pub fn ringBell(self: *Surface) !void { .required => |path| .{ path, false }, }; - const volume: f64 = @min( - @max( - 0.0, - self.app.config.@"bell-audio-volume", - ), - 1.0, - ); + const volume = std.math.clamp(self.app.config.@"bell-audio-volume", 0.0, 1.0); std.debug.assert(std.fs.path.isAbsolute(pathname)); const media_file = gtk.MediaFile.newForFilename(pathname); From ba08b0cce51b61efd01c1d9634cbab7c5ddea6c3 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 23 Apr 2025 10:38:47 -0500 Subject: [PATCH 249/642] gtk custom bell audio: optional -> required --- src/apprt/gtk/Surface.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index d756925b3..bcb78e087 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2408,9 +2408,9 @@ pub fn ringBell(self: *Surface) !void { if (features.audio) audio: { // Play a user-specified audio file. - const pathname, const optional = switch (self.app.config.@"bell-audio-path" orelse break :audio) { - .optional => |path| .{ path, true }, - .required => |path| .{ path, false }, + const pathname, const required = switch (self.app.config.@"bell-audio-path" orelse break :audio) { + .optional => |path| .{ path, false }, + .required => |path| .{ path, true }, }; const volume = std.math.clamp(self.app.config.@"bell-audio-volume", 0.0, 1.0); @@ -2418,7 +2418,7 @@ pub fn ringBell(self: *Surface) !void { std.debug.assert(std.fs.path.isAbsolute(pathname)); const media_file = gtk.MediaFile.newForFilename(pathname); - if (!optional) { + if (required) { _ = gobject.Object.signals.notify.connect( media_file, ?*anyopaque, From 528814da7984a1ab28dfcc277a97fdd81965724e Mon Sep 17 00:00:00 2001 From: Weizhao Ouyang Date: Wed, 14 May 2025 23:05:36 +0800 Subject: [PATCH 250/642] url: restrict file paths regex to one slash This restricts the valid path prefixes to prevent false matches caused by literal dot. Signed-off-by: Weizhao Ouyang --- src/config/url.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/url.zig b/src/config/url.zig index 9f9f3fa4a..da3928aff 100644 --- a/src/config/url.zig +++ b/src/config/url.zig @@ -26,7 +26,7 @@ pub const regex = "(?:" ++ url_schemes ++ \\)(?: ++ ipv6_url_pattern ++ - \\|[\w\-.~:/?#@!$&*+,;=%]+(?:[\(\[]\w*[\)\]])?)+(? Date: Wed, 14 May 2025 12:05:33 -0700 Subject: [PATCH 251/642] bench: add `--mode=gen-osc` to generate synthetic OSC sequences This commit adds a few new mode flags to the `bench-stream` program to generator synthetic OSC sequences. The new modes are `gen-osc`, `gen-osc-valid`, and `gen-osc-invalid`. The `gen-osc` mode generates equal parts valid and invalid OSC sequences, while the suffixed variants are for generating only valid or invalid sequences, respectively. This commit also fixes our build system to actually be able to build the benchmarks. It turns out we were just rebuilding the main Ghostty binary for `-Demit-bench`. And, our benchmarks didn't run under Zig 0.14, which is now fixed. An important new design I'm working towards in this commit is to split out synthetic data generation to a dedicated package in `src/bench/synth` although I'm tempted to move it to `src/synth` since it may be useful outside of benchmarks. The synth package is a work-in-progress, but it contains a hint of what's to come. I ultimately want to able to generate all kinds of synthetic data with a lot of knobs to control dimensionality (e.g. in the case of OSC sequences: valid/invalid, length, operation types, etc.). --- src/bench/codepoint-width.zig | 2 +- src/bench/grapheme-break.zig | 2 +- src/bench/page-init.zig | 2 +- src/bench/parser.zig | 2 +- src/bench/stream.zig | 34 +++++- src/bench/synth/main.zig | 15 +++ src/bench/synth/osc.zig | 197 ++++++++++++++++++++++++++++++++++ src/build/SharedDeps.zig | 3 + src/main_ghostty.zig | 1 + src/terminal/osc.zig | 6 +- 10 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 src/bench/synth/main.zig create mode 100644 src/bench/synth/osc.zig diff --git a/src/bench/codepoint-width.zig b/src/bench/codepoint-width.zig index ce44bccb0..07c865e55 100644 --- a/src/bench/codepoint-width.zig +++ b/src/bench/codepoint-width.zig @@ -68,7 +68,7 @@ pub fn main() !void { var args: Args = .{}; defer args.deinit(); { - var iter = try std.process.argsWithAllocator(alloc); + var iter = try cli.args.argsIterator(alloc); defer iter.deinit(); try cli.args.parse(Args, alloc, &args, &iter); } diff --git a/src/bench/grapheme-break.zig b/src/bench/grapheme-break.zig index bbe2171d5..049af4a91 100644 --- a/src/bench/grapheme-break.zig +++ b/src/bench/grapheme-break.zig @@ -60,7 +60,7 @@ pub fn main() !void { var args: Args = .{}; defer args.deinit(); { - var iter = try std.process.argsWithAllocator(alloc); + var iter = try cli.args.argsIterator(alloc); defer iter.deinit(); try cli.args.parse(Args, alloc, &args, &iter); } diff --git a/src/bench/page-init.zig b/src/bench/page-init.zig index e45d64fbb..9b0d1ac1d 100644 --- a/src/bench/page-init.zig +++ b/src/bench/page-init.zig @@ -45,7 +45,7 @@ pub fn main() !void { var args: Args = .{}; defer args.deinit(); { - var iter = try std.process.argsWithAllocator(alloc); + var iter = try cli.args.argsIterator(alloc); defer iter.deinit(); try cli.args.parse(Args, alloc, &args, &iter); } diff --git a/src/bench/parser.zig b/src/bench/parser.zig index ee6c3ee94..9245c06cb 100644 --- a/src/bench/parser.zig +++ b/src/bench/parser.zig @@ -27,7 +27,7 @@ pub fn main() !void { var args: Args = args: { var args: Args = .{}; errdefer args.deinit(); - var iter = try std.process.argsWithAllocator(alloc); + var iter = try cli.args.argsIterator(alloc); defer iter.deinit(); try cli.args.parse(Args, alloc, &args, &iter); break :args args; diff --git a/src/bench/stream.zig b/src/bench/stream.zig index a7abb37cc..0c7d421cc 100644 --- a/src/bench/stream.zig +++ b/src/bench/stream.zig @@ -15,6 +15,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const ziglyph = @import("ziglyph"); const cli = @import("../cli.zig"); const terminal = @import("../terminal/main.zig"); +const synth = @import("synth/main.zig"); const Args = struct { mode: Mode = .noop, @@ -70,6 +71,14 @@ const Mode = enum { // Generate an infinite stream of arbitrary random bytes. @"gen-rand", + + // Generate an infinite stream of OSC requests. These will be mixed + // with valid and invalid OSC requests by default, but the + // `-valid` and `-invalid`-suffixed variants can be used to get only + // a specific type of OSC request. + @"gen-osc", + @"gen-osc-valid", + @"gen-osc-invalid", }; pub const std_options: std.Options = .{ @@ -84,7 +93,7 @@ pub fn main() !void { var args: Args = .{}; defer args.deinit(); { - var iter = try std.process.argsWithAllocator(alloc); + var iter = try cli.args.argsIterator(alloc); defer iter.deinit(); try cli.args.parse(Args, alloc, &args, &iter); } @@ -100,6 +109,9 @@ pub fn main() !void { .@"gen-ascii" => try genAscii(writer, seed), .@"gen-utf8" => try genUtf8(writer, seed), .@"gen-rand" => try genRand(writer, seed), + .@"gen-osc" => try genOsc(writer, seed, 0.5), + .@"gen-osc-valid" => try genOsc(writer, seed, 1.0), + .@"gen-osc-invalid" => try genOsc(writer, seed, 0.0), .noop => try benchNoop(reader, buf), // Handle the ones that depend on terminal state next @@ -142,7 +154,7 @@ fn genAscii(writer: anytype, seed: u64) !void { /// Generates an infinite stream of bytes from the given alphabet. fn genData(writer: anytype, alphabet: []const u8, seed: u64) !void { - var prng = std.rand.DefaultPrng.init(seed); + var prng = std.Random.DefaultPrng.init(seed); const rnd = prng.random(); var buf: [1024]u8 = undefined; while (true) { @@ -159,7 +171,7 @@ fn genData(writer: anytype, alphabet: []const u8, seed: u64) !void { } fn genUtf8(writer: anytype, seed: u64) !void { - var prng = std.rand.DefaultPrng.init(seed); + var prng = std.Random.DefaultPrng.init(seed); const rnd = prng.random(); var buf: [1024]u8 = undefined; while (true) { @@ -180,8 +192,22 @@ fn genUtf8(writer: anytype, seed: u64) !void { } } +fn genOsc(writer: anytype, seed: u64, p_valid: f64) !void { + var prng = std.Random.DefaultPrng.init(seed); + const gen: synth.OSC = .{ .rand = prng.random(), .p_valid = p_valid }; + + var buf: [1024]u8 = undefined; + while (true) { + const seq = try gen.next(&buf); + writer.writeAll(seq) catch |err| switch (err) { + error.BrokenPipe => return, // stdout closed + else => return err, + }; + } +} + fn genRand(writer: anytype, seed: u64) !void { - var prng = std.rand.DefaultPrng.init(seed); + var prng = std.Random.DefaultPrng.init(seed); const rnd = prng.random(); var buf: [1024]u8 = undefined; while (true) { diff --git a/src/bench/synth/main.zig b/src/bench/synth/main.zig new file mode 100644 index 000000000..eda2dec28 --- /dev/null +++ b/src/bench/synth/main.zig @@ -0,0 +1,15 @@ +//! Package synth contains functions for generating synthetic data for +//! the purpose of benchmarking, primarily. This can also probably be used +//! for testing and fuzzing (probably generating a corpus rather than +//! directly fuzzing) and more. +//! +//! The synthetic data generators in this package are usually not performant +//! enough to be streamed in real time. They should instead be used to +//! generate a large amount of data in a single go and then streamed +//! from there. + +pub const OSC = @import("osc.zig").Generator; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/src/bench/synth/osc.zig b/src/bench/synth/osc.zig new file mode 100644 index 000000000..61f168b58 --- /dev/null +++ b/src/bench/synth/osc.zig @@ -0,0 +1,197 @@ +const std = @import("std"); +const assert = std.debug.assert; + +/// Synthetic OSC request generator. +/// +/// I tried to balance generality and practicality. I implemented mainly +/// all I need at the time of writing this, but I think this can be iterated +/// over time to be a general purpose OSC generator with a lot of +/// configurability. I limited the configurability to what I need but still +/// tried to lay out the code in a way that it can be extended easily. +pub const Generator = struct { + /// Random number generator. + rand: std.Random, + + /// Probability of a valid OSC sequence being generated. + p_valid: f64 = 1.0, + + pub const Error = error{NoSpaceLeft}; + + /// We use a FBS as a direct parameter below in non-pub functions, + /// but we should probably just switch to `[]u8`. + const FBS = std.io.FixedBufferStream([]u8); + + /// Get the next OSC request in bytes. The generated OSC request will + /// have the prefix `ESC ]` and the terminator `BEL` (0x07). + /// + /// This will generate both valid and invalid OSC requests (based on + /// the `p_valid` probability value). Invalid requests still have the + /// prefix and terminator, but the content in between is not a valid + /// OSC request. + /// + /// The buffer must be at least 3 bytes long to accommodate the + /// prefix and terminator. + pub fn next(self: *const Generator, buf: []u8) Error![]const u8 { + assert(buf.len >= 3); + var fbs: FBS = std.io.fixedBufferStream(buf); + const writer = fbs.writer(); + + // Start OSC (ESC ]) + try writer.writeAll("\x1b]"); + + // Determine if we are generating a valid or invalid OSC request. + switch (self.chooseValidity()) { + .valid => try self.nextValid(&fbs), + .invalid => try self.nextInvalid(&fbs), + } + + // Terminate OSC + try writer.writeAll("\x07"); + return fbs.getWritten(); + } + + fn nextValid(self: *const Generator, fbs: *FBS) Error!void { + try self.nextValidExact(fbs, self.rand.enumValue(ValidKind)); + } + + fn nextValidExact(self: *const Generator, fbs: *FBS, k: ValidKind) Error!void { + switch (k) { + .change_window_title => { + try fbs.writer().writeAll("0;"); // Set window title + try self.randomBytes(fbs, 1, fbs.buffer.len); + }, + + .prompt_start => { + try fbs.writer().writeAll("133;A"); // Start prompt + + // aid + if (self.rand.boolean()) { + try fbs.writer().writeAll(";aid="); + try self.randomBytes(fbs, 1, 16); + } + + // redraw + if (self.rand.boolean()) { + try fbs.writer().writeAll(";redraw="); + if (self.rand.boolean()) { + try fbs.writer().writeAll("1"); + } else { + try fbs.writer().writeAll("0"); + } + } + }, + + .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt + } + } + + fn nextInvalid(self: *const Generator, fbs: *FBS) Error!void { + switch (self.rand.enumValue(InvalidKind)) { + .random => try self.randomBytes(fbs, 1, fbs.buffer.len), + .good_prefix => { + try fbs.writer().writeAll("133;"); + try self.randomBytes(fbs, 2, fbs.buffer.len); + }, + } + } + + /// Generate a random string of bytes up to `max_len` bytes or + /// until we run out of space in the buffer, whichever is + /// smaller. + /// + /// This will avoid the terminator characters (0x1B and 0x07) and + /// replace them by incrementing them by one. + fn randomBytes( + self: *const Generator, + fbs: *FBS, + min_len: usize, + max_len: usize, + ) Error!void { + const len = @min( + self.rand.intRangeAtMostBiased(usize, min_len, max_len), + fbs.buffer.len - fbs.pos - 1, // leave space for terminator + ); + var rem: usize = len; + var buf: [1024]u8 = undefined; + while (rem > 0) { + self.rand.bytes(&buf); + std.mem.replaceScalar(u8, &buf, 0x1B, 0x1C); + std.mem.replaceScalar(u8, &buf, 0x07, 0x08); + + const n = @min(rem, buf.len); + try fbs.writer().writeAll(buf[0..n]); + rem -= n; + } + } + + /// Choose whether to generate a valid or invalid OSC request based + /// on the validity probability. + fn chooseValidity(self: *const Generator) Validity { + return if (self.rand.float(f64) > self.p_valid) + .invalid + else + .valid; + } + + const Validity = enum { valid, invalid }; + + const ValidKind = enum { + change_window_title, + prompt_start, + prompt_end, + }; + + const InvalidKind = enum { + /// Literally random bytes. Might even be valid, but probably not. + random, + + /// A good prefix, but ultimately invalid format. + good_prefix, + }; +}; + +/// A fixed seed we can use for our tests to avoid flakes. +const test_seed = 0xC0FFEEEEEEEEEEEE; + +test "OSC generator" { + var prng = std.Random.DefaultPrng.init(test_seed); + var buf: [4096]u8 = undefined; + const gen: Generator = .{ .rand = prng.random() }; + for (0..50) |_| _ = try gen.next(&buf); +} + +test "OSC generator valid" { + const testing = std.testing; + const terminal = @import("../../terminal/main.zig"); + + var prng = std.Random.DefaultPrng.init(test_seed); + var buf: [256]u8 = undefined; + const gen: Generator = .{ + .rand = prng.random(), + .p_valid = 1.0, + }; + for (0..50) |_| { + const seq = try gen.next(&buf); + var parser: terminal.osc.Parser = .{}; + for (seq[2 .. seq.len - 1]) |c| parser.next(c); + try testing.expect(parser.end(null) != null); + } +} + +test "OSC generator invalid" { + const testing = std.testing; + const terminal = @import("../../terminal/main.zig"); + + var prng = std.Random.DefaultPrng.init(test_seed); + var buf: [256]u8 = undefined; + const gen: Generator = .{ + .rand = prng.random(), + .p_valid = 0.0, + }; + for (0..50) |_| { + const seq = try gen.next(&buf); + var parser: terminal.osc.Parser = .{}; + for (seq[2 .. seq.len - 1]) |c| parser.next(c); + try testing.expect(parser.end(null) == null); + } +} diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 4b97298f7..0df261600 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -60,6 +60,9 @@ pub fn changeEntrypoint( var result = self.*; result.config = config; + result.options = b.addOptions(); + try config.addOptions(result.options); + return result; } diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 6a4688dc7..4a9f2b138 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -182,6 +182,7 @@ test { _ = @import("surface_mouse.zig"); // Libraries + _ = @import("bench/synth/main.zig"); _ = @import("crash/main.zig"); _ = @import("datastruct/main.zig"); _ = @import("inspector/main.zig"); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index faf376d13..ce7afdf64 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -6,6 +6,7 @@ const osc = @This(); const std = @import("std"); +const builtin = @import("builtin"); const mem = std.mem; const assert = std.debug.assert; const Allocator = mem.Allocator; @@ -1332,7 +1333,10 @@ pub const Parser = struct { /// the response terminator. pub fn end(self: *Parser, terminator_ch: ?u8) ?Command { if (!self.complete) { - log.warn("invalid OSC command: {s}", .{self.buf[0..self.buf_idx]}); + if (comptime !builtin.is_test) log.warn( + "invalid OSC command: {s}", + .{self.buf[0..self.buf_idx]}, + ); return null; } From 55c1ef779f314d87b138943d8a5b3e1f18157862 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 14 May 2025 21:12:20 -0600 Subject: [PATCH 252/642] fix(Metal): interpolate kitty images uint textures can't be interpolated apparently --- src/renderer/metal/image.zig | 2 +- src/renderer/shaders/cell.metal | 25 ++++++++++++------------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 835fbd672..cb675404d 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -441,7 +441,7 @@ pub const Image = union(enum) { }; // Set our properties - desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8uint)); + desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8unorm)); desc.setProperty("width", @as(c_ulong, @intCast(p.width))); desc.setProperty("height", @as(c_ulong, @intCast(p.height))); diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index e80ead9ad..80ffc00de 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -621,9 +621,6 @@ vertex ImageVertexOut image_vertex( texture2d image [[texture(0)]], constant Uniforms& uniforms [[buffer(1)]] ) { - // The size of the image in pixels - float2 image_size = float2(image.get_width(), image.get_height()); - // Turn the image position into a vertex point depending on the // vertex ID. Since we use instanced drawing, we have 4 vertices // for each corner of the cell. We can use vertex ID to determine @@ -638,11 +635,12 @@ vertex ImageVertexOut image_vertex( corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; - // The texture coordinates start at our source x/y, then add the width/height - // as enabled by our instance id, then normalize to [0, 1] + // The texture coordinates start at our source x/y + // and add the width/height depending on the corner. + // + // We don't need to normalize because we use pixel addressing for our sampler. float2 tex_coord = in.source_rect.xy; tex_coord += in.source_rect.zw * corner; - tex_coord /= image_size; ImageVertexOut out; @@ -659,18 +657,19 @@ vertex ImageVertexOut image_vertex( fragment float4 image_fragment( ImageVertexOut in [[stage_in]], - texture2d image [[texture(0)]], + texture2d image [[texture(0)]], constant Uniforms& uniforms [[buffer(1)]] ) { - constexpr sampler textureSampler(address::clamp_to_edge, filter::linear); + constexpr sampler textureSampler( + coord::pixel, + address::clamp_to_edge, + filter::linear + ); - // Ehhhhh our texture is in RGBA8Uint but our color attachment is - // BGRA8Unorm. So we need to convert it. We should really be converting - // our texture to BGRA8Unorm. - uint4 rgba = image.sample(textureSampler, in.tex_coord); + float4 rgba = image.sample(textureSampler, in.tex_coord); return load_color( - uchar4(rgba), + uchar4(rgba * 255.0), // We assume all images are sRGB regardless of the configured colorspace // TODO: Maybe support wide gamut images? false, From 7ccc18133205e504ad08e992d1d52411465f0312 Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Thu, 15 May 2025 13:34:44 +0800 Subject: [PATCH 253/642] macos: add "Check for Updates" action, menu item & key-binding support --- include/ghostty.h | 1 + macos/Sources/App/macOS/AppDelegate.swift | 9 +++++---- macos/Sources/App/macOS/MainMenu.xib | 3 +++ macos/Sources/Ghostty/Ghostty.App.swift | 11 +++++++++++ src/App.zig | 1 + src/apprt/action.zig | 3 +++ src/input/Binding.zig | 6 ++++++ src/input/command.zig | 6 ++++++ 8 files changed, 36 insertions(+), 4 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 72f23b22b..941223943 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -667,6 +667,7 @@ typedef enum { GHOSTTY_ACTION_CONFIG_CHANGE, GHOSTTY_ACTION_CLOSE_WINDOW, GHOSTTY_ACTION_RING_BELL, + GHOSTTY_ACTION_CHECK_FOR_UPDATES } ghostty_action_tag_e; typedef union { diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c5d63f55d..38b26f606 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -154,10 +154,6 @@ class AppDelegate: NSObject, toggleSecureInput(self) } - // Hook up updater menu - menuCheckForUpdates?.target = updaterController - menuCheckForUpdates?.action = #selector(SPUStandardUpdaterController.checkForUpdates(_:)) - // Initial config loading ghosttyConfigDidChange(config: ghostty.config) @@ -374,6 +370,7 @@ class AppDelegate: NSObject, private func syncMenuShortcuts(_ config: Ghostty.Config) { guard ghostty.readiness == .ready else { return } + syncMenuShortcut(config, action: "check_for_updates", menuItem: self.menuCheckForUpdates) syncMenuShortcut(config, action: "open_config", menuItem: self.menuOpenConfig) syncMenuShortcut(config, action: "reload_config", menuItem: self.menuReloadConfig) syncMenuShortcut(config, action: "quit", menuItem: self.menuQuit) @@ -791,6 +788,10 @@ class AppDelegate: NSObject, ghostty.reloadConfig() } + @IBAction func checkForUpdates(_ sender: Any?) { + updaterController.checkForUpdates(sender) + } + @IBAction func newWindow(_ sender: Any?) { terminalManager.newWindow() diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 724f21355..828e82bd0 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -76,6 +76,9 @@ + + + diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 65e91ce83..7b9e49f4c 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -550,6 +550,9 @@ extension Ghostty { case GHOSTTY_ACTION_RING_BELL: ringBell(app, target: target) + case GHOSTTY_ACTION_CHECK_FOR_UPDATES: + checkForUpdates(app) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -588,6 +591,14 @@ extension Ghostty { #endif } + private static func checkForUpdates( + _ app: ghostty_app_t, + ) { + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + appDelegate.checkForUpdates(nil) + } + } + private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { switch (target.tag) { case GHOSTTY_TARGET_APP: diff --git a/src/App.zig b/src/App.zig index 15859d115..005b745a6 100644 --- a/src/App.zig +++ b/src/App.zig @@ -444,6 +444,7 @@ pub fn performAction( .close_all_windows => _ = try rt_app.performAction(.app, .close_all_windows, {}), .toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}), .toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}), + .check_for_updates => _ = try rt_app.performAction(.app, .check_for_updates, {}), } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 4be296f09..8a23bc1a4 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -255,6 +255,8 @@ pub const Action = union(Key) { /// it needs to ring the bell. This is usually a sound or visual effect. ring_bell, + check_for_updates, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -301,6 +303,7 @@ pub const Action = union(Key) { config_change, close_window, ring_bell, + check_for_updates, }; /// Sync with: ghostty_action_u diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 89c5e4352..a22eb174d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -509,6 +509,11 @@ pub const Action = union(enum) { /// This currently only works on macOS. toggle_visibility, + /// Check for updates. + /// + /// This currently only works on macOS. + check_for_updates, + /// Quit ghostty. quit, @@ -791,6 +796,7 @@ pub const Action = union(enum) { .quit, .toggle_quick_terminal, .toggle_visibility, + .check_for_updates, => .app, // These are app but can be special-cased in a surface context. diff --git a/src/input/command.zig b/src/input/command.zig index 1f685269b..8ef4a5f0e 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -364,6 +364,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle secure input mode.", }}, + .check_for_updates => comptime &.{.{ + .action = .check_for_updates, + .title = "Check for Updates", + .description = "Check for updates to the application.", + }}, + .quit => comptime &.{.{ .action = .quit, .title = "Quit", From f6d56f4f03ceb33b52e8cc9358eb7128807652f4 Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Thu, 15 May 2025 23:26:47 +0800 Subject: [PATCH 254/642] Handle check_for_updates as unimplemented action --- src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 1 + 2 files changed, 2 insertions(+) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index e416d5645..221d5344a 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -249,6 +249,7 @@ pub const App = struct { .prompt_title, .reset_window_size, .ring_bell, + .check_for_updates, => { log.info("unimplemented action={}", .{action}); return false; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 06cc41b9d..cddcf7159 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -504,6 +504,7 @@ pub fn performAction( .renderer_health, .color_change, .reset_window_size, + .check_for_updates, => { log.warn("unimplemented action={}", .{action}); return false; From 048e4acb2c744620b6aa9de5b2e26dfd2bb1b3cb Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Tue, 22 Apr 2025 13:09:15 +0800 Subject: [PATCH 255/642] gtk: implement command palette --- src/apprt/gtk/App.zig | 21 ++- src/apprt/gtk/CommandPalette.zig | 229 +++++++++++++++++++++++ src/apprt/gtk/Window.zig | 22 ++- src/apprt/gtk/gresource.zig | 1 + src/apprt/gtk/style.css | 16 ++ src/apprt/gtk/ui/1.5/command-palette.blp | 106 +++++++++++ src/config/Config.zig | 14 +- src/input/Binding.zig | 2 - 8 files changed, 399 insertions(+), 12 deletions(-) create mode 100644 src/apprt/gtk/CommandPalette.zig create mode 100644 src/apprt/gtk/ui/1.5/command-palette.blp diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 06cc41b9d..e29cbc306 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -492,11 +492,11 @@ pub fn performAction( .toggle_quick_terminal => return try self.toggleQuickTerminal(), .secure_input => self.setSecureInput(target, value), .ring_bell => try self.ringBell(target), + .toggle_command_palette => try self.toggleCommandPalette(target), // Unimplemented .close_all_windows, .float_window, - .toggle_command_palette, .toggle_visibility, .cell_size, .key_sequence, @@ -750,7 +750,7 @@ fn toggleWindowDecorations( .surface => |v| { const window = v.rt_surface.container.window() orelse { log.info( - "toggleFullscreen invalid for container={s}", + "toggleWindowDecorations invalid for container={s}", .{@tagName(v.rt_surface.container)}, ); return; @@ -792,6 +792,23 @@ fn ringBell(_: *App, target: apprt.Target) !void { } } +fn toggleCommandPalette(_: *App, target: apprt.Target) !void { + switch (target) { + .app => {}, + .surface => |surface| { + const window = surface.rt_surface.container.window() orelse { + log.info( + "toggleCommandPalette invalid for container={s}", + .{@tagName(surface.rt_surface.container)}, + ); + return; + }; + + window.toggleCommandPalette(); + }, + } +} + fn quitTimer(self: *App, mode: apprt.action.QuitTimer) void { switch (mode) { .start => self.startQuitTimer(), diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig new file mode 100644 index 000000000..ce6d035a5 --- /dev/null +++ b/src/apprt/gtk/CommandPalette.zig @@ -0,0 +1,229 @@ +const CommandPalette = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const adw = @import("adw"); +const gio = @import("gio"); +const gobject = @import("gobject"); +const gtk = @import("gtk"); + +const configpkg = @import("../../config.zig"); +const inputpkg = @import("../../input.zig"); +const key = @import("key.zig"); +const Builder = @import("Builder.zig"); +const Window = @import("Window.zig"); + +const log = std.log.scoped(.command_palette); + +window: *Window, + +arena: std.heap.ArenaAllocator, + +/// The dialog object containing the palette UI. +dialog: *adw.Dialog, + +/// The search input text field. +search: *gtk.SearchEntry, + +/// The view containing each result row. +view: *gtk.ListView, + +/// The model that provides filtered data for the view to display. +model: *gio.ListModel, + +/// The list that serves as the data source of the model. +/// This is where all command data is ultimately stored. +source: *gio.ListStore, + +pub fn init(self: *CommandPalette, window: *Window) !void { + // Register the custom command type *before* initializing the builder + // If we don't do this now, the builder will complain that it doesn't know + // about this type and fail to initialize + _ = Command.getGObjectType(); + + var builder = Builder.init("command-palette", 1, 5); + + self.* = .{ + .window = window, + .arena = .init(window.app.core_app.alloc), + .dialog = builder.getObject(adw.Dialog, "command-palette").?, + .search = builder.getObject(gtk.SearchEntry, "search").?, + .view = builder.getObject(gtk.ListView, "view").?, + .model = builder.getObject(gio.ListModel, "model").?, + .source = builder.getObject(gio.ListStore, "source").?, + }; + + // Manually take a reference here so that the dialog + // remains in memory after closing + self.dialog.ref(); + errdefer self.dialog.unref(); + + _ = gtk.SearchEntry.signals.stop_search.connect( + self.search, + *CommandPalette, + searchStopped, + self, + .{}, + ); + + _ = gtk.SearchEntry.signals.activate.connect( + self.search, + *CommandPalette, + searchActivated, + self, + .{}, + ); + + _ = gtk.ListView.signals.activate.connect( + self.view, + *CommandPalette, + rowActivated, + self, + .{}, + ); + + try self.updateConfig(&self.window.app.config); +} + +pub fn deinit(self: *CommandPalette) void { + self.arena.deinit(); + self.dialog.unref(); +} + +pub fn toggle(self: *CommandPalette) void { + self.dialog.present(self.window.window.as(gtk.Widget)); +} + +pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void { + // Clear existing binds and clear allocated data + self.source.removeAll(); + _ = self.arena.reset(.retain_capacity); + + // TODO: Allow user-configured palette entries + for (inputpkg.command.defaults) |command| { + const cmd = try Command.new( + self.arena.allocator(), + command, + config.keybind.set, + ); + self.source.append(cmd.as(gobject.Object)); + } +} + +fn activated(self: *CommandPalette, pos: c_uint) void { + // Use self.model and not self.source here to use the list of *visible* results + const object = self.model.as(gio.ListModel).getObject(pos) orelse return; + const cmd = gobject.ext.cast(Command, object) orelse return; + + const action = inputpkg.Binding.Action.parse( + std.mem.span(cmd.cmd_c.action_key), + ) catch |err| { + log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err }); + return; + }; + + self.window.performBindingAction(action); + _ = self.dialog.close(); +} + +fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { + // ESC was pressed - close the palette + _ = self.dialog.close(); +} + +fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { + // If Enter is pressed in the search bar, + // then activate the first entry (if any) + self.activated(0); +} + +fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void { + self.activated(pos); +} + +/// Object that wraps around a command. +/// +/// As GTK list models only accept objects that are within the GObject hierarchy, +/// we have to construct a wrapper to be easily consumed by the list model. +const Command = extern struct { + parent: Parent, + cmd_c: inputpkg.Command.C, + + pub const getGObjectType = gobject.ext.defineClass(Command, .{ + .name = "GhosttyCommand", + .classInit = Class.init, + }); + + pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command { + const self = gobject.ext.newInstance(Command, .{}); + var buf: [64]u8 = undefined; + + const action = action: { + const trigger = keybinds.getTrigger(cmd.action) orelse break :action null; + const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null; + break :action try alloc.dupeZ(u8, accel); + }; + + self.cmd_c = .{ + .title = cmd.title.ptr, + .description = cmd.description.ptr, + .action = if (action) |v| v.ptr else "", + .action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}), + }; + + return self; + } + + fn as(self: *Command, comptime T: type) *T { + return gobject.ext.as(T, self); + } + + pub const Parent = gobject.Object; + + pub const Class = extern struct { + parent: Parent.Class, + + pub const Instance = Command; + + pub fn init(class: *Class) callconv(.c) void { + const info = @typeInfo(inputpkg.Command.C).@"struct"; + + // Expose all fields on the Command.C struct as properties + // that can be accessed by the GObject type system + // (and by extension, blueprints) + const properties = comptime props: { + var props: [info.fields.len]type = undefined; + + for (info.fields, 0..) |field, i| { + const accessor = struct { + fn getter(cmd: *Command) ?[:0]const u8 { + return std.mem.span(@field(cmd.cmd_c, field.name)); + } + }; + + // "Canonicalize" field names into the format GObject expects + const prop_name = prop_name: { + var buf: [field.name.len:0]u8 = undefined; + _ = std.mem.replace(u8, field.name, "_", "-", &buf); + break :prop_name buf; + }; + + props[i] = gobject.ext.defineProperty( + &prop_name, + Command, + ?[:0]const u8, + .{ + .default = null, + .accessor = .{ .getter = &accessor.getter }, + }, + ); + } + + break :props props; + }; + + gobject.ext.registerProperties(class, &properties); + } + }; +}; diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d82087ff0..f2dde2ab9 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -34,6 +34,7 @@ const gtk_key = @import("key.zig"); const TabView = @import("TabView.zig"); const HeaderBar = @import("headerbar.zig"); const CloseDialog = @import("CloseDialog.zig"); +const CommandPalette = @import("CommandPalette.zig"); const winprotopkg = @import("winproto.zig"); const gtk_version = @import("gtk_version.zig"); const adw_version = @import("adw_version.zig"); @@ -67,6 +68,9 @@ titlebar_menu: Menu(Window, "titlebar_menu", true), /// The libadwaita widget for receiving toast send requests. toast_overlay: *adw.ToastOverlay, +/// The command palette. +command_palette: CommandPalette, + /// See adwTabOverviewOpen for why we have this. adw_tab_overview_focus_timer: ?c_uint = null, @@ -139,6 +143,7 @@ pub fn init(self: *Window, app: *App) !void { .notebook = undefined, .titlebar_menu = undefined, .toast_overlay = undefined, + .command_palette = undefined, .winproto = .none, }; @@ -167,6 +172,8 @@ pub fn init(self: *Window, app: *App) !void { // Setup our notebook self.notebook.init(self); + if (adw_version.supportsDialogs()) try self.command_palette.init(self); + // If we are using Adwaita, then we can support the tab overview. self.tab_overview = if (adw_version.supportsTabOverview()) overview: { const tab_overview = adw.TabOverview.new(); @@ -460,6 +467,9 @@ pub fn updateConfig( // We always resync our appearance whenever the config changes. try self.syncAppearance(); + + // Update binds inside the command palette + try self.command_palette.updateConfig(config); } /// Updates appearance based on config settings. Will be called once upon window @@ -600,6 +610,7 @@ fn initActions(self: *Window) void { pub fn deinit(self: *Window) void { self.winproto.deinit(self.app.core_app.alloc); + if (adw_version.supportsDialogs()) self.command_palette.deinit(); if (self.adw_tab_overview_focus_timer) |timer| { _ = glib.Source.remove(timer); @@ -729,6 +740,15 @@ pub fn toggleWindowDecorations(self: *Window) void { }; } +/// Toggle the window decorations for this window. +pub fn toggleCommandPalette(self: *Window) void { + if (adw_version.supportsDialogs()) { + self.command_palette.toggle(); + } else { + log.warn("libadwaita 1.5+ is required for the command palette", .{}); + } +} + /// Grabs focus on the currently selected tab. pub fn focusCurrentTab(self: *Window) void { const tab = self.notebook.currentTab() orelse return; @@ -820,7 +840,7 @@ fn gtkWindowUpdateScaleFactor( } /// Perform a binding action on the window's action surface. -fn performBindingAction(self: *Window, action: input.Binding.Action) void { +pub fn performBindingAction(self: *Window, action: input.Binding.Action) void { const surface = self.actionSurface() orelse return; _ = surface.performBindingAction(action) catch |err| { log.warn("error performing binding action error={}", .{err}); diff --git a/src/apprt/gtk/gresource.zig b/src/apprt/gtk/gresource.zig index a1db8ac62..45623ab2a 100644 --- a/src/apprt/gtk/gresource.zig +++ b/src/apprt/gtk/gresource.zig @@ -63,6 +63,7 @@ pub const blueprint_files = [_]VersionedBlueprint{ .{ .major = 1, .minor = 5, .name = "prompt-title-dialog" }, .{ .major = 1, .minor = 5, .name = "config-errors-dialog" }, .{ .major = 1, .minor = 0, .name = "menu-headerbar-split_menu" }, + .{ .major = 1, .minor = 5, .name = "command-palette" }, .{ .major = 1, .minor = 0, .name = "menu-surface-context_menu" }, .{ .major = 1, .minor = 0, .name = "menu-window-titlebar_menu" }, .{ .major = 1, .minor = 5, .name = "ccw-osc-52-read" }, diff --git a/src/apprt/gtk/style.css b/src/apprt/gtk/style.css index ecaef6b33..7c4b53d03 100644 --- a/src/apprt/gtk/style.css +++ b/src/apprt/gtk/style.css @@ -73,3 +73,19 @@ window.ssd.no-border-radius { filter: blur(5px); transition: filter 0.3s ease; } + +.command-palette-search { + font-size: 1.25rem; + padding: 4px; + -gtk-icon-size: 20px; +} + +.command-palette-search > image:first-child { + margin-left: 8px; + margin-right: 4px; +} + +.command-palette-search > image:last-child { + margin-left: 4px; + margin-right: 8px; +} diff --git a/src/apprt/gtk/ui/1.5/command-palette.blp b/src/apprt/gtk/ui/1.5/command-palette.blp new file mode 100644 index 000000000..76bcc1700 --- /dev/null +++ b/src/apprt/gtk/ui/1.5/command-palette.blp @@ -0,0 +1,106 @@ +using Gtk 4.0; +using Gio 2.0; +using Adw 1; + +Adw.Dialog command-palette { + content-width: 700; + + Adw.ToolbarView { + top-bar-style: flat; + + [top] + Adw.HeaderBar { + [title] + SearchEntry search { + hexpand: true; + placeholder-text: _("Execute a command…"); + + styles [ + "command-palette-search", + ] + } + } + + ScrolledWindow { + min-content-height: 300; + + ListView view { + show-separators: true; + single-click-activate: true; + + model: NoSelection model { + model: FilterListModel { + incremental: true; + + filter: AnyFilter { + StringFilter { + expression: expr item as <$GhosttyCommand>.title; + search: bind search.text; + } + + StringFilter { + expression: expr item as <$GhosttyCommand>.action-key; + search: bind search.text; + } + }; + + model: Gio.ListStore source { + item-type: typeof<$GhosttyCommand>; + }; + }; + }; + + styles [ + "rich-list", + ] + + factory: BuilderListItemFactory { + template ListItem { + child: Box { + orientation: horizontal; + spacing: 10; + tooltip-text: bind template.item as <$GhosttyCommand>.description; + + Box { + orientation: vertical; + hexpand: true; + + Label { + ellipsize: end; + halign: start; + wrap: false; + single-line-mode: true; + + styles [ + "title", + ] + + label: bind template.item as <$GhosttyCommand>.title; + } + + Label { + ellipsize: end; + halign: start; + wrap: false; + single-line-mode: true; + + styles [ + "subtitle", + "monospace", + ] + + label: bind template.item as <$GhosttyCommand>.action-key; + } + } + + ShortcutLabel { + accelerator: bind template.item as <$GhosttyCommand>.action; + valign: center; + } + }; + } + }; + } + } + } +} diff --git a/src/config/Config.zig b/src/config/Config.zig index ca330f8f6..765b63c46 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4736,6 +4736,13 @@ pub const Keybinds = struct { .{ .toggle_split_zoom = {} }, ); + // Toggle command palette, matches VSCode + try self.set.put( + alloc, + .{ .key = .{ .unicode = 'p' }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) }, + .toggle_command_palette, + ); + // Mac-specific keyboard bindings. if (comptime builtin.target.os.tag.isDarwin()) { try self.set.put( @@ -4908,13 +4915,6 @@ pub const Keybinds = struct { .{ .jump_to_prompt = 1 }, ); - // Toggle command palette, matches VSCode - try self.set.put( - alloc, - .{ .key = .{ .unicode = 'p' }, .mods = .{ .super = true, .shift = true } }, - .{ .toggle_command_palette = {} }, - ); - // Inspector, matching Chromium try self.set.put( alloc, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 89c5e4352..eed137948 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -455,8 +455,6 @@ pub const Action = union(enum) { /// that lets you see what actions you can perform, their associated /// keybindings (if any), a search bar to filter the actions, and /// the ability to then execute the action. - /// - /// This only works on macOS. toggle_command_palette, /// Toggle the "quick" terminal. The quick terminal is a terminal that From 3b013b117487ff4c7b8e3a2a335a1624143a61c0 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 23 Apr 2025 15:55:23 +0800 Subject: [PATCH 256/642] gtk: add command palette to titlebar menu --- src/apprt/gtk/App.zig | 1 + src/apprt/gtk/Window.zig | 9 +++++++++ src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index e29cbc306..4fbdec7a7 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1047,6 +1047,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("app.open-config", .{ .open_config = {} }); try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} }); try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle }); + try self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette); try self.syncActionAccelerator("win.close", .{ .close_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} }); diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index f2dde2ab9..4a5926a97 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -587,6 +587,7 @@ fn initActions(self: *Window) void { .{ "split-left", gtkActionSplitLeft }, .{ "split-up", gtkActionSplitUp }, .{ "toggle-inspector", gtkActionToggleInspector }, + .{ "toggle-command-palette", gtkActionToggleCommandPalette }, .{ "copy", gtkActionCopy }, .{ "paste", gtkActionPaste }, .{ "reset", gtkActionReset }, @@ -1102,6 +1103,14 @@ fn gtkActionToggleInspector( self.performBindingAction(.{ .inspector = .toggle }); } +fn gtkActionToggleCommandPalette( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *Window, +) callconv(.C) void { + self.performBindingAction(.toggle_command_palette); +} + fn gtkActionCopy( _: *gio.SimpleAction, _: ?*glib.Variant, diff --git a/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp b/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp index 71e7d060c..3273aa81c 100644 --- a/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp +++ b/src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp @@ -81,6 +81,11 @@ menu menu { } section { + item { + label: _("Command Palette"); + action: "win.toggle-command-palette"; + } + item { label: _("Terminal Inspector"); action: "win.toggle-inspector"; From e97dfc2e196fc5cedf754f433a48d9e8f4889df6 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 23 Apr 2025 16:03:09 +0800 Subject: [PATCH 257/642] gtk(command_palette): filter out certain actions --- src/apprt/gtk/CommandPalette.zig | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index ce6d035a5..b72eaa8d2 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -102,6 +102,16 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi // TODO: Allow user-configured palette entries for (inputpkg.command.defaults) |command| { + // Filter out actions that are not implemented + // or don't make sense for GTK + switch (command.action) { + .close_all_windows, + .toggle_secure_input, + => continue, + + else => {}, + } + const cmd = try Command.new( self.arena.allocator(), command, From 91f811bfbf1e96cff608e181d327ee1e154e828d Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 23 Apr 2025 16:58:45 +0800 Subject: [PATCH 258/642] translations: update --- po/com.mitchellh.ghostty.pot | 42 +++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index 3892d14d8..d6a99d01d 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-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -50,7 +50,7 @@ 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 msgid "Reload Configuration" msgstr "" @@ -78,6 +78,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" @@ -115,7 +119,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:255 msgid "New Tab" msgstr "" @@ -143,20 +147,24 @@ 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:1024 msgid "About Ghostty" msgstr "" -#: 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 "" @@ -197,31 +205,35 @@ msgid "" "commands may be executed." msgstr "" -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:208 msgid "Main Menu" msgstr "" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:229 msgid "View Open Tabs" msgstr "" -#: src/apprt/gtk/Window.zig:249 +#: src/apprt/gtk/Window.zig:256 msgid "New Split" msgstr "" -#: src/apprt/gtk/Window.zig:312 +#: src/apprt/gtk/Window.zig:319 msgid "" "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" -#: src/apprt/gtk/Window.zig:744 +#: src/apprt/gtk/Window.zig:765 msgid "Reloaded the configuration" msgstr "" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:1005 msgid "Ghostty Developers" msgstr "" +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "" + #: src/apprt/gtk/CloseDialog.zig:47 msgid "Close" msgstr "" @@ -261,7 +273,3 @@ msgstr "" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" msgstr "" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "" From 7293d91f1095d9ae7f380ff29c073ad3c5618de0 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Thu, 24 Apr 2025 12:04:39 +0800 Subject: [PATCH 259/642] translations(zh_CN): update --- po/zh_CN.UTF-8.po | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index 80c3766aa..ee2c51362 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-04-22 08:57-0700\n" +"POT-Creation-Date: 2025-04-23 16:58+0800\n" "PO-Revision-Date: 2025-02-27 09:16+0100\n" "Last-Translator: Leah \n" "Language-Team: Chinese (simplified) \n" @@ -51,7 +51,7 @@ 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 msgid "Reload Configuration" msgstr "重新加载配置" @@ -79,6 +79,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" @@ -116,7 +120,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:255 msgid "New Tab" msgstr "新建标签页" @@ -144,20 +148,24 @@ 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:1024 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 "退出" @@ -198,31 +206,35 @@ msgid "" "commands may be executed." msgstr "将以下内容粘贴至终端内将可能执行有害命令。" -#: src/apprt/gtk/Window.zig:201 +#: src/apprt/gtk/Window.zig:208 msgid "Main Menu" msgstr "主菜单" -#: src/apprt/gtk/Window.zig:222 +#: src/apprt/gtk/Window.zig:229 msgid "View Open Tabs" msgstr "浏览标签页" -#: src/apprt/gtk/Window.zig:249 +#: src/apprt/gtk/Window.zig:256 msgid "New Split" -msgstr "" +msgstr "新建分屏" -#: src/apprt/gtk/Window.zig:312 +#: 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:744 +#: src/apprt/gtk/Window.zig:765 msgid "Reloaded the configuration" msgstr "已重新加载配置" -#: src/apprt/gtk/Window.zig:984 +#: src/apprt/gtk/Window.zig:1005 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 "关闭" @@ -262,7 +274,3 @@ msgstr "分屏内正在运行中的进程将被终止。" #: src/apprt/gtk/Surface.zig:1243 msgid "Copied to clipboard" msgstr "已复制至剪贴板" - -#: src/apprt/gtk/inspector.zig:144 -msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty 终端调试器" From 2800e0c99b8b95c84200c2fba736f9298e77e9ca Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Thu, 24 Apr 2025 12:39:36 +0800 Subject: [PATCH 260/642] gtk(command_palette): address feedback related to selections See #7173, #7175 --- src/apprt/gtk/CommandPalette.zig | 9 ++++----- src/apprt/gtk/ui/1.5/command-palette.blp | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index b72eaa8d2..07b63d99c 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -30,7 +30,7 @@ search: *gtk.SearchEntry, view: *gtk.ListView, /// The model that provides filtered data for the view to display. -model: *gio.ListModel, +model: *gtk.SingleSelection, /// The list that serves as the data source of the model. /// This is where all command data is ultimately stored. @@ -50,7 +50,7 @@ pub fn init(self: *CommandPalette, window: *Window) !void { .dialog = builder.getObject(adw.Dialog, "command-palette").?, .search = builder.getObject(gtk.SearchEntry, "search").?, .view = builder.getObject(gtk.ListView, "view").?, - .model = builder.getObject(gio.ListModel, "model").?, + .model = builder.getObject(gtk.SingleSelection, "model").?, .source = builder.getObject(gio.ListStore, "source").?, }; @@ -143,9 +143,8 @@ fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { } fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { - // If Enter is pressed in the search bar, - // then activate the first entry (if any) - self.activated(0); + // If Enter is pressed, activate the selected entry + self.activated(self.model.getSelected()); } fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void { diff --git a/src/apprt/gtk/ui/1.5/command-palette.blp b/src/apprt/gtk/ui/1.5/command-palette.blp index 76bcc1700..a84482091 100644 --- a/src/apprt/gtk/ui/1.5/command-palette.blp +++ b/src/apprt/gtk/ui/1.5/command-palette.blp @@ -28,7 +28,7 @@ Adw.Dialog command-palette { show-separators: true; single-click-activate: true; - model: NoSelection model { + model: SingleSelection model { model: FilterListModel { incremental: true; From cc65dfc90ee41e0925c0664466287c3803ddf858 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Thu, 15 May 2025 17:58:55 +0200 Subject: [PATCH 261/642] gtk(command_palette): focus fixes --- src/apprt/gtk/CommandPalette.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index 07b63d99c..fda2c5ca8 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -93,6 +93,9 @@ pub fn deinit(self: *CommandPalette) void { pub fn toggle(self: *CommandPalette) void { self.dialog.present(self.window.window.as(gtk.Widget)); + + // Focus on the search bar when opening the dialog + self.dialog.setFocus(self.search.as(gtk.Widget)); } pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void { @@ -126,6 +129,12 @@ fn activated(self: *CommandPalette, pos: c_uint) void { const object = self.model.as(gio.ListModel).getObject(pos) orelse return; const cmd = gobject.ext.cast(Command, object) orelse return; + // Close before running the action in order to avoid being replaced by another + // dialog (such as the change title dialog). If that occurs then the command + // palette dialog won't be counted as having closed properly and cannot + // receive focus when reopened. + _ = self.dialog.close(); + const action = inputpkg.Binding.Action.parse( std.mem.span(cmd.cmd_c.action_key), ) catch |err| { @@ -134,7 +143,6 @@ fn activated(self: *CommandPalette, pos: c_uint) void { }; self.window.performBindingAction(action); - _ = self.dialog.close(); } fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void { From f343e1ba461b00baf380539ac7a8f161d600f60e Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Fri, 16 May 2025 00:40:25 +0800 Subject: [PATCH 262/642] Fix comma typo --- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 7b9e49f4c..6736449a4 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -592,7 +592,7 @@ extension Ghostty { } private static func checkForUpdates( - _ app: ghostty_app_t, + _ app: ghostty_app_t ) { if let appDelegate = NSApplication.shared.delegate as? AppDelegate { appDelegate.checkForUpdates(nil) From d6dea79bde1f2060fce56d178102db2c80773332 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 5 Feb 2025 13:58:01 +0100 Subject: [PATCH 263/642] gtk: add option to always display the tab bar Also fixes crashes in both vanilla GTK and Adwaita implementations of `closeTab`, which erroneously close windows twice when there are no more tabs left (we probably already handle it somewhere else). --- src/apprt/gtk/Window.zig | 56 ++++++++++++++++++++++------------------ src/config/Config.zig | 29 ++++++++++++++++++++- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index d82087ff0..6bed56ec0 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -54,6 +54,9 @@ window: *adw.ApplicationWindow, /// The header bar for the window. headerbar: HeaderBar, +/// The tab bar for the window. +tab_bar: *adw.TabBar, + /// The tab overview for the window. This is possibly null since there is no /// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0). tab_overview: ?*adw.TabOverview, @@ -82,6 +85,7 @@ pub const DerivedConfig = struct { gtk_tabs_location: configpkg.Config.GtkTabsLocation, gtk_wide_tabs: bool, gtk_toolbar_style: configpkg.Config.GtkToolbarStyle, + window_show_tab_bar: configpkg.Config.WindowShowTabBar, quick_terminal_position: configpkg.Config.QuickTerminalPosition, quick_terminal_size: configpkg.Config.QuickTerminalSize, @@ -101,6 +105,7 @@ pub const DerivedConfig = struct { .gtk_tabs_location = config.@"gtk-tabs-location", .gtk_wide_tabs = config.@"gtk-wide-tabs", .gtk_toolbar_style = config.@"gtk-toolbar-style", + .window_show_tab_bar = config.@"window-show-tab-bar", .quick_terminal_position = config.@"quick-terminal-position", .quick_terminal_size = config.@"quick-terminal-size", @@ -135,6 +140,7 @@ pub fn init(self: *Window, app: *App) !void { .config = DerivedConfig.init(&app.config), .window = undefined, .headerbar = undefined, + .tab_bar = undefined, .tab_overview = null, .notebook = undefined, .titlebar_menu = undefined, @@ -216,8 +222,9 @@ pub fn init(self: *Window, app: *App) !void { // If we're using an AdwWindow then we can support the tab overview. if (self.tab_overview) |tab_overview| { if (!adw_version.supportsTabOverview()) unreachable; - const btn = switch (self.config.gtk_tabs_location) { - .top, .bottom => btn: { + + const btn = switch (self.config.window_show_tab_bar) { + .always, .auto => btn: { const btn = gtk.ToggleButton.new(); btn.as(gtk.Widget).setTooltipText(i18n._("View Open Tabs")); btn.as(gtk.Button).setIconName("view-grid-symbolic"); @@ -229,8 +236,7 @@ pub fn init(self: *Window, app: *App) !void { ); break :btn btn.as(gtk.Widget); }, - - .hidden => btn: { + .never => btn: { const btn = adw.TabButton.new(); btn.setView(self.notebook.tab_view); btn.as(gtk.Actionable).setActionName("overview.open"); @@ -376,21 +382,16 @@ pub fn init(self: *Window, app: *App) !void { // Our actions for the menu initActions(self); + self.tab_bar = adw.TabBar.new(); + self.tab_bar.setView(self.notebook.tab_view); + if (adw_version.supportsToolbarView()) { const toolbar_view = adw.ToolbarView.new(); toolbar_view.addTopBar(self.headerbar.asWidget()); - if (self.config.gtk_tabs_location != .hidden) { - const tab_bar = adw.TabBar.new(); - tab_bar.setView(self.notebook.tab_view); - - if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0); - - switch (self.config.gtk_tabs_location) { - .top => toolbar_view.addTopBar(tab_bar.as(gtk.Widget)), - .bottom => toolbar_view.addBottomBar(tab_bar.as(gtk.Widget)), - .hidden => unreachable, - } + switch (self.config.gtk_tabs_location) { + .top => toolbar_view.addTopBar(self.tab_bar.as(gtk.Widget)), + .bottom => toolbar_view.addBottomBar(self.tab_bar.as(gtk.Widget)), } toolbar_view.setContent(box.as(gtk.Widget)); @@ -405,23 +406,18 @@ pub fn init(self: *Window, app: *App) !void { // Set our application window content. self.tab_overview.?.setChild(toolbar_view.as(gtk.Widget)); self.window.setContent(self.tab_overview.?.as(gtk.Widget)); - } else tab_bar: { - if (self.config.gtk_tabs_location == .hidden) break :tab_bar; + } else { // In earlier adwaita versions, we need to add the tabbar manually since we do not use // an AdwToolbarView. - const tab_bar = adw.TabBar.new(); - tab_bar.as(gtk.Widget).addCssClass("inline"); + self.tab_bar.as(gtk.Widget).addCssClass("inline"); + switch (self.config.gtk_tabs_location) { .top => box.insertChildAfter( - tab_bar.as(gtk.Widget), + self.tab_bar.as(gtk.Widget), self.headerbar.asWidget(), ), - .bottom => box.append(tab_bar.as(gtk.Widget)), - .hidden => unreachable, + .bottom => box.append(self.tab_bar.as(gtk.Widget)), } - tab_bar.setView(self.notebook.tab_view); - - if (!self.config.gtk_wide_tabs) tab_bar.setExpandTabs(0); } // If we want the window to be maximized, we do that here. @@ -543,6 +539,16 @@ pub fn syncAppearance(self: *Window) !void { } } + self.tab_bar.setExpandTabs(@intFromBool(self.config.gtk_wide_tabs)); + self.tab_bar.setAutohide(switch (self.config.window_show_tab_bar) { + .auto, .never => @intFromBool(true), + .always => @intFromBool(false), + }); + self.tab_bar.as(gtk.Widget).setVisible(switch (self.config.window_show_tab_bar) { + .always, .auto => @intFromBool(true), + .never => @intFromBool(false), + }); + self.winproto.syncAppearance() catch |err| { log.warn("failed to sync winproto appearance error={}", .{err}); }; diff --git a/src/config/Config.zig b/src/config/Config.zig index ca330f8f6..fd6ae798e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1410,6 +1410,27 @@ keybind: Keybinds = .{}, /// * `end` - Insert the new tab at the end of the tab list. @"window-new-tab-position": WindowNewTabPosition = .current, +/// Whether to show the tab bar. +/// +/// Valid values: +/// +/// - `always` +/// +/// Always display the tab bar, even when there's only one tab. +/// +/// - `auto` *(default)* +/// +/// Automatically show and hide the tab bar. The tab bar is only +/// shown when there are two or more tabs present. +/// +/// - `never` +/// +/// Never show the tab bar. Tabs are only accessible via the tab +/// overview or by keybind actions. +/// +/// Currently only supported on Linux (GTK). +@"window-show-tab-bar": WindowShowTabBar = .auto, + /// Background color for the window titlebar. This only takes effect if /// window-theme is set to ghostty. Currently only supported in the GTK app /// runtime. @@ -5747,7 +5768,6 @@ pub const GtkSingleInstance = enum { pub const GtkTabsLocation = enum { top, bottom, - hidden, }; /// See gtk-toolbar-style @@ -5795,6 +5815,13 @@ pub const WindowNewTabPosition = enum { end, }; +/// See window-show-tab-bar +pub const WindowShowTabBar = enum { + always, + auto, + never, +}; + /// See resize-overlay pub const ResizeOverlay = enum { always, From 709b0214a022b31c570f883d4d513f79c4b6d520 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 15 May 2025 11:37:27 -0600 Subject: [PATCH 264/642] fix(renderer): Don't force images to grid/cell sizes This problem was introduced by f091a69 (PR #6675). I've gone ahead and overhauled the placement positioning logic as well; it was doing a lot of expensive calls before, I've significantly reduced that. Clipping partially off-screen images is now handled entirely by the renderer, rather than while preparing the placement, and as such the grid position passed to the image shader is now signed. --- src/renderer/Metal.zig | 75 +++------- src/renderer/OpenGL.zig | 79 ++++------- src/renderer/metal/image.zig | 8 +- src/renderer/opengl/ImageProgram.zig | 8 +- src/renderer/opengl/image.zig | 4 +- src/terminal/kitty/graphics_storage.zig | 177 ++++++++++++++++-------- 6 files changed, 174 insertions(+), 177 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index ddc94b1ec..99dbc838e 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1872,6 +1872,8 @@ fn prepKittyGraphics( // points. This lets us determine offsets and containment of placements. const top = t.screen.pages.getTopLeft(.viewport); const bot = t.screen.pages.getBottomRight(.viewport).?; + const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; + const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; // Go through the placements and ensure the image is loaded on the GPU. var it = storage.placements.iterator(); @@ -1903,7 +1905,7 @@ fn prepKittyGraphics( continue; }; - try self.prepKittyPlacement(t, &top, &bot, &image, p); + try self.prepKittyPlacement(t, top_y, bot_y, &image, p); } // If we have virtual placements then we need to scan for placeholders. @@ -2009,8 +2011,8 @@ fn prepKittyVirtualPlacement( fn prepKittyPlacement( self: *Metal, t: *terminal.Terminal, - top: *const terminal.Pin, - bot: *const terminal.Pin, + top_y: u32, + bot_y: u32, image: *const terminal.kitty.graphics.Image, p: *const terminal.kitty.graphics.ImageStorage.Placement, ) !void { @@ -2018,78 +2020,47 @@ fn prepKittyPlacement( // a rect then its virtual or something so skip it. const rect = p.rect(image.*, t) orelse return; + // This is expensive but necessary. + const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; + // If the selection isn't within our viewport then skip it. - if (bot.before(rect.top_left)) return; - if (rect.bottom_right.before(top.*)) return; - - // If the top left is outside the viewport we need to calc an offset - // so that we render (0, 0) with some offset for the texture. - const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: { - const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y; - const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const offset_cells = vp_y - img_y; - const offset_pixels = offset_cells * self.grid_metrics.cell_height; - break :offset_y @intCast(offset_pixels); - } else 0; - - // Get the grid size that respects aspect ratio - const grid_size = p.gridSize(image.*, t); - - // If we specify `rows` then our offset above is in viewport space - // and not in the coordinate space of the source image. Without `rows` - // that's one and the same. - const source_offset_y: u32 = if (grid_size.rows > 0) source_offset_y: { - // Determine the scale factor to apply for this row height. - const image_height: f64 = @floatFromInt(image.height); - const viewport_height: f64 = @floatFromInt(grid_size.rows * self.grid_metrics.cell_height); - const scale: f64 = image_height / viewport_height; - - // Apply the scale to the offset - const offset_y_f64: f64 = @floatFromInt(offset_y); - const source_offset_y_f64: f64 = offset_y_f64 * scale; - break :source_offset_y @intFromFloat(@round(source_offset_y_f64)); - } else offset_y; + if (img_top_y > bot_y) return; + if (img_bot_y < top_y) return; // We need to prep this image for upload if it isn't in the cache OR // it is in the cache but the transmit time doesn't match meaning this // image is different. try self.prepKittyImage(image); - // Convert our screen point to a viewport point - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rect.top_left, - ) orelse .{ .viewport = .{} }; + // Calculate the dimensions of our image, taking in to + // account the rows / columns specified by the placement. + const dest_size = p.calculatedSize(image.*, t); // Calculate the source rectangle const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y + source_offset_y); + const source_y = @min(image.height, p.source_y); const source_width = if (p.source_width > 0) @min(image.width - source_x, p.source_width) else image.width; const source_height = if (p.source_height > 0) - @min(image.height, p.source_height) + @min(image.height - source_y, p.source_height) else - image.height -| source_y; + image.height; - // Calculate the width/height of our image. - const dest_width = grid_size.cols * self.grid_metrics.cell_width; - const dest_height = if (grid_size.rows > 0) rows: { - // Clip to the viewport to handle scrolling. offset_y is already in - // viewport scale so we can subtract it directly. - break :rows (grid_size.rows * self.grid_metrics.cell_height) - offset_y; - } else source_height; + // Get the viewport-relative Y position of the placement. + const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); // Accumulate the placement - if (image.width > 0 and image.height > 0) { + if (dest_size.width > 0 and dest_size.height > 0) { try self.image_placements.append(self.alloc, .{ .image_id = image.id, .x = @intCast(rect.top_left.x), - .y = @intCast(viewport.viewport.y), + .y = y_pos, .z = p.z, - .width = dest_width, - .height = dest_height, + .width = dest_size.width, + .height = dest_size.height, .cell_offset_x = p.x_offset, .cell_offset_y = p.y_offset, .source_x = source_x, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index a3a2d8f7e..d0222a390 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -913,6 +913,8 @@ fn prepKittyGraphics( // points. This lets us determine offsets and containment of placements. const top = t.screen.pages.getTopLeft(.viewport); const bot = t.screen.pages.getBottomRight(.viewport).?; + const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; + const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; // Go through the placements and ensure the image is loaded on the GPU. var it = storage.placements.iterator(); @@ -944,7 +946,7 @@ fn prepKittyGraphics( continue; }; - try self.prepKittyPlacement(t, &top, &bot, &image, p); + try self.prepKittyPlacement(t, top_y, bot_y, &image, p); } // If we have virtual placements then we need to scan for placeholders. @@ -1050,8 +1052,8 @@ fn prepKittyVirtualPlacement( fn prepKittyPlacement( self: *OpenGL, t: *terminal.Terminal, - top: *const terminal.Pin, - bot: *const terminal.Pin, + top_y: u32, + bot_y: u32, image: *const terminal.kitty.graphics.Image, p: *const terminal.kitty.graphics.ImageStorage.Placement, ) !void { @@ -1059,78 +1061,47 @@ fn prepKittyPlacement( // a rect then its virtual or something so skip it. const rect = p.rect(image.*, t) orelse return; + // This is expensive but necessary. + const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; + // If the selection isn't within our viewport then skip it. - if (bot.before(rect.top_left)) return; - if (rect.bottom_right.before(top.*)) return; - - // If the top left is outside the viewport we need to calc an offset - // so that we render (0, 0) with some offset for the texture. - const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: { - const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y; - const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const offset_cells = vp_y - img_y; - const offset_pixels = offset_cells * self.grid_metrics.cell_height; - break :offset_y @intCast(offset_pixels); - } else 0; - - // Get the grid size that respects aspect ratio - const grid_size = p.gridSize(image.*, t); - - // If we specify `rows` then our offset above is in viewport space - // and not in the coordinate space of the source image. Without `rows` - // that's one and the same. - const source_offset_y: u32 = if (grid_size.rows > 0) source_offset_y: { - // Determine the scale factor to apply for this row height. - const image_height: f64 = @floatFromInt(image.height); - const viewport_height: f64 = @floatFromInt(grid_size.rows * self.grid_metrics.cell_height); - const scale: f64 = image_height / viewport_height; - - // Apply the scale to the offset - const offset_y_f64: f64 = @floatFromInt(offset_y); - const source_offset_y_f64: f64 = offset_y_f64 * scale; - break :source_offset_y @intFromFloat(@round(source_offset_y_f64)); - } else offset_y; + if (img_top_y > bot_y) return; + if (img_bot_y < top_y) return; // We need to prep this image for upload if it isn't in the cache OR // it is in the cache but the transmit time doesn't match meaning this // image is different. try self.prepKittyImage(image); - // Convert our screen point to a viewport point - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rect.top_left, - ) orelse .{ .viewport = .{} }; + // Calculate the dimensions of our image, taking in to + // account the rows / columns specified by the placement. + const dest_size = p.calculatedSize(image.*, t); // Calculate the source rectangle const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y + source_offset_y); + const source_y = @min(image.height, p.source_y); const source_width = if (p.source_width > 0) @min(image.width - source_x, p.source_width) else image.width; const source_height = if (p.source_height > 0) - @min(image.height, p.source_height) + @min(image.height - source_y, p.source_height) else - image.height -| source_y; + image.height; - // Calculate the width/height of our image. - const dest_width = grid_size.cols * self.grid_metrics.cell_width; - const dest_height = if (grid_size.rows > 0) rows: { - // Clip to the viewport to handle scrolling. offset_y is already in - // viewport scale so we can subtract it directly. - break :rows (grid_size.rows * self.grid_metrics.cell_height) - offset_y; - } else source_height; + // Get the viewport-relative Y position of the placement. + const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); // Accumulate the placement - if (image.width > 0 and image.height > 0) { + if (dest_size.width > 0 and dest_size.height > 0) { try self.image_placements.append(self.alloc, .{ .image_id = image.id, .x = @intCast(rect.top_left.x), - .y = @intCast(viewport.viewport.y), + .y = y_pos, .z = p.z, - .width = dest_width, - .height = dest_height, + .width = dest_size.width, + .height = dest_size.height, .cell_offset_x = p.x_offset, .cell_offset_y = p.y_offset, .source_x = source_x, @@ -2511,8 +2482,8 @@ fn drawImages( // Setup our data try bind.vbo.setData(ImageProgram.Input{ - .grid_col = @intCast(p.x), - .grid_row = @intCast(p.y), + .grid_col = p.x, + .grid_row = p.y, .cell_offset_x = p.cell_offset_x, .cell_offset_y = p.cell_offset_y, .source_x = p.source_x, diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index cb675404d..ff13a49e8 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -13,16 +13,16 @@ pub const Placement = struct { image_id: u32, /// The grid x/y where this placement is located. - x: u32, - y: u32, + x: i32, + y: i32, z: i32, /// The width/height of the placed image. width: u32, height: u32, - /// The offset in pixels from the top left of the cell. This is - /// clamped to the size of a cell. + /// The offset in pixels from the top left of the cell. + /// This is clamped to the size of a cell. cell_offset_x: u32, cell_offset_y: u32, diff --git a/src/renderer/opengl/ImageProgram.zig b/src/renderer/opengl/ImageProgram.zig index e53891818..ff6794085 100644 --- a/src/renderer/opengl/ImageProgram.zig +++ b/src/renderer/opengl/ImageProgram.zig @@ -11,8 +11,8 @@ vbo: gl.Buffer, pub const Input = extern struct { /// vec2 grid_coord - grid_col: u16, - grid_row: u16, + grid_col: i32, + grid_row: i32, /// vec2 cell_offset cell_offset_x: u32 = 0, @@ -66,8 +66,8 @@ pub fn init() !ImageProgram { var vbobind = try vbo.bind(.array); defer vbobind.unbind(); var offset: usize = 0; - try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Input), offset); - offset += 2 * @sizeOf(u16); + try vbobind.attributeAdvanced(0, 2, gl.c.GL_INT, false, @sizeOf(Input), offset); + offset += 2 * @sizeOf(i32); try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); offset += 2 * @sizeOf(u32); try vbobind.attributeAdvanced(2, 4, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); diff --git a/src/renderer/opengl/image.zig b/src/renderer/opengl/image.zig index 85f59f1f3..b22d10ea3 100644 --- a/src/renderer/opengl/image.zig +++ b/src/renderer/opengl/image.zig @@ -11,8 +11,8 @@ pub const Placement = struct { image_id: u32, /// The grid x/y where this placement is located. - x: u32, - y: u32, + x: i32, + y: i32, z: i32, /// The width/height of the placed image. diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 06769dc3c..6e336e785 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -658,6 +658,86 @@ pub const ImageStorage = struct { } } + /// Calculates the size of this placement's image in pixels, + /// taking in to account the specified rows and columns. + pub fn calculatedSize( + self: Placement, + image: Image, + t: *const terminal.Terminal, + ) struct { + width: u32, + height: u32, + } { + // Height / width of the image in px. + const width = if (self.source_width > 0) self.source_width else image.width; + const height = if (self.source_height > 0) self.source_height else image.height; + + // If we don't have any specified cols or rows then the placement + // should be the native size of the image, and doesn't need to be + // re-scaled. + if (self.columns == 0 and self.rows == 0) return .{ + .width = width, + .height = height, + }; + + // We calculate the size of a cell so that we can multiply + // it by the specified cols/rows to get the correct px size. + // + // We assume that the width is divided evenly by the column + // count and the height by the row count, because it should be. + const cell_width: u32 = t.width_px / t.cols; + const cell_height: u32 = t.height_px / t.rows; + + const width_f64: f64 = @floatFromInt(width); + const height_f64: f64 = @floatFromInt(height); + + // If we have a specified cols AND rows then we calculate + // the width and height from them directly, we don't need + // to adjust for aspect ratio. + if (self.columns > 0 and self.rows > 0) { + const calc_width = cell_width * self.columns; + const calc_height = cell_height * self.rows; + + return .{ + .width = calc_width, + .height = calc_height, + }; + } + + // Either the columns or the rows were specified, but not both, + // so we need to calculate the other one based on the aspect ratio. + + // If only the columns were specified, we determine + // the height of the image based on the aspect ratio. + if (self.columns > 0) { + const aspect = height_f64 / width_f64; + const calc_width: u32 = cell_width * self.columns; + const calc_height: u32 = @intFromFloat(@round( + @as(f64, @floatFromInt(calc_width)) * aspect, + )); + + return .{ + .width = calc_width, + .height = calc_height, + }; + } + + // Otherwise, only the rows were specified, so we + // determine the width based on the aspect ratio. + { + const aspect = width_f64 / height_f64; + const calc_height: u32 = cell_height * self.rows; + const calc_width: u32 = @intFromFloat(@round( + @as(f64, @floatFromInt(calc_height)) * aspect, + )); + + return .{ + .width = calc_width, + .height = calc_height, + }; + } + } + /// Returns the size in grid cells that this placement takes up. pub fn gridSize( self: Placement, @@ -667,60 +747,29 @@ pub const ImageStorage = struct { cols: u32, rows: u32, } { + // If we have a specified columns and rows then this is trivial. if (self.columns > 0 and self.rows > 0) return .{ .cols = self.columns, .rows = self.rows, }; - // Calculate our cell size. - const terminal_width_f64: f64 = @floatFromInt(t.width_px); - const terminal_height_f64: f64 = @floatFromInt(t.height_px); - const grid_columns_f64: f64 = @floatFromInt(t.cols); - const grid_rows_f64: f64 = @floatFromInt(t.rows); - const cell_width_f64 = terminal_width_f64 / grid_columns_f64; - const cell_height_f64 = terminal_height_f64 / grid_rows_f64; - - // Our image width - const width_px = if (self.source_width > 0) self.source_width else image.width; - const height_px = if (self.source_height > 0) self.source_height else image.height; - - // Calculate our image size in grid cells - const width_f64: f64 = @floatFromInt(width_px); - const height_f64: f64 = @floatFromInt(height_px); - - // If only columns is specified, calculate rows based on aspect ratio - if (self.columns > 0 and self.rows == 0) { - const cols_f64: f64 = @floatFromInt(self.columns); - const cols_px = cols_f64 * cell_width_f64; - const aspect_ratio = height_f64 / width_f64; - const rows_px = cols_px * aspect_ratio; - const rows_cells = rows_px / cell_height_f64; - return .{ - .cols = self.columns, - .rows = @intFromFloat(@ceil(rows_cells)), - }; - } - - // If only rows is specified, calculate columns based on aspect ratio - if (self.rows > 0 and self.columns == 0) { - const rows_f64: f64 = @floatFromInt(self.rows); - const rows_px = rows_f64 * cell_height_f64; - const aspect_ratio = width_f64 / height_f64; - const cols_px = rows_px * aspect_ratio; - const cols_cells = cols_px / cell_width_f64; - return .{ - .cols = @intFromFloat(@ceil(cols_cells)), - .rows = self.rows, - }; - } - - const width_cells: u32 = @intFromFloat(@ceil(width_f64 / cell_width_f64)); - const height_cells: u32 = @intFromFloat(@ceil(height_f64 / cell_height_f64)); - + // Otherwise we calculate the pixel size, divide by + // cell size, and round up to the nearest integer. + const calc_size = self.calculatedSize(image, t); return .{ - .cols = width_cells, - .rows = height_cells, + .cols = std.math.divCeil( + u32, + calc_size.width + self.x_offset, + t.width_px / t.cols, + ) catch 0, + .rows = std.math.divCeil( + u32, + calc_size.height + self.y_offset, + t.height_px / t.rows, + ) catch 0, }; + // NOTE: Above `divCeil`s can only fail if the cell size is 0, + // in such a case it seems safe to return 0 for this. } /// Returns a selection of the entire rectangle this placement @@ -1269,36 +1318,42 @@ test "storage: aspect ratio calculation when only columns or rows specified" { var t = try terminal.Terminal.init(alloc, .{ .cols = 100, .rows = 100 }); defer t.deinit(alloc); - t.width_px = 100; - t.height_px = 100; + t.width_px = 1000; // 10 px per col + t.height_px = 2000; // 20 px per row // Case 1: Only columns specified { - const image = Image{ .id = 1, .width = 4, .height = 2 }; + const image = Image{ .id = 1, .width = 16, .height = 9 }; var placement = ImageStorage.Placement{ .location = .{ .virtual = {} }, - .columns = 6, + .columns = 10, .rows = 0, }; - const grid_size = placement.gridSize(image, &t); - // 6 columns * (2/4) = 3 rows - try testing.expectEqual(@as(u32, 6), grid_size.cols); - try testing.expectEqual(@as(u32, 3), grid_size.rows); + // Image is 16x9, set to a width of 10 columns, at 10px per column + // that's 100px width. 100px * (9 / 16) = 56.25, which sould round + // to a height of 56px. + + const calc_size = placement.calculatedSize(image, &t); + try testing.expectEqual(@as(u32, 100), calc_size.width); + try testing.expectEqual(@as(u32, 56), calc_size.height); } // Case 2: Only rows specified { - const image = Image{ .id = 2, .width = 2, .height = 4 }; + const image = Image{ .id = 2, .width = 16, .height = 9 }; var placement = ImageStorage.Placement{ .location = .{ .virtual = {} }, .columns = 0, - .rows = 6, + .rows = 5, }; - const grid_size = placement.gridSize(image, &t); - // 6 rows * (2/4) = 3 columns - try testing.expectEqual(@as(u32, 3), grid_size.cols); - try testing.expectEqual(@as(u32, 6), grid_size.rows); + // Image is 16x9, set to a height of 5 rows, at 20px per row that's + // 100px height. 100px * (16 / 9) = 177.77..., which should round to + // a width of 178px. + + const calc_size = placement.calculatedSize(image, &t); + try testing.expectEqual(@as(u32, 178), calc_size.width); + try testing.expectEqual(@as(u32, 100), calc_size.height); } } From ed207514e98ff7c810c6fdc1e8905b57f3ecbbb3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 15 May 2025 11:59:17 -0600 Subject: [PATCH 265/642] typo --- src/terminal/kitty/graphics_storage.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/terminal/kitty/graphics_storage.zig b/src/terminal/kitty/graphics_storage.zig index 6e336e785..0c3022e4a 100644 --- a/src/terminal/kitty/graphics_storage.zig +++ b/src/terminal/kitty/graphics_storage.zig @@ -1331,7 +1331,7 @@ test "storage: aspect ratio calculation when only columns or rows specified" { }; // Image is 16x9, set to a width of 10 columns, at 10px per column - // that's 100px width. 100px * (9 / 16) = 56.25, which sould round + // that's 100px width. 100px * (9 / 16) = 56.25, which should round // to a height of 56px. const calc_size = placement.calculatedSize(image, &t); From e2f3b6211f87ff81c38043d66de21c73e577bebf Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 15 May 2025 12:06:30 -0600 Subject: [PATCH 266/642] fix(Metal): use sRGB texture format for gamma correct interpolation otherwise images will be too dark when scaled --- src/renderer/metal/api.zig | 1 + src/renderer/metal/image.zig | 2 +- src/renderer/shaders/cell.metal | 14 +++++++------- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 19db17ba4..46cb4f6bc 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -96,6 +96,7 @@ pub const MTLVertexStepFunction = enum(c_ulong) { pub const MTLPixelFormat = enum(c_ulong) { r8unorm = 10, rgba8unorm = 70, + rgba8unorm_srgb = 71, rgba8uint = 73, bgra8unorm = 80, bgra8unorm_srgb = 81, diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index ff13a49e8..7d2599308 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -441,7 +441,7 @@ pub const Image = union(enum) { }; // Set our properties - desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8unorm)); + desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8unorm_srgb)); desc.setProperty("width", @as(c_ulong, @intCast(p.width))); desc.setProperty("height", @as(c_ulong, @intCast(p.height))); diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 80ffc00de..5b3875221 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -668,12 +668,12 @@ fragment float4 image_fragment( float4 rgba = image.sample(textureSampler, in.tex_coord); - return load_color( - uchar4(rgba * 255.0), - // We assume all images are sRGB regardless of the configured colorspace - // TODO: Maybe support wide gamut images? - false, - uniforms.use_linear_blending - ); + if (!uniforms.use_linear_blending) { + rgba = unlinearize(rgba); + } + + rgba.rgb *= rgba.a; + + return rgba; } From ea79fdea119b40e4eca875611d4d224bddb963f1 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 15 May 2025 12:22:56 -0600 Subject: [PATCH 267/642] fix(OpenGL): use sRGB texture format for gamma correct interpolation otherwise images will be too dark when scaled --- pkg/opengl/Texture.zig | 3 +++ src/renderer/opengl/image.zig | 4 ++-- src/renderer/shaders/image.f.glsl | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 5804ef538..fa5cf770b 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -70,6 +70,9 @@ pub const InternalFormat = enum(c_int) { rgb = c.GL_RGB, rgba = c.GL_RGBA, + srgb = c.GL_SRGB, + srgba = c.GL_SRGB_ALPHA, + // There are so many more that I haven't filled in. _, }; diff --git a/src/renderer/opengl/image.zig b/src/renderer/opengl/image.zig index b22d10ea3..26cd90736 100644 --- a/src/renderer/opengl/image.zig +++ b/src/renderer/opengl/image.zig @@ -368,8 +368,8 @@ pub const Image = union(enum) { internal: gl.Texture.InternalFormat, format: gl.Texture.Format, } = switch (self.*) { - .pending_rgb, .replace_rgb => .{ .internal = .rgb, .format = .rgb }, - .pending_rgba, .replace_rgba => .{ .internal = .rgba, .format = .rgba }, + .pending_rgb, .replace_rgb => .{ .internal = .srgb, .format = .rgb }, + .pending_rgba, .replace_rgba => .{ .internal = .srgba, .format = .rgba }, else => unreachable, }; diff --git a/src/renderer/shaders/image.f.glsl b/src/renderer/shaders/image.f.glsl index e8c00b271..e4aa9ef8e 100644 --- a/src/renderer/shaders/image.f.glsl +++ b/src/renderer/shaders/image.f.glsl @@ -6,7 +6,24 @@ layout(location = 0) out vec4 out_FragColor; uniform sampler2D image; +// Converts a color from linear to sRGB gamma encoding. +vec4 unlinearize(vec4 linear) { + bvec3 cutoff = lessThan(linear.rgb, vec3(0.0031308)); + vec3 higher = pow(linear.rgb, vec3(1.0/2.4)) * vec3(1.055) - vec3(0.055); + vec3 lower = linear.rgb * vec3(12.92); + + return vec4(mix(higher, lower, cutoff), linear.a); +} + void main() { vec4 color = texture(image, tex_coord); + + // Our texture is stored with an sRGB internal format, + // which means that the values are linearized when we + // sample the texture, but for now we actually want to + // output the color with gamma compression, so we do + // that. + color = unlinearize(color); + out_FragColor = vec4(color.rgb * color.a, color.a); } From 8a0ca1b573a447bed6ccad9c64e31de130f718d4 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 18 May 2025 00:14:40 +0000 Subject: [PATCH 268/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 187c67531..796ce1475 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz", - .hash = "N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz", + .hash = "N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 8b29ff0c3..68ec4522a 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs": { + "N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz", - "hash": "sha256-YIlb2eSviWrRc+hbwgsAHLeCY3JgbYWjd9ZbOpXe1Qg=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz", + "hash": "sha256-DKWVUxZEZA8x+3njPaTucr/u/Mmhef0YwhwOnOWn/N4=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 056c7b75e..7c3e08d2d 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs"; + name = "N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz"; - hash = "sha256-YIlb2eSviWrRc+hbwgsAHLeCY3JgbYWjd9ZbOpXe1Qg="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz"; + hash = "sha256-DKWVUxZEZA8x+3njPaTucr/u/Mmhef0YwhwOnOWn/N4="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index dd38069c4..0c71c80e4 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 9dbd2c18d..2ee48f269 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/93eb37fadd58231e06ad32ef79205324ea6189c0.tar.gz", - "dest": "vendor/p/N-V-__8AAIcFXgTG-wQvRJNXLt9vV5q3Tq8VryNTjKxTWsMs", - "sha256": "60895bd9e4af896ad173e85bc20b001cb7826372606d85a377d65b3a95ded508" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz", + "dest": "vendor/p/N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn", + "sha256": "0ca595531644640f31fb79e33da4ee72bfeefcc9a179fd18c21c0e9ce5a7fcde" }, { "type": "archive", From 54dbd1990a111ae5042d2ae757f2b1f625b5bd00 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 28 Feb 2025 14:43:55 +0100 Subject: [PATCH 269/642] gtk: implement global shortcuts It's been a lot of D-Bus related pain and suffering, but here it is. I'm not sure about how well this is integrated inside App, but I'm fairly proud of the standalone logic. --- src/apprt/gtk/App.zig | 12 + src/apprt/gtk/GlobalShortcuts.zig | 419 ++++++++++++++++++++++++++++++ src/apprt/gtk/key.zig | 59 ++++- 3 files changed, 484 insertions(+), 6 deletions(-) create mode 100644 src/apprt/gtk/GlobalShortcuts.zig diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 06cc41b9d..ab5276915 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -40,6 +40,7 @@ const Window = @import("Window.zig"); const ConfigErrorsDialog = @import("ConfigErrorsDialog.zig"); const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig"); const CloseDialog = @import("CloseDialog.zig"); +const GlobalShortcuts = @import("GlobalShortcuts.zig"); const Split = @import("Split.zig"); const inspector = @import("inspector.zig"); const key = @import("key.zig"); @@ -95,6 +96,8 @@ css_provider: *gtk.CssProvider, /// Providers for loading custom stylesheets defined by user custom_css_providers: std.ArrayListUnmanaged(*gtk.CssProvider) = .{}, +global_shortcuts: ?GlobalShortcuts, + /// The timer used to quit the application after the last window is closed. quit_timer: union(enum) { off: void, @@ -422,6 +425,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // our "activate" call above will open a window. .running = gio_app.getIsRemote() == 0, .css_provider = css_provider, + .global_shortcuts = .init(core_app.alloc, gio_app), }; } @@ -443,6 +447,8 @@ pub fn terminate(self: *App) void { self.winproto.deinit(self.core_app.alloc); + if (self.global_shortcuts) |*shortcuts| shortcuts.deinit(); + self.config.deinit(); } @@ -1012,6 +1018,12 @@ fn syncConfigChanges(self: *App, window: ?*Window) !void { ConfigErrorsDialog.maybePresent(self, window); try self.syncActionAccelerators(); + if (self.global_shortcuts) |*shortcuts| { + shortcuts.refreshSession(self) catch |err| { + log.warn("failed to refresh global shortcuts={}", .{err}); + }; + } + // Load our runtime and custom CSS. If this fails then our window is just stuck // with the old CSS but we don't want to fail the entire sync operation. self.loadRuntimeCss() catch |err| switch (err) { diff --git a/src/apprt/gtk/GlobalShortcuts.zig b/src/apprt/gtk/GlobalShortcuts.zig new file mode 100644 index 000000000..7d960d7bf --- /dev/null +++ b/src/apprt/gtk/GlobalShortcuts.zig @@ -0,0 +1,419 @@ +const GlobalShortcuts = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const gio = @import("gio"); +const glib = @import("glib"); +const gobject = @import("gobject"); + +const App = @import("App.zig"); +const configpkg = @import("../../config.zig"); +const Binding = @import("../../input.zig").Binding; +const key = @import("key.zig"); + +const log = std.log.scoped(.global_shortcuts); +const Token = [16]u8; + +app: *App, +arena: std.heap.ArenaAllocator, +dbus: *gio.DBusConnection, + +/// A mapping from a unique ID to an action. +/// Currently the unique ID is simply the serialized representation of the +/// trigger that was used for the action as triggers are unique in the keymap, +/// but this may change in the future. +map: std.StringArrayHashMapUnmanaged(Binding.Action) = .{}, + +/// The handle of the current global shortcuts portal session, +/// as a D-Bus object path. +handle: ?[:0]const u8 = null, + +/// The D-Bus signal subscription for the response signal on requests. +/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null. +response_subscription: c_uint = 0, + +/// The D-Bus signal subscription for the keybind activate signal. +/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null. +activate_subscription: c_uint = 0, + +pub fn init(alloc: Allocator, gio_app: *gio.Application) ?GlobalShortcuts { + const dbus = gio_app.getDbusConnection() orelse return null; + + return .{ + // To be initialized later + .app = undefined, + .arena = .init(alloc), + .dbus = dbus, + }; +} + +pub fn deinit(self: *GlobalShortcuts) void { + self.close(); + self.arena.deinit(); +} + +fn close(self: *GlobalShortcuts) void { + if (self.response_subscription != 0) { + self.dbus.signalUnsubscribe(self.response_subscription); + self.response_subscription = 0; + } + + if (self.activate_subscription != 0) { + self.dbus.signalUnsubscribe(self.activate_subscription); + self.activate_subscription = 0; + } + + if (self.handle) |handle| { + // Close existing session + self.dbus.call( + "org.freedesktop.portal.Desktop", + handle, + "org.freedesktop.portal.Session", + "Close", + null, + null, + .{}, + -1, + null, + null, + null, + ); + self.handle = null; + } +} + +pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void { + // Ensure we have a valid reference to the app + // (it was left uninitialized in `init`) + self.app = app; + + // Close any existing sessions + self.close(); + + // Update map + var trigger_buf: [256]u8 = undefined; + + self.map.clearRetainingCapacity(); + var it = self.app.config.keybind.set.bindings.iterator(); + + while (it.next()) |entry| { + const leaf = switch (entry.value_ptr.*) { + // Global shortcuts can't have leaders + .leader => continue, + .leaf => |leaf| leaf, + }; + if (!leaf.flags.global) continue; + + const trigger = try key.xdgShortcutFromTrigger( + &trigger_buf, + entry.key_ptr.*, + ) orelse continue; + + try self.map.put( + self.arena.allocator(), + try self.arena.allocator().dupeZ(u8, trigger), + leaf.action, + ); + } + + try self.request(.create_session); +} + +fn shortcutActivated( + _: *gio.DBusConnection, + _: ?[*:0]const u8, + _: [*:0]const u8, + _: [*:0]const u8, + _: [*:0]const u8, + params: *glib.Variant, + ud: ?*anyopaque, +) callconv(.c) void { + const self: *GlobalShortcuts = @ptrCast(@alignCast(ud)); + + // 2nd value in the tuple is the activated shortcut ID + // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated + var shortcut_id: [*:0]const u8 = undefined; + params.getChild(1, "&s", &shortcut_id); + log.debug("activated={s}", .{shortcut_id}); + + const action = self.map.get(std.mem.span(shortcut_id)) orelse return; + + self.app.core_app.performAllAction(self.app, action) catch |err| { + log.err("failed to perform action={}", .{err}); + }; +} + +const Method = enum { + create_session, + bind_shortcuts, + + fn name(self: Method) [:0]const u8 { + return switch (self) { + .create_session => "CreateSession", + .bind_shortcuts => "BindShortcuts", + }; + } + + /// Construct the payload expected by the XDG portal call. + fn makePayload( + self: Method, + shortcuts: *GlobalShortcuts, + request_token: [:0]const u8, + ) ?*glib.Variant { + switch (self) { + // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession + .create_session => { + var session_token: Token = undefined; + return glib.Variant.newParsed( + "({'handle_token': <%s>, 'session_handle_token': <%s>},)", + request_token.ptr, + generateToken(&session_token).ptr, + ); + }, + // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts + .bind_shortcuts => { + const handle = shortcuts.handle orelse return null; + + const bind_type = glib.VariantType.new("a(sa{sv})"); + defer glib.free(bind_type); + + var binds: glib.VariantBuilder = undefined; + glib.VariantBuilder.init(&binds, bind_type); + + var action_buf: [256]u8 = undefined; + + var it = shortcuts.map.iterator(); + while (it.next()) |entry| { + const trigger = entry.key_ptr.*.ptr; + const action = std.fmt.bufPrintZ( + &action_buf, + "{}", + .{entry.value_ptr.*}, + ) catch continue; + + binds.addParsed( + "(%s, {'description': <%s>, 'preferred_trigger': <%s>})", + trigger, + action.ptr, + trigger, + ); + } + + return glib.Variant.newParsed( + "(%o, %*, '', {'handle_token': <%s>})", + handle.ptr, + binds.end(), + request_token.ptr, + ); + }, + } + } + + fn onResponse(self: Method, shortcuts: *GlobalShortcuts, vardict: *glib.Variant) void { + switch (self) { + .create_session => { + var handle: ?[*:0]u8 = null; + if (vardict.lookup("session_handle", "&s", &handle) == 0) { + log.err( + "session handle not found in response={s}", + .{vardict.print(@intFromBool(true))}, + ); + return; + } + + shortcuts.handle = shortcuts.arena.allocator().dupeZ(u8, std.mem.span(handle.?)) catch { + log.err("out of memory: failed to clone session handle", .{}); + return; + }; + + log.debug("session_handle={?s}", .{handle}); + + // Subscribe to keybind activations + shortcuts.activate_subscription = shortcuts.dbus.signalSubscribe( + null, + "org.freedesktop.portal.GlobalShortcuts", + "Activated", + "/org/freedesktop/portal/desktop", + handle, + .{ .match_arg0_path = true }, + shortcutActivated, + shortcuts, + null, + ); + + shortcuts.request(.bind_shortcuts) catch |err| { + log.err("failed to bind shortcuts={}", .{err}); + return; + }; + }, + .bind_shortcuts => {}, + } + } +}; + +/// Submit a request to the global shortcuts portal. +fn request( + self: *GlobalShortcuts, + comptime method: Method, +) !void { + // NOTE(pluiedev): + // XDG Portals are really, really poorly-designed pieces of hot garbage. + // How the protocol is _initially_ designed to work is as follows: + // + // 1. The client calls a method which returns the path of a Request object; + // 2. The client waits for the Response signal under said object path; + // 3. When the signal arrives, the actual return value and status code + // become available for the client for further processing. + // + // THIS DOES NOT WORK. Once the first two steps are complete, the client + // needs to immediately start listening for the third step, but an overeager + // server implementation could easily send the Response signal before the + // client is even ready, causing communications to break down over a simple + // race condition/two generals' problem that even _TCP_ had figured out + // decades ago. Worse yet, you get exactly _one_ chance to listen for the + // signal, or else your communication attempt so far has all been in vain. + // + // And they know this. Instead of fixing their freaking protocol, they just + // ask clients to manually construct the expected object path and subscribe + // to the request signal beforehand, making the whole response value of + // the original call COMPLETELY MEANINGLESS. + // + // Furthermore, this is _entirely undocumented_ aside from one tiny + // paragraph under the documentation for the Request interface, and + // anyone would be forgiven for missing it without reading the libportal + // source code. + // + // When in Rome, do as the Romans do, I guess...? + + const callbacks = struct { + fn gotResponseHandle( + source: ?*gobject.Object, + res: *gio.AsyncResult, + _: ?*anyopaque, + ) callconv(.c) void { + const dbus_ = gobject.ext.cast(gio.DBusConnection, source.?).?; + + var err: ?*glib.Error = null; + defer if (err) |err_| err_.free(); + + const params_ = dbus_.callFinish(res, &err) orelse { + if (err) |err_| log.err("request failed={s} ({})", .{ + err_.f_message orelse "(unknown)", + err_.f_code, + }); + return; + }; + defer params_.unref(); + + // TODO: XDG recommends updating the signal subscription if the actual + // returned request path is not the same as the expected request + // path, to retain compatibility with older versions of XDG portals. + // Although it suffers from the race condition outlined above, + // we should still implement this at some point. + } + + // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response + fn responded( + dbus: *gio.DBusConnection, + _: ?[*:0]const u8, + _: [*:0]const u8, + _: [*:0]const u8, + _: [*:0]const u8, + params_: *glib.Variant, + ud: ?*anyopaque, + ) callconv(.c) void { + const self_: *GlobalShortcuts = @ptrCast(@alignCast(ud)); + + // Unsubscribe from the response signal + if (self_.response_subscription != 0) { + dbus.signalUnsubscribe(self_.response_subscription); + self_.response_subscription = 0; + } + + var response: u32 = 0; + var vardict: ?*glib.Variant = null; + params_.get("(u@a{sv})", &response, &vardict); + + switch (response) { + 0 => { + log.debug("request successful", .{}); + method.onResponse(self_, vardict.?); + }, + 1 => log.debug("request was cancelled by user", .{}), + 2 => log.warn("request ended unexpectedly", .{}), + else => log.err("unrecognized response code={}", .{response}), + } + } + }; + + var request_token_buf: Token = undefined; + const request_token = generateToken(&request_token_buf); + + const payload = method.makePayload(self, request_token) orelse return; + const request_path = try self.getRequestPath(request_token); + + self.response_subscription = self.dbus.signalSubscribe( + null, + "org.freedesktop.portal.Request", + "Response", + request_path, + null, + .{}, + callbacks.responded, + self, + null, + ); + + self.dbus.call( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + "org.freedesktop.portal.GlobalShortcuts", + method.name(), + payload, + null, + .{}, + -1, + null, + callbacks.gotResponseHandle, + null, + ); +} + +/// Generate a random token suitable for use in requests. +fn generateToken(buf: *Token) [:0]const u8 { + // u28 takes up 7 bytes in hex, 8 bytes for "ghostty_" and 1 byte for NUL + // 7 + 8 + 1 = 16 + return std.fmt.bufPrintZ( + buf, + "ghostty_{x:0<7}", + .{std.crypto.random.int(u28)}, + ) catch unreachable; +} + +/// Get the XDG portal request path for the current Ghostty instance. +/// +/// If this sounds like nonsense, see `request` for an explanation as to +/// why we need to do this. +fn getRequestPath(self: *GlobalShortcuts, token: [:0]const u8) ![:0]const u8 { + // See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html + // for the syntax XDG portals expect. + + // `getUniqueName` should never return null here as we're using an ordinary + // message bus connection. If it doesn't, something is very wrong + const unique_name = std.mem.span(self.dbus.getUniqueName().?); + + const object_path = try std.mem.joinZ(self.arena.allocator(), "/", &.{ + "/org/freedesktop/portal/desktop/request", + unique_name[1..], // Remove leading `:` + token, + }); + + // Sanitize the unique name by replacing every `.` with `_`. + // In effect, this will turn a unique name like `:1.192` into `1_192`. + // Valid D-Bus object path components never contain `.`s anyway, so we're + // free to replace all instances of `.` here and avoid extra allocation. + std.mem.replaceScalar(u8, object_path, '.', '_'); + + return object_path; +} diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index b3330eb40..2376f6bbc 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -20,10 +20,45 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u if (trigger.mods.super) try writer.writeAll(""); // Write our key + if (!try writeTriggerKey(writer, trigger)) return null; + + // We need to make the string null terminated. + try writer.writeByte(0); + const slice = buf_stream.getWritten(); + return slice[0 .. slice.len - 1 :0]; +} + +/// Returns a XDG-compliant shortcuts string from a trigger. +/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/ +pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 { + var buf_stream = std.io.fixedBufferStream(buf); + const writer = buf_stream.writer(); + + // Modifiers + if (trigger.mods.shift) try writer.writeAll("SHIFT+"); + if (trigger.mods.ctrl) try writer.writeAll("CTRL+"); + if (trigger.mods.alt) try writer.writeAll("ALT+"); + if (trigger.mods.super) try writer.writeAll("LOGO+"); + + // Write our key + // NOTE: While the spec specifies that only libxkbcommon keysyms are + // expected, using GTK's keysyms should still work as they are identical + // to *X11's* keysyms (which I assume is a subset of libxkbcommon's). + // I haven't been able to any evidence to back up that assumption but + // this works for now + if (!try writeTriggerKey(writer, trigger)) return null; + + // We need to make the string null terminated. + try writer.writeByte(0); + const slice = buf_stream.getWritten(); + return slice[0 .. slice.len - 1 :0]; +} + +fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool { switch (trigger.key) { .physical => |k| { - const keyval = keyvalFromKey(k) orelse return null; - try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return null)); + const keyval = keyvalFromKey(k) orelse return false; + try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false)); }, .unicode => |cp| { @@ -35,10 +70,7 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u }, } - // We need to make the string null terminated. - try writer.writeByte(0); - const slice = buf_stream.getWritten(); - return slice[0 .. slice.len - 1 :0]; + return true; } pub fn translateMods(state: gdk.ModifierType) input.Mods { @@ -208,6 +240,21 @@ test "accelFromTrigger" { })).?); } +test "xdgShortcutFromTrigger" { + const testing = std.testing; + var buf: [256]u8 = undefined; + + try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{ + .mods = .{ .super = true }, + .key = .{ .translated = .q }, + })).?); + + try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{ + .mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true }, + .key = .{ .unicode = 92 }, + })).?); +} + /// A raw entry in the keymap. Our keymap contains mappings between /// GDK keys and our own key enum. const RawEntry = struct { c_uint, input.Key }; From 6827dc096437fd2cfdfc376dd760e6815b996f75 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Mon, 14 Apr 2025 16:12:56 +0800 Subject: [PATCH 270/642] config: document `global:` support on Linux Compiling this list of known supported and unsupported platforms has been amazingly painful. Never change, Linux desktop. --- src/config/Config.zig | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index ca330f8f6..0c4e07f06 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1110,12 +1110,33 @@ class: ?[:0]const u8 = null, /// `global:unconsumed:ctrl+a=reload_config` will make the keybind global /// and not consume the input to reload the config. /// -/// Note: `global:` is only supported on macOS. On macOS, -/// this feature requires accessibility permissions to be granted to Ghostty. -/// When a `global:` keybind is specified and Ghostty is launched or reloaded, -/// Ghostty will attempt to request these permissions. If the permissions are -/// not granted, the keybind will not work. On macOS, you can find these -/// permissions in System Preferences -> Privacy & Security -> Accessibility. +/// Note: `global:` is only supported on macOS and certain Linux platforms. +/// +/// On macOS, this feature requires accessibility permissions to be granted +/// to Ghostty. When a `global:` keybind is specified and Ghostty is launched +/// or reloaded, Ghostty will attempt to request these permissions. +/// If the permissions are not granted, the keybind will not work. On macOS, +/// you can find these permissions in System Preferences -> Privacy & Security +/// -> Accessibility. +/// +/// On Linux, you need a desktop environment that implements the +/// [Global Shortcuts](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html) +/// protocol as a part of its XDG desktop protocol implementation. +/// Desktop environments that are known to support (or not support) +/// global shortcuts include: +/// +/// - Users using KDE Plasma (since [5.27](https://kde.org/announcements/plasma/5/5.27.0/#wayland)) +/// and GNOME (since [48](https://release.gnome.org/48/#and-thats-not-all)) should be able +/// to use global shortcuts with little to no configuration. +/// +/// - Some manual configuration is required on Hyprland. Consult the steps +/// outlined on the [Hyprland Wiki](https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts) +/// to set up global shortcuts correctly. +/// (Important: [`xdg-desktop-portal-hyprland`](https://wiki.hyprland.org/Hypr-Ecosystem/xdg-desktop-portal-hyprland/) +/// must also be installed!) +/// +/// - 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)). keybind: Keybinds = .{}, /// Horizontal window padding. This applies padding between the terminal cells From ac6aa8d395658041d6777e13d74d17baeaffc073 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 May 2025 13:56:22 -0700 Subject: [PATCH 271/642] Add `selection-clear-on-typing` Fixes #7392 Docs: > Whether to clear selected text when typing. This defaults to `true`. > This is typical behavior for most terminal emulators as well as > text input fields. If you set this to `false`, then the selected text > will not be cleared when typing. > > "Typing" is specifically defined as any non-modifier (shift, control, > alt, etc.) keypress that produces data to be sent to the application > running within the terminal (e.g. the shell). Additionally, selection > is cleared when any preedit or composition state is started (e.g. > when typing languages such as Japanese). > > If this is `false`, then the selection can still be manually > cleared by clicking once or by pressing `escape`. --- src/Surface.zig | 14 ++++++++++++-- src/config/Config.zig | 15 +++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 138cd4839..f9e232340 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -253,6 +253,7 @@ const DerivedConfig = struct { mouse_shift_capture: configpkg.MouseShiftCapture, macos_non_native_fullscreen: configpkg.NonNativeFullscreen, macos_option_as_alt: ?configpkg.OptionAsAlt, + selection_clear_on_typing: bool, vt_kam_allowed: bool, window_padding_top: u32, window_padding_bottom: u32, @@ -316,6 +317,7 @@ const DerivedConfig = struct { .mouse_shift_capture = config.@"mouse-shift-capture", .macos_non_native_fullscreen = config.@"macos-non-native-fullscreen", .macos_option_as_alt = config.@"macos-option-as-alt", + .selection_clear_on_typing = config.@"selection-clear-on-typing", .vt_kam_allowed = config.@"vt-kam-allowed", .window_padding_top = config.@"window-padding-y".top_left, .window_padding_bottom = config.@"window-padding-y".bottom_right, @@ -1687,7 +1689,9 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void { if (self.renderer_state.preedit != null or preedit_ != null) { - self.setSelection(null) catch {}; + if (self.config.selection_clear_on_typing) { + self.setSelection(null) catch {}; + } } // We always clear our prior preedit @@ -1930,7 +1934,13 @@ pub fn keyCallback( if (!event.key.modifier()) { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); - try self.setSelection(null); + + if (self.config.selection_clear_on_typing or + event.key == .escape) + { + try self.setSelection(null); + } + try self.io.terminal.scrollViewport(.{ .bottom = {} }); try self.queueRender(); } diff --git a/src/config/Config.zig b/src/config/Config.zig index 81291b4e5..6f1e89d41 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -474,6 +474,21 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF }, /// selection color will vary across the selection. @"selection-invert-fg-bg": bool = false, +/// Whether to clear selected text when typing. This defaults to `true`. +/// This is typical behavior for most terminal emulators as well as +/// text input fields. If you set this to `false`, then the selected text +/// will not be cleared when typing. +/// +/// "Typing" is specifically defined as any non-modifier (shift, control, +/// alt, etc.) keypress that produces data to be sent to the application +/// running within the terminal (e.g. the shell). Additionally, selection +/// is cleared when any preedit or composition state is started (e.g. +/// when typing languages such as Japanese). +/// +/// If this is `false`, then the selection can still be manually +/// cleared by clicking once or by pressing `escape`. +@"selection-clear-on-typing": bool = true, + /// The minimum contrast ratio between the foreground and background colors. /// The contrast ratio is a value between 1 and 21. A value of 1 allows for no /// contrast (e.g. black on black). This value is the contrast ratio as defined From 9ad0e4675bf27f7df78fbabb58533502931b5a45 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 19 May 2025 18:42:16 -0500 Subject: [PATCH 272/642] nix: keep symbols if we're building a debug package also add CI tests to make sure debug symbols exist Co-authored-by: Mitchell Hashimoto --- .github/workflows/test.yml | 19 +++++++++++++++++-- nix/package.nix | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6e3a77a0..b32eda0f6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -211,8 +211,23 @@ jobs: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: Test NixOS package build - run: nix build .#ghostty + - name: Test release NixOS package build + run: nix build .#ghostty-releasefast + + - name: Check version + run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.ReleaseFast' + + - name: Check to see if the binary has been stripped + run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'no symbols' + + - name: Test debug NixOS package build + run: nix build .#ghostty-debug + + - name: Check version + run: result/bin/ghostty +version | grep -q 'builtin.OptimizeMode.Debug' + + - name: Check to see if the binary has not been stripped + run: nm result/bin/.ghostty-wrapped 2>&1 | grep -q 'main_ghostty.main' build-dist: runs-on: namespace-profile-ghostty-md diff --git a/nix/package.nix b/nix/package.nix index a39f5b835..08dfd710b 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -36,6 +36,7 @@ buildInputs = import ./build-support/build-inputs.nix { inherit pkgs lib stdenv enableX11 enableWayland; }; + strip = optimize != "Debug" && optimize != "ReleaseSafe"; in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; @@ -87,6 +88,7 @@ in buildInputs = buildInputs; dontConfigure = true; + dontStrip = !strip; GI_TYPELIB_PATH = gi_typelib_path; @@ -96,6 +98,7 @@ in "-Dversion-string=${finalAttrs.version}-${revision}-nix" "-Dgtk-x11=${lib.boolToString enableX11}" "-Dgtk-wayland=${lib.boolToString enableWayland}" + "-Dstrip=${lib.boolToString strip}" ]; outputs = [ From 3d2bc3dca14573817661649aab438b87a741e478 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 19 May 2025 17:05:46 -0700 Subject: [PATCH 273/642] build: add unwind tables and frame pointers to debug/test builds This fixes an issue where stack traces were unreliable on some platforms (namely aarch64-linux in a MacOS VM). I'm unsure if this is a bug in Zig (defaults should be changed?) or what, because this isn't necessary on other platforms, but this works around the issue. I've unconditionally enabled this for all platforms, depending on build mode (debug/test) and not the target. This is because I don't think there is a downside for other platforms but if thats wrong we can fix that quickly. Some binaries have this unconditionally enabled regardless of build mode (e.g. the Unicode tables generator) because having symbols in those cases is always useful. Some unrelated GTK test fix is also included here. I'm not sure why CI didn't catch this (perhaps we only run tests for none-runtime) but all tests pass locally and we can look into that elsewhere. --- build.zig | 12 +++++++++--- src/apprt/gtk/key.zig | 2 +- src/build/GhosttyBench.zig | 10 ++++++---- src/build/GhosttyDocs.zig | 9 +++++++-- src/build/GhosttyExe.zig | 12 ++++++++---- src/build/GhosttyFrameData.zig | 9 +++++++-- src/build/GhosttyWebdata.zig | 9 +++++++-- src/build/HelpStrings.zig | 9 +++++++-- src/build/UnicodeTables.zig | 9 +++++++-- 9 files changed, 59 insertions(+), 22 deletions(-) diff --git a/build.zig b/build.zig index 0751bab51..80af88488 100644 --- a/build.zig +++ b/build.zig @@ -110,9 +110,15 @@ pub fn build(b: *std.Build) !void { const test_exe = b.addTest(.{ .name = "ghostty-test", - .root_source_file = b.path("src/main.zig"), - .target = config.target, - .filter = test_filter, + .filters = if (test_filter) |v| &.{v} else &.{}, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = config.target, + .optimize = .Debug, + .strip = false, + .omit_frame_pointer = false, + .unwind_tables = .sync, + }), }); { diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 2376f6bbc..3dcfaed98 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -246,7 +246,7 @@ test "xdgShortcutFromTrigger" { try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{ .mods = .{ .super = true }, - .key = .{ .translated = .q }, + .key = .{ .unicode = 'q' }, })).?); try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{ diff --git a/src/build/GhosttyBench.zig b/src/build/GhosttyBench.zig index 27f40abff..9e93a3b85 100644 --- a/src/build/GhosttyBench.zig +++ b/src/build/GhosttyBench.zig @@ -36,11 +36,13 @@ pub fn init( const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name}); const c_exe = b.addExecutable(.{ .name = bin_name, - .root_source_file = b.path("src/main.zig"), - .target = deps.config.target, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = deps.config.target, - // We always want our benchmarks to be in release mode. - .optimize = .ReleaseFast, + // We always want our benchmarks to be in release mode. + .optimize = .ReleaseFast, + }), }); c_exe.linkLibC(); diff --git a/src/build/GhosttyDocs.zig b/src/build/GhosttyDocs.zig index d6ebe30eb..4b5dbfd92 100644 --- a/src/build/GhosttyDocs.zig +++ b/src/build/GhosttyDocs.zig @@ -26,8 +26,13 @@ pub fn init( inline for (manpages) |manpage| { const generate_markdown = b.addExecutable(.{ .name = "mdgen_" ++ manpage.name ++ "_" ++ manpage.section, - .root_source_file = b.path("src/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + .strip = false, + .omit_frame_pointer = false, + .unwind_tables = .sync, + }), }); deps.help_strings.addImport(generate_markdown); diff --git a/src/build/GhosttyExe.zig b/src/build/GhosttyExe.zig index e251e7b45..083aecdb5 100644 --- a/src/build/GhosttyExe.zig +++ b/src/build/GhosttyExe.zig @@ -13,10 +13,14 @@ install_step: *std.Build.Step.InstallArtifact, pub fn init(b: *std.Build, cfg: *const Config, deps: *const SharedDeps) !Ghostty { const exe: *std.Build.Step.Compile = b.addExecutable(.{ .name = "ghostty", - .root_source_file = b.path("src/main.zig"), - .target = cfg.target, - .optimize = cfg.optimize, - .strip = cfg.strip, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = cfg.target, + .optimize = cfg.optimize, + .strip = cfg.strip, + .omit_frame_pointer = cfg.strip, + .unwind_tables = if (cfg.strip) .none else .sync, + }), }); const install_step = b.addInstallArtifact(exe, .{}); diff --git a/src/build/GhosttyFrameData.zig b/src/build/GhosttyFrameData.zig index b07e7333f..3dc638a05 100644 --- a/src/build/GhosttyFrameData.zig +++ b/src/build/GhosttyFrameData.zig @@ -15,8 +15,13 @@ output: std.Build.LazyPath, pub fn init(b: *std.Build) !GhosttyFrameData { const exe = b.addExecutable(.{ .name = "framegen", - .root_source_file = b.path("src/build/framegen/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/build/framegen/main.zig"), + .target = b.graph.host, + .strip = false, + .omit_frame_pointer = false, + .unwind_tables = .sync, + }), }); const run = b.addRunArtifact(exe); diff --git a/src/build/GhosttyWebdata.zig b/src/build/GhosttyWebdata.zig index fef08434f..b0201c3ff 100644 --- a/src/build/GhosttyWebdata.zig +++ b/src/build/GhosttyWebdata.zig @@ -18,8 +18,13 @@ pub fn init( { const webgen_config = b.addExecutable(.{ .name = "webgen_config", - .root_source_file = b.path("src/main.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = b.graph.host, + .strip = false, + .omit_frame_pointer = false, + .unwind_tables = .sync, + }), }); deps.help_strings.addImport(webgen_config); diff --git a/src/build/HelpStrings.zig b/src/build/HelpStrings.zig index d088e6c3e..04ae629b7 100644 --- a/src/build/HelpStrings.zig +++ b/src/build/HelpStrings.zig @@ -12,8 +12,13 @@ output: std.Build.LazyPath, pub fn init(b: *std.Build, cfg: *const Config) !HelpStrings { const exe = b.addExecutable(.{ .name = "helpgen", - .root_source_file = b.path("src/helpgen.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/helpgen.zig"), + .target = b.graph.host, + .strip = false, + .omit_frame_pointer = false, + .unwind_tables = .sync, + }), }); const help_config = config: { diff --git a/src/build/UnicodeTables.zig b/src/build/UnicodeTables.zig index 58af17a6e..5bba2341b 100644 --- a/src/build/UnicodeTables.zig +++ b/src/build/UnicodeTables.zig @@ -12,8 +12,13 @@ output: std.Build.LazyPath, pub fn init(b: *std.Build) !UnicodeTables { const exe = b.addExecutable(.{ .name = "unigen", - .root_source_file = b.path("src/unicode/props.zig"), - .target = b.graph.host, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/unicode/props.zig"), + .target = b.graph.host, + .strip = false, + .omit_frame_pointer = false, + .unwind_tables = .sync, + }), }); if (b.lazyDependency("ziglyph", .{ From ae095d2262230cd23d8c23c16115900156641984 Mon Sep 17 00:00:00 2001 From: Liam Hupfer Date: Mon, 19 May 2025 22:01:33 -0500 Subject: [PATCH 274/642] flatpak: Add --device=all permission Without --device=all, the sandbox gets a dedicated PTY namespace. Commands run on the host via the HostCommand D-Bus interface receive the file descriptors from the namespaced PTY but cannot determine its path via ttyname(3). This breaks commands like tty(1), ps(1) and emacsclient(1). Add --device=all so the host PTY namespace is used when allocating TTYs. Applications with access to org.freedesktop.Flatpak can already give themselves arbitrary permissions, so the sandboxing benefits of restricted device access are limited. For terminal emulators, the primary benefit of Flatpak is the predictability of the distro-independent target runtime rather than sandboxing. --- flatpak/com.mitchellh.ghostty.Devel.yml | 2 ++ flatpak/com.mitchellh.ghostty.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/flatpak/com.mitchellh.ghostty.Devel.yml b/flatpak/com.mitchellh.ghostty.Devel.yml index 244c3987f..fe24a7c56 100644 --- a/flatpak/com.mitchellh.ghostty.Devel.yml +++ b/flatpak/com.mitchellh.ghostty.Devel.yml @@ -14,6 +14,8 @@ desktop-file-name-suffix: " (Devel)" finish-args: # 3D rendering - --device=dri + # use host PTS namespace + - --device=all # Windowing - --share=ipc - --socket=fallback-x11 diff --git a/flatpak/com.mitchellh.ghostty.yml b/flatpak/com.mitchellh.ghostty.yml index 17c92633f..1b119c11b 100644 --- a/flatpak/com.mitchellh.ghostty.yml +++ b/flatpak/com.mitchellh.ghostty.yml @@ -9,6 +9,8 @@ command: ghostty finish-args: # 3D rendering - --device=dri + # use host PTS namespace + - --device=all # Windowing - --share=ipc - --socket=fallback-x11 From 81647bfae6883fb759a6fb005a55eddfc2cae50a Mon Sep 17 00:00:00 2001 From: Emir SARI Date: Wed, 21 May 2025 20:06:07 +0300 Subject: [PATCH 275/642] Update Turkish translations --- po/tr_TR.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index ac1bfdfc7..3de70d61c 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -216,7 +216,7 @@ msgstr "Açık Sekmeleri Görüntüle" #: src/apprt/gtk/Window.zig:249 msgid "New Split" -msgstr "" +msgstr "Yeni Bölme" #: src/apprt/gtk/Window.zig:312 msgid "" From f1c42c9f8c4e1ebc6352fd58e6c20ec0ae9a2b63 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 May 2025 10:14:39 -0700 Subject: [PATCH 276/642] synthetic package This introduces a new package `src/synthetic` for generating synthetic data, currently primarily for benchmarking but other use cases can emerge. The synthetic package exports a runtime-dispatched type `Generator` that can generate data of various types. To start, we have a bytes, utf8, and OSC generator. The goal of each generator is to expose knobs to tune the probabilities of various outcomes. For example, the UTF-8 generator has a knob to tune the probability of generating 1, 2, 3, or 4-byte UTF-8 sequences. Ultimately, the goal is to be able to collect probability data empirically that we can then use for benchmarks so we can optimize various parts of the codebase on real-world data shape distributions. --- src/bench/stream.zig | 129 +++++++++------------ src/bench/synth/main.zig | 15 --- src/bench/synth/osc.zig | 197 -------------------------------- src/main_ghostty.zig | 2 +- src/synthetic/Bytes.zig | 53 +++++++++ src/synthetic/Generator.zig | 42 +++++++ src/synthetic/Osc.zig | 221 ++++++++++++++++++++++++++++++++++++ src/synthetic/Utf8.zig | 103 +++++++++++++++++ src/synthetic/main.zig | 23 ++++ 9 files changed, 497 insertions(+), 288 deletions(-) delete mode 100644 src/bench/synth/main.zig delete mode 100644 src/bench/synth/osc.zig create mode 100644 src/synthetic/Bytes.zig create mode 100644 src/synthetic/Generator.zig create mode 100644 src/synthetic/Osc.zig create mode 100644 src/synthetic/Utf8.zig create mode 100644 src/synthetic/main.zig diff --git a/src/bench/stream.zig b/src/bench/stream.zig index 0c7d421cc..6309c9e7f 100644 --- a/src/bench/stream.zig +++ b/src/bench/stream.zig @@ -12,10 +12,9 @@ const std = @import("std"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; -const ziglyph = @import("ziglyph"); const cli = @import("../cli.zig"); const terminal = @import("../terminal/main.zig"); -const synth = @import("synth/main.zig"); +const synthetic = @import("../synthetic/main.zig"); const Args = struct { mode: Mode = .noop, @@ -102,16 +101,57 @@ pub fn main() !void { const writer = std.io.getStdOut().writer(); const buf = try alloc.alloc(u8, args.@"buffer-size"); + // Build our RNG const seed: u64 = if (args.seed >= 0) @bitCast(args.seed) else @truncate(@as(u128, @bitCast(std.time.nanoTimestamp()))); + var prng = std.Random.DefaultPrng.init(seed); + const rand = prng.random(); // Handle the modes that do not depend on terminal state first. switch (args.mode) { - .@"gen-ascii" => try genAscii(writer, seed), - .@"gen-utf8" => try genUtf8(writer, seed), - .@"gen-rand" => try genRand(writer, seed), - .@"gen-osc" => try genOsc(writer, seed, 0.5), - .@"gen-osc-valid" => try genOsc(writer, seed, 1.0), - .@"gen-osc-invalid" => try genOsc(writer, seed, 0.0), + .@"gen-ascii" => { + var gen: synthetic.Bytes = .{ + .rand = rand, + .alphabet = synthetic.Bytes.Alphabet.ascii, + }; + try generate(writer, gen.generator()); + }, + + .@"gen-utf8" => { + var gen: synthetic.Utf8 = .{ + .rand = rand, + }; + try generate(writer, gen.generator()); + }, + + .@"gen-rand" => { + var gen: synthetic.Bytes = .{ .rand = rand }; + try generate(writer, gen.generator()); + }, + + .@"gen-osc" => { + var gen: synthetic.Osc = .{ + .rand = rand, + .p_valid = 0.5, + }; + try generate(writer, gen.generator()); + }, + + .@"gen-osc-valid" => { + var gen: synthetic.Osc = .{ + .rand = rand, + .p_valid = 1.0, + }; + try generate(writer, gen.generator()); + }, + + .@"gen-osc-invalid" => { + var gen: synthetic.Osc = .{ + .rand = rand, + .p_valid = 0.0, + }; + try generate(writer, gen.generator()); + }, + .noop => try benchNoop(reader, buf), // Handle the ones that depend on terminal state next @@ -145,75 +185,14 @@ pub fn main() !void { } } -/// Generates an infinite stream of random printable ASCII characters. -/// This has no control characters in it at all. -fn genAscii(writer: anytype, seed: u64) !void { - const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~"; - try genData(writer, alphabet, seed); -} - -/// Generates an infinite stream of bytes from the given alphabet. -fn genData(writer: anytype, alphabet: []const u8, seed: u64) !void { - var prng = std.Random.DefaultPrng.init(seed); - const rnd = prng.random(); +fn generate( + writer: anytype, + gen: synthetic.Generator, +) !void { var buf: [1024]u8 = undefined; while (true) { - for (&buf) |*c| { - const idx = rnd.uintLessThanBiased(usize, alphabet.len); - c.* = alphabet[idx]; - } - - writer.writeAll(&buf) catch |err| switch (err) { - error.BrokenPipe => return, // stdout closed - else => return err, - }; - } -} - -fn genUtf8(writer: anytype, seed: u64) !void { - var prng = std.Random.DefaultPrng.init(seed); - const rnd = prng.random(); - var buf: [1024]u8 = undefined; - while (true) { - var i: usize = 0; - while (i <= buf.len - 4) { - const cp: u18 = while (true) { - const cp = rnd.int(u18); - if (ziglyph.isPrint(cp)) break cp; - }; - - i += try std.unicode.utf8Encode(cp, buf[i..]); - } - - writer.writeAll(buf[0..i]) catch |err| switch (err) { - error.BrokenPipe => return, // stdout closed - else => return err, - }; - } -} - -fn genOsc(writer: anytype, seed: u64, p_valid: f64) !void { - var prng = std.Random.DefaultPrng.init(seed); - const gen: synth.OSC = .{ .rand = prng.random(), .p_valid = p_valid }; - - var buf: [1024]u8 = undefined; - while (true) { - const seq = try gen.next(&buf); - writer.writeAll(seq) catch |err| switch (err) { - error.BrokenPipe => return, // stdout closed - else => return err, - }; - } -} - -fn genRand(writer: anytype, seed: u64) !void { - var prng = std.Random.DefaultPrng.init(seed); - const rnd = prng.random(); - var buf: [1024]u8 = undefined; - while (true) { - rnd.bytes(&buf); - - writer.writeAll(&buf) catch |err| switch (err) { + const data = try gen.next(&buf); + writer.writeAll(data) catch |err| switch (err) { error.BrokenPipe => return, // stdout closed else => return err, }; diff --git a/src/bench/synth/main.zig b/src/bench/synth/main.zig deleted file mode 100644 index eda2dec28..000000000 --- a/src/bench/synth/main.zig +++ /dev/null @@ -1,15 +0,0 @@ -//! Package synth contains functions for generating synthetic data for -//! the purpose of benchmarking, primarily. This can also probably be used -//! for testing and fuzzing (probably generating a corpus rather than -//! directly fuzzing) and more. -//! -//! The synthetic data generators in this package are usually not performant -//! enough to be streamed in real time. They should instead be used to -//! generate a large amount of data in a single go and then streamed -//! from there. - -pub const OSC = @import("osc.zig").Generator; - -test { - @import("std").testing.refAllDecls(@This()); -} diff --git a/src/bench/synth/osc.zig b/src/bench/synth/osc.zig deleted file mode 100644 index 61f168b58..000000000 --- a/src/bench/synth/osc.zig +++ /dev/null @@ -1,197 +0,0 @@ -const std = @import("std"); -const assert = std.debug.assert; - -/// Synthetic OSC request generator. -/// -/// I tried to balance generality and practicality. I implemented mainly -/// all I need at the time of writing this, but I think this can be iterated -/// over time to be a general purpose OSC generator with a lot of -/// configurability. I limited the configurability to what I need but still -/// tried to lay out the code in a way that it can be extended easily. -pub const Generator = struct { - /// Random number generator. - rand: std.Random, - - /// Probability of a valid OSC sequence being generated. - p_valid: f64 = 1.0, - - pub const Error = error{NoSpaceLeft}; - - /// We use a FBS as a direct parameter below in non-pub functions, - /// but we should probably just switch to `[]u8`. - const FBS = std.io.FixedBufferStream([]u8); - - /// Get the next OSC request in bytes. The generated OSC request will - /// have the prefix `ESC ]` and the terminator `BEL` (0x07). - /// - /// This will generate both valid and invalid OSC requests (based on - /// the `p_valid` probability value). Invalid requests still have the - /// prefix and terminator, but the content in between is not a valid - /// OSC request. - /// - /// The buffer must be at least 3 bytes long to accommodate the - /// prefix and terminator. - pub fn next(self: *const Generator, buf: []u8) Error![]const u8 { - assert(buf.len >= 3); - var fbs: FBS = std.io.fixedBufferStream(buf); - const writer = fbs.writer(); - - // Start OSC (ESC ]) - try writer.writeAll("\x1b]"); - - // Determine if we are generating a valid or invalid OSC request. - switch (self.chooseValidity()) { - .valid => try self.nextValid(&fbs), - .invalid => try self.nextInvalid(&fbs), - } - - // Terminate OSC - try writer.writeAll("\x07"); - return fbs.getWritten(); - } - - fn nextValid(self: *const Generator, fbs: *FBS) Error!void { - try self.nextValidExact(fbs, self.rand.enumValue(ValidKind)); - } - - fn nextValidExact(self: *const Generator, fbs: *FBS, k: ValidKind) Error!void { - switch (k) { - .change_window_title => { - try fbs.writer().writeAll("0;"); // Set window title - try self.randomBytes(fbs, 1, fbs.buffer.len); - }, - - .prompt_start => { - try fbs.writer().writeAll("133;A"); // Start prompt - - // aid - if (self.rand.boolean()) { - try fbs.writer().writeAll(";aid="); - try self.randomBytes(fbs, 1, 16); - } - - // redraw - if (self.rand.boolean()) { - try fbs.writer().writeAll(";redraw="); - if (self.rand.boolean()) { - try fbs.writer().writeAll("1"); - } else { - try fbs.writer().writeAll("0"); - } - } - }, - - .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt - } - } - - fn nextInvalid(self: *const Generator, fbs: *FBS) Error!void { - switch (self.rand.enumValue(InvalidKind)) { - .random => try self.randomBytes(fbs, 1, fbs.buffer.len), - .good_prefix => { - try fbs.writer().writeAll("133;"); - try self.randomBytes(fbs, 2, fbs.buffer.len); - }, - } - } - - /// Generate a random string of bytes up to `max_len` bytes or - /// until we run out of space in the buffer, whichever is - /// smaller. - /// - /// This will avoid the terminator characters (0x1B and 0x07) and - /// replace them by incrementing them by one. - fn randomBytes( - self: *const Generator, - fbs: *FBS, - min_len: usize, - max_len: usize, - ) Error!void { - const len = @min( - self.rand.intRangeAtMostBiased(usize, min_len, max_len), - fbs.buffer.len - fbs.pos - 1, // leave space for terminator - ); - var rem: usize = len; - var buf: [1024]u8 = undefined; - while (rem > 0) { - self.rand.bytes(&buf); - std.mem.replaceScalar(u8, &buf, 0x1B, 0x1C); - std.mem.replaceScalar(u8, &buf, 0x07, 0x08); - - const n = @min(rem, buf.len); - try fbs.writer().writeAll(buf[0..n]); - rem -= n; - } - } - - /// Choose whether to generate a valid or invalid OSC request based - /// on the validity probability. - fn chooseValidity(self: *const Generator) Validity { - return if (self.rand.float(f64) > self.p_valid) - .invalid - else - .valid; - } - - const Validity = enum { valid, invalid }; - - const ValidKind = enum { - change_window_title, - prompt_start, - prompt_end, - }; - - const InvalidKind = enum { - /// Literally random bytes. Might even be valid, but probably not. - random, - - /// A good prefix, but ultimately invalid format. - good_prefix, - }; -}; - -/// A fixed seed we can use for our tests to avoid flakes. -const test_seed = 0xC0FFEEEEEEEEEEEE; - -test "OSC generator" { - var prng = std.Random.DefaultPrng.init(test_seed); - var buf: [4096]u8 = undefined; - const gen: Generator = .{ .rand = prng.random() }; - for (0..50) |_| _ = try gen.next(&buf); -} - -test "OSC generator valid" { - const testing = std.testing; - const terminal = @import("../../terminal/main.zig"); - - var prng = std.Random.DefaultPrng.init(test_seed); - var buf: [256]u8 = undefined; - const gen: Generator = .{ - .rand = prng.random(), - .p_valid = 1.0, - }; - for (0..50) |_| { - const seq = try gen.next(&buf); - var parser: terminal.osc.Parser = .{}; - for (seq[2 .. seq.len - 1]) |c| parser.next(c); - try testing.expect(parser.end(null) != null); - } -} - -test "OSC generator invalid" { - const testing = std.testing; - const terminal = @import("../../terminal/main.zig"); - - var prng = std.Random.DefaultPrng.init(test_seed); - var buf: [256]u8 = undefined; - const gen: Generator = .{ - .rand = prng.random(), - .p_valid = 0.0, - }; - for (0..50) |_| { - const seq = try gen.next(&buf); - var parser: terminal.osc.Parser = .{}; - for (seq[2 .. seq.len - 1]) |c| parser.next(c); - try testing.expect(parser.end(null) == null); - } -} diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index 4a9f2b138..985c6c9bd 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -182,12 +182,12 @@ test { _ = @import("surface_mouse.zig"); // Libraries - _ = @import("bench/synth/main.zig"); _ = @import("crash/main.zig"); _ = @import("datastruct/main.zig"); _ = @import("inspector/main.zig"); _ = @import("terminal/main.zig"); _ = @import("terminfo/main.zig"); _ = @import("simd/main.zig"); + _ = @import("synthetic/main.zig"); _ = @import("unicode/main.zig"); } diff --git a/src/synthetic/Bytes.zig b/src/synthetic/Bytes.zig new file mode 100644 index 000000000..8a8207ba9 --- /dev/null +++ b/src/synthetic/Bytes.zig @@ -0,0 +1,53 @@ +/// Generates bytes. +const Bytes = @This(); + +const std = @import("std"); +const Generator = @import("Generator.zig"); + +/// Random number generator. +rand: std.Random, + +/// The minimum and maximum length of the generated bytes. The maximum +/// length will be capped to the length of the buffer passed in if the +/// buffer length is smaller. +min_len: usize = 1, +max_len: usize = std.math.maxInt(usize), + +/// The possible bytes that can be generated. If a byte is duplicated +/// in the alphabet, it will be more likely to be generated. That's a +/// side effect of the generator, not an intended use case. +alphabet: ?[]const u8 = null, + +/// Predefined alphabets. +pub const Alphabet = struct { + pub const ascii = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;':\\\",./<>?`~"; +}; + +pub fn generator(self: *Bytes) Generator { + return .init(self, next); +} + +pub fn next(self: *Bytes, buf: []u8) Generator.Error![]const u8 { + const len = @min( + self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), + buf.len, + ); + + const result = buf[0..len]; + self.rand.bytes(result); + if (self.alphabet) |alphabet| { + for (result) |*byte| byte.* = alphabet[byte.* % alphabet.len]; + } + + return result; +} + +test "bytes" { + const testing = std.testing; + var prng = std.Random.DefaultPrng.init(0); + var buf: [256]u8 = undefined; + var v: Bytes = .{ .rand = prng.random() }; + const gen = v.generator(); + const result = try gen.next(&buf); + try testing.expect(result.len > 0); +} diff --git a/src/synthetic/Generator.zig b/src/synthetic/Generator.zig new file mode 100644 index 000000000..7478a54c3 --- /dev/null +++ b/src/synthetic/Generator.zig @@ -0,0 +1,42 @@ +/// A common interface for all generators. +const Generator = @This(); + +const std = @import("std"); +const assert = std.debug.assert; + +/// For generators, this is the only error that is allowed to be +/// returned by the next function. +pub const Error = error{NoSpaceLeft}; + +/// The vtable for the generator. +ptr: *anyopaque, +nextFn: *const fn (ptr: *anyopaque, buf: []u8) Error![]const u8, + +/// Create a new generator from a pointer and a function pointer. +/// This usually is only called by generator implementations, not +/// generator users. +pub fn init( + pointer: anytype, + comptime nextFn: fn (ptr: @TypeOf(pointer), buf: []u8) Error![]const u8, +) Generator { + const Ptr = @TypeOf(pointer); + assert(@typeInfo(Ptr) == .pointer); // Must be a pointer + assert(@typeInfo(Ptr).pointer.size == .one); // Must be a single-item pointer + assert(@typeInfo(@typeInfo(Ptr).pointer.child) == .@"struct"); // Must point to a struct + const gen = struct { + fn next(ptr: *anyopaque, buf: []u8) Error![]const u8 { + const self: Ptr = @ptrCast(@alignCast(ptr)); + return try nextFn(self, buf); + } + }; + + return .{ + .ptr = pointer, + .nextFn = gen.next, + }; +} + +/// Get the next value from the generator. Returns the data written. +pub fn next(self: Generator, buf: []u8) Error![]const u8 { + return try self.nextFn(self.ptr, buf); +} diff --git a/src/synthetic/Osc.zig b/src/synthetic/Osc.zig new file mode 100644 index 000000000..e0a6b42a0 --- /dev/null +++ b/src/synthetic/Osc.zig @@ -0,0 +1,221 @@ +/// Generates random terminal OSC requests. +const Osc = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Generator = @import("Generator.zig"); +const Bytes = @import("Bytes.zig"); + +/// Valid OSC request kinds that can be generated. +pub const ValidKind = enum { + change_window_title, + prompt_start, + prompt_end, +}; + +/// Invalid OSC request kinds that can be generated. +pub const InvalidKind = enum { + /// Literally random bytes. Might even be valid, but probably not. + random, + + /// A good prefix, but ultimately invalid format. + good_prefix, +}; + +/// Random number generator. +rand: std.Random, + +/// Probability of a valid OSC sequence being generated. +p_valid: f64 = 1.0, + +/// Probabilities of specific valid or invalid OSC request kinds. +/// The probabilities are weighted relative to each other, so they +/// can sum greater than 1.0. A kind of weight 1.0 and a kind of +/// weight 2.0 will have a 2:1 chance of the latter being selected. +p_valid_kind: std.enums.EnumArray(ValidKind, f64) = .initFill(1.0), +p_invalid_kind: std.enums.EnumArray(InvalidKind, f64) = .initFill(1.0), + +/// The alphabet for random bytes (omitting 0x1B and 0x07). +const bytes_alphabet: []const u8 = alphabet: { + var alphabet: [256]u8 = undefined; + for (0..alphabet.len) |i| { + if (i == 0x1B or i == 0x07) { + alphabet[i] = @intCast(i + 1); + } else { + alphabet[i] = @intCast(i); + } + } + const result = alphabet; + break :alphabet &result; +}; + +pub fn generator(self: *Osc) Generator { + return .init(self, next); +} + +/// Get the next OSC request in bytes. The generated OSC request will +/// have the prefix `ESC ]` and the terminator `BEL` (0x07). +/// +/// This will generate both valid and invalid OSC requests (based on +/// the `p_valid` probability value). Invalid requests still have the +/// prefix and terminator, but the content in between is not a valid +/// OSC request. +/// +/// The buffer must be at least 3 bytes long to accommodate the +/// prefix and terminator. +pub fn next(self: *Osc, buf: []u8) Generator.Error![]const u8 { + if (buf.len < 3) return error.NoSpaceLeft; + const unwrapped = try self.nextUnwrapped(buf[2 .. buf.len - 1]); + buf[0] = 0x1B; // ESC + buf[1] = ']'; + buf[unwrapped.len + 2] = 0x07; // BEL + return buf[0 .. unwrapped.len + 3]; +} + +fn nextUnwrapped(self: *Osc, buf: []u8) Generator.Error![]const u8 { + return switch (self.chooseValidity()) { + .valid => valid: { + const Indexer = @TypeOf(self.p_valid_kind).Indexer; + const idx = self.rand.weightedIndex(f64, &self.p_valid_kind.values); + break :valid try self.nextUnwrappedValidExact( + buf, + Indexer.keyForIndex(idx), + ); + }, + + .invalid => invalid: { + const Indexer = @TypeOf(self.p_invalid_kind).Indexer; + const idx = self.rand.weightedIndex(f64, &self.p_invalid_kind.values); + break :invalid try self.nextUnwrappedInvalidExact( + buf, + Indexer.keyForIndex(idx), + ); + }, + }; +} + +fn nextUnwrappedValidExact(self: *const Osc, buf: []u8, k: ValidKind) Generator.Error![]const u8 { + var fbs = std.io.fixedBufferStream(buf); + switch (k) { + .change_window_title => { + try fbs.writer().writeAll("0;"); // Set window title + var bytes_gen = self.bytes(); + const title = try bytes_gen.next(fbs.buffer[fbs.pos..]); + try fbs.seekBy(@intCast(title.len)); + }, + + .prompt_start => { + try fbs.writer().writeAll("133;A"); // Start prompt + + // aid + if (self.rand.boolean()) { + var bytes_gen = self.bytes(); + bytes_gen.max_len = 16; + try fbs.writer().writeAll(";aid="); + const aid = try bytes_gen.next(fbs.buffer[fbs.pos..]); + try fbs.seekBy(@intCast(aid.len)); + } + + // redraw + if (self.rand.boolean()) { + try fbs.writer().writeAll(";redraw="); + if (self.rand.boolean()) { + try fbs.writer().writeAll("1"); + } else { + try fbs.writer().writeAll("0"); + } + } + }, + + .prompt_end => try fbs.writer().writeAll("133;B"), // End prompt + } + + return fbs.getWritten(); +} + +fn nextUnwrappedInvalidExact( + self: *const Osc, + buf: []u8, + k: InvalidKind, +) Generator.Error![]const u8 { + switch (k) { + .random => { + var bytes_gen = self.bytes(); + return try bytes_gen.next(buf); + }, + + .good_prefix => { + var fbs = std.io.fixedBufferStream(buf); + try fbs.writer().writeAll("133;"); + var bytes_gen = self.bytes(); + const data = try bytes_gen.next(fbs.buffer[fbs.pos..]); + try fbs.seekBy(@intCast(data.len)); + return fbs.getWritten(); + }, + } +} + +fn bytes(self: *const Osc) Bytes { + return .{ + .rand = self.rand, + .alphabet = bytes_alphabet, + }; +} + +/// Choose whether to generate a valid or invalid OSC request based +/// on the validity probability. +fn chooseValidity(self: *const Osc) Validity { + return if (self.rand.float(f64) > self.p_valid) + .invalid + else + .valid; +} + +const Validity = enum { valid, invalid }; + +/// A fixed seed we can use for our tests to avoid flakes. +const test_seed = 0xC0FFEEEEEEEEEEEE; + +test "OSC generator" { + var prng = std.Random.DefaultPrng.init(test_seed); + var buf: [4096]u8 = undefined; + var v: Osc = .{ .rand = prng.random() }; + const gen = v.generator(); + for (0..50) |_| _ = try gen.next(&buf); +} + +test "OSC generator valid" { + const testing = std.testing; + const terminal = @import("../terminal/main.zig"); + + var prng = std.Random.DefaultPrng.init(test_seed); + var buf: [256]u8 = undefined; + var gen: Osc = .{ + .rand = prng.random(), + .p_valid = 1.0, + }; + for (0..50) |_| { + const seq = try gen.next(&buf); + var parser: terminal.osc.Parser = .{}; + for (seq[2 .. seq.len - 1]) |c| parser.next(c); + try testing.expect(parser.end(null) != null); + } +} + +test "OSC generator invalid" { + const testing = std.testing; + const terminal = @import("../terminal/main.zig"); + + var prng = std.Random.DefaultPrng.init(test_seed); + var buf: [256]u8 = undefined; + var gen: Osc = .{ + .rand = prng.random(), + .p_valid = 0.0, + }; + for (0..50) |_| { + const seq = try gen.next(&buf); + var parser: terminal.osc.Parser = .{}; + for (seq[2 .. seq.len - 1]) |c| parser.next(c); + try testing.expect(parser.end(null) == null); + } +} diff --git a/src/synthetic/Utf8.zig b/src/synthetic/Utf8.zig new file mode 100644 index 000000000..c3ace6505 --- /dev/null +++ b/src/synthetic/Utf8.zig @@ -0,0 +1,103 @@ +/// Generates UTF-8. +/// +/// This doesn't yet generate multi-codepoint graphemes, but it +/// has the ability to generate a custom distribution of UTF-8 +/// encoding lengths (1, 2, 3, or 4 bytes). +const Utf8 = @This(); + +const std = @import("std"); +const assert = std.debug.assert; +const Generator = @import("Generator.zig"); + +/// Possible UTF-8 encoding lengths. +pub const Utf8Len = enum(u3) { + one = 1, + two = 2, + three = 3, + four = 4, +}; + +/// Random number generator. +rand: std.Random, + +/// The minimum and maximum length of the generated bytes. The maximum +/// length will be capped to the length of the buffer passed in if the +/// buffer length is smaller. +min_len: usize = 1, +max_len: usize = std.math.maxInt(usize), + +/// Probability of a specific UTF-8 encoding length being generated. +/// The probabilities are weighted relative to each other, so they +/// can sum greater than 1.0. A length of weight 1.0 and a length +/// of weight 2.0 will have a 2:1 chance of the latter being +/// selected. +/// +/// If a UTF-8 encoding of a chosen length can't fit into the remaining +/// buffer, a smaller length will be chosen. For small buffers this may +/// skew the distribution of lengths. +p_length: std.enums.EnumArray(Utf8Len, f64) = .initFill(1.0), + +pub fn generator(self: *Utf8) Generator { + return .init(self, next); +} + +pub fn next(self: *Utf8, buf: []u8) Generator.Error![]const u8 { + const len = @min( + self.rand.intRangeAtMostBiased(usize, self.min_len, self.max_len), + buf.len, + ); + + const result = buf[0..len]; + var rem: usize = len; + while (rem > 0) { + // Pick a utf8 byte count to generate. + const utf8_len: Utf8Len = len: { + const Indexer = @TypeOf(self.p_length).Indexer; + const idx = self.rand.weightedIndex(f64, &self.p_length.values); + var utf8_len = Indexer.keyForIndex(idx); + assert(rem > 0); + while (@intFromEnum(utf8_len) > rem) { + // If the chosen length can't fit into the remaining buffer, + // choose a smaller length. + utf8_len = @enumFromInt(@intFromEnum(utf8_len) - 1); + } + break :len utf8_len; + }; + + // Generate a UTF-8 sequence that encodes to this length. + const cp: u21 = switch (utf8_len) { + .one => self.rand.intRangeAtMostBiased(u21, 0x00, 0x7F), + .two => self.rand.intRangeAtMostBiased(u21, 0x80, 0x7FF), + .three => self.rand.intRangeAtMostBiased(u21, 0x800, 0xFFFF), + .four => self.rand.intRangeAtMostBiased(u21, 0x10000, 0x10FFFF), + }; + + assert(std.unicode.utf8CodepointSequenceLength( + cp, + ) catch unreachable == @intFromEnum(utf8_len)); + rem -= std.unicode.utf8Encode( + cp, + result[result.len - rem ..], + ) catch |err| switch (err) { + // Impossible because our generation above is hardcoded to + // produce a valid range. If not, a bug. + error.CodepointTooLarge => unreachable, + + // Possible, in which case we redo the loop and encode nothing. + error.Utf8CannotEncodeSurrogateHalf => continue, + }; + } + + return result; +} + +test "utf8" { + const testing = std.testing; + var prng = std.Random.DefaultPrng.init(0); + var buf: [256]u8 = undefined; + var v: Utf8 = .{ .rand = prng.random() }; + const gen = v.generator(); + const result = try gen.next(&buf); + try testing.expect(result.len > 0); + try testing.expect(std.unicode.utf8ValidateSlice(result)); +} diff --git a/src/synthetic/main.zig b/src/synthetic/main.zig new file mode 100644 index 000000000..67cd47054 --- /dev/null +++ b/src/synthetic/main.zig @@ -0,0 +1,23 @@ +//! The synthetic package contains an abstraction for generating +//! synthetic data. The motivating use case for this package is to +//! generate synthetic data for benchmarking, but it may also expand +//! to other use cases such as fuzzing (e.g. to generate a corpus +//! rather than directly fuzzing). +//! +//! The generators in this package are typically not performant +//! enough to be streamed in real time. They should instead be +//! used to generate a large amount of data in a single go +//! and then streamed from there. +//! +//! The generators are aimed for terminal emulation, but the package +//! is not limited to that and we may want to extract this to a +//! standalone package one day. + +pub const Generator = @import("Generator.zig"); +pub const Bytes = @import("Bytes.zig"); +pub const Utf8 = @import("Utf8.zig"); +pub const Osc = @import("Osc.zig"); + +test { + @import("std").testing.refAllDecls(@This()); +} From adbf834c36e9d780aa7da855b3f278ba7231a0e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 17:40:25 +0000 Subject: [PATCH 277/642] build(deps): bump cachix/install-nix-action from 30 to 31 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 30 to 31. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md) - [Commits](https://github.com/cachix/install-nix-action/compare/v30...v31) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-version: '31' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-pr.yml | 4 +-- .github/workflows/release-tag.yml | 4 +-- .github/workflows/release-tip.yml | 8 ++--- .github/workflows/test.yml | 42 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index ec55f2dff..f87f27c5a 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -42,7 +42,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@v30 + uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index ced497997..62ec4ff7c 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -57,7 +57,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -209,7 +209,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index ab103d6df..7400e1d40 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -89,7 +89,7 @@ jobs: /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable @@ -130,7 +130,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index d23787743..7510a8b52 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -112,7 +112,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -164,7 +164,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -379,7 +379,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -554,7 +554,7 @@ jobs: fetch-depth: 0 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b32eda0f6..b09a6d095 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -74,7 +74,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -105,7 +105,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -141,7 +141,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -170,7 +170,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -203,7 +203,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -247,7 +247,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -276,7 +276,7 @@ jobs: uses: actions/checkout@v4 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -316,7 +316,7 @@ jobs: uses: actions/checkout@v4 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -499,7 +499,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -530,7 +530,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -575,7 +575,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -614,7 +614,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -634,7 +634,7 @@ jobs: uses: actions/checkout@v4 # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -667,7 +667,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -694,7 +694,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -721,7 +721,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -748,7 +748,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -775,7 +775,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -802,7 +802,7 @@ jobs: path: | /nix /zig - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -839,7 +839,7 @@ jobs: /zig # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v30 + - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 @@ -896,7 +896,7 @@ jobs: /nix /zig - name: Setup Nix - uses: cachix/install-nix-action@v30 + uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index fed6d2db7..1481c3a86 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -29,7 +29,7 @@ jobs: /zig - name: Setup Nix - uses: cachix/install-nix-action@v30 + uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - uses: cachix/cachix-action@v15 From 56fb1cbaaf6cfca40fa5130d6c0511e65a36ce19 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 17:40:28 +0000 Subject: [PATCH 278/642] build(deps): bump namespacelabs/nscloud-cache-action from 1.2.0 to 1.2.7 Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.0 to 1.2.7. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/v1.2.0...v1.2.7) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.2.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/test.yml | 38 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index ec55f2dff..e28d71daf 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index ab103d6df..7b2b4ae9f 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index d23787743..bad9c40bf 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b32eda0f6..bacfbc42a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -98,7 +98,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -134,7 +134,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -163,7 +163,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -196,7 +196,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -240,7 +240,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -382,7 +382,7 @@ jobs: mkdir dist tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -492,7 +492,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -523,7 +523,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -568,7 +568,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -607,7 +607,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -662,7 +662,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -689,7 +689,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -716,7 +716,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -743,7 +743,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -770,7 +770,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -797,7 +797,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -832,7 +832,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix @@ -890,7 +890,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index fed6d2db7..0eef9d9c9 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.0 + uses: namespacelabs/nscloud-cache-action@v1.2.7 with: path: | /nix From 907956130035eb086ea7428ce100f9b32f3f4600 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 May 2025 21:15:50 +0000 Subject: [PATCH 279/642] build(deps): bump cachix/cachix-action from 15 to 16 Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 15 to 16. - [Release notes](https://github.com/cachix/cachix-action/releases) - [Commits](https://github.com/cachix/cachix-action/compare/v15...v16) --- updated-dependencies: - dependency-name: cachix/cachix-action dependency-version: '16' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-pr.yml | 4 +-- .github/workflows/release-tag.yml | 4 +-- .github/workflows/release-tip.yml | 8 ++--- .github/workflows/test.yml | 42 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index f87f27c5a..7a64ae605 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -45,7 +45,7 @@ jobs: uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 62ec4ff7c..574b1ab73 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -60,7 +60,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -212,7 +212,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 7400e1d40..26a08be89 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -93,7 +93,7 @@ jobs: with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -133,7 +133,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 7510a8b52..84ba3d3de 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -115,7 +115,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -167,7 +167,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -382,7 +382,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -557,7 +557,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b09a6d095..2016fd41c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -77,7 +77,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -108,7 +108,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -144,7 +144,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -173,7 +173,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -206,7 +206,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -250,7 +250,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -279,7 +279,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -319,7 +319,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -502,7 +502,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -533,7 +533,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -578,7 +578,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -617,7 +617,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -637,7 +637,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -670,7 +670,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -697,7 +697,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -724,7 +724,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -751,7 +751,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -778,7 +778,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -805,7 +805,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -842,7 +842,7 @@ jobs: - uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" @@ -899,7 +899,7 @@ jobs: uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 1481c3a86..855e7f637 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -32,7 +32,7 @@ jobs: uses: cachix/install-nix-action@v31 with: nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v15 + - uses: cachix/cachix-action@v16 with: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" From ab25600b2dd63d877b3ed56e58f8350dd30dd700 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoffer=20T=C3=B8nnessen?= Date: Fri, 23 May 2025 10:46:24 +0200 Subject: [PATCH 280/642] Add new and update Norwegian split translations This change changes the wording on the split pane functionality. The new wording is taken from the macOS terminal app when the whole system is translated to Norwegian. macOS uses "Del opp vindu" and "Lukk delt vindu" for "Split Pane" and "Close Split Pane". So instead of using "split" the verb in question is always "del". Personally I find this translation to be better rooted in Norwegian. When looking at the German translation, which is often a good indicator for Norwegian as well, one can see the same wording being used. --- po/nb_NO.UTF-8.po | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index ad76eea3d..2685d67bb 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -63,25 +63,25 @@ msgstr "Last konfigurasjon på nytt" #: 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 "Splitt opp" +msgstr "Del oppover" #: 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 "Splitt ned" +msgstr "Del nedover" #: 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 "Splitt venstre" +msgstr "Del til venstre" #: 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 "Splitt høyre" +msgstr "Del til høyre" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6 @@ -107,7 +107,7 @@ msgstr "Nullstill" #: 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 "Splitt" +msgstr "Del vindu" #: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33 #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45 @@ -218,7 +218,7 @@ msgstr "Se åpne faner" #: src/apprt/gtk/Window.zig:249 msgid "New Split" -msgstr "" +msgstr "Del opp vindu" #: src/apprt/gtk/Window.zig:312 msgid "" @@ -251,7 +251,7 @@ msgstr "Lukk fane?" #: src/apprt/gtk/CloseDialog.zig:90 msgid "Close Split?" -msgstr "Lukk splitt?" +msgstr "Lukk delt vindu?" #: src/apprt/gtk/CloseDialog.zig:96 msgid "All terminal sessions will be terminated." From a8651882a752ecba3d3ad2177d7b288969d4a31d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Sat, 24 May 2025 00:29:53 +0200 Subject: [PATCH 281/642] add cut/copy/paste keys The origin of these keys are old sun keyboards. They are getting picked up by the custom (progammable) keyboard scene (see https://github.com/manna-harbour/miryoku for a popular layout). Support in ghosty is quite handy because it allows to bind copy/paste in a way that doesn't overlap with ctrl-c/ctrl-v, which can have special bindings in some terminal applications. --- include/ghostty.h | 5 +++++ src/apprt/gtk/key.zig | 4 ++++ src/input/key.zig | 8 ++++++++ src/input/keycodes.zig | 3 +++ 4 files changed, 20 insertions(+) diff --git a/include/ghostty.h b/include/ghostty.h index 941223943..950f5ef80 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -292,6 +292,11 @@ typedef enum { GHOSTTY_KEY_AUDIO_VOLUME_MUTE, GHOSTTY_KEY_AUDIO_VOLUME_UP, GHOSTTY_KEY_WAKE_UP, + + // "Legacy, Non-standard, and Special Keys" § 3.7 + GHOSTTY_KEY_COPY, + GHOSTTY_KEY_CUT, + GHOSTTY_KEY_PASTE, } ghostty_input_key_e; typedef struct { diff --git a/src/apprt/gtk/key.zig b/src/apprt/gtk/key.zig index 3dcfaed98..fc3296366 100644 --- a/src/apprt/gtk/key.zig +++ b/src/apprt/gtk/key.zig @@ -388,6 +388,10 @@ const keymap: []const RawEntry = &.{ .{ gdk.KEY_KP_Delete, .numpad_delete }, .{ gdk.KEY_KP_Begin, .numpad_begin }, + .{ gdk.KEY_Copy, .copy }, + .{ gdk.KEY_Cut, .cut }, + .{ gdk.KEY_Paste, .paste }, + .{ gdk.KEY_Shift_L, .shift_left }, .{ gdk.KEY_Control_L, .control_left }, .{ gdk.KEY_Alt_L, .alt_left }, diff --git a/src/input/key.zig b/src/input/key.zig index 9dad37d78..28aa3ccf4 100644 --- a/src/input/key.zig +++ b/src/input/key.zig @@ -454,6 +454,11 @@ pub const Key = enum(c_int) { audio_volume_up, wake_up, + // "Legacy, Non-standard, and Special Keys" § 3.7 + copy, + cut, + paste, + /// Converts an ASCII character to a key, if possible. This returns /// null if the character is unknown. /// @@ -797,6 +802,9 @@ pub const Key = enum(c_int) { .audio_volume_up, .wake_up, .help, + .copy, + .cut, + .paste, => null, .unidentified, diff --git a/src/input/keycodes.zig b/src/input/keycodes.zig index b4004088e..a85f36d31 100644 --- a/src/input/keycodes.zig +++ b/src/input/keycodes.zig @@ -130,6 +130,9 @@ const code_to_key = code_to_key: { .{ "PageUp", .page_up }, .{ "Delete", .delete }, .{ "End", .end }, + .{ "Copy", .copy }, + .{ "Cut", .cut }, + .{ "Paste", .paste }, .{ "PageDown", .page_down }, .{ "ArrowRight", .arrow_right }, .{ "ArrowLeft", .arrow_left }, From b94d2da56745eca0544012aa443ea71a54a195b6 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 25 May 2025 00:15:05 +0000 Subject: [PATCH 282/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 796ce1475..3c6ed95ed 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz", - .hash = "N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz", + .hash = "N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 68ec4522a..b1d919f3a 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn": { + "N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz", - "hash": "sha256-DKWVUxZEZA8x+3njPaTucr/u/Mmhef0YwhwOnOWn/N4=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz", + "hash": "sha256-2AsOCV9RymfDbhFFRdNVE+GYCAmE713tM27TBPKxAW0=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 7c3e08d2d..ce4a656c7 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn"; + name = "N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz"; - hash = "sha256-DKWVUxZEZA8x+3njPaTucr/u/Mmhef0YwhwOnOWn/N4="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz"; + hash = "sha256-2AsOCV9RymfDbhFFRdNVE+GYCAmE713tM27TBPKxAW0="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 0c71c80e4..cb8195752 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 2ee48f269..d56e6d121 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/f979d8b1959d004390acede9f298be389cb9a1e0.tar.gz", - "dest": "vendor/p/N-V-__8AANf-XQSCQIcmjPV_GQZLPBxaAgzzw_3UWOmkDUXn", - "sha256": "0ca595531644640f31fb79e33da4ee72bfeefcc9a179fd18c21c0e9ce5a7fcde" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz", + "dest": "vendor/p/N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj", + "sha256": "d80b0e095f51ca67c36e114545d35513e198080984ef5ded336ed304f2b1016d" }, { "type": "archive", From 0415a65083fa64b89b9b2048e1b8d02e1411c2f3 Mon Sep 17 00:00:00 2001 From: alex-huff Date: Sun, 25 May 2025 11:43:40 -0500 Subject: [PATCH 283/642] gtk: improve app id validation 'g_application_id_is_valid' doesn't allow empty elements or elements that start with digits. This commit updates 'isValidAppId' to be more consistant with 'g_application_id_is_valid' avoiding the app id defaulting to 'GTK Application' for app ids like '0foo.bar' or 'foo..bar'. --- src/apprt/gtk/App.zig | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index da828b973..9e5037d5c 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -1690,30 +1690,33 @@ fn initActions(self: *App) void { } fn isValidAppId(app_id: [:0]const u8) bool { - if (app_id.len > 255 or app_id.len == 0) return false; - if (app_id[0] == '.') return false; - if (app_id[app_id.len - 1] == '.') return false; + if (app_id.len > 255) return false; - var hasDot = false; + var hasSep = false; + var lastWasSep = true; for (app_id) |char| { switch (char) { - 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {}, - '.' => hasDot = true, + 'a'...'z', 'A'...'Z', '_', '-' => {}, + '0'...'9', '.' => if (lastWasSep) return false, else => return false, } + lastWasSep = char == '.'; + hasSep = hasSep or lastWasSep; } - if (!hasDot) return false; - - return true; + return hasSep and !lastWasSep; } test "isValidAppId" { try testing.expect(isValidAppId("foo.bar")); try testing.expect(isValidAppId("foo.bar.baz")); + try testing.expect(isValidAppId("f00.bar")); + try testing.expect(isValidAppId("foo-bar._baz")); try testing.expect(!isValidAppId("foo")); try testing.expect(!isValidAppId("foo.bar?")); try testing.expect(!isValidAppId("foo.")); try testing.expect(!isValidAppId(".foo")); try testing.expect(!isValidAppId("")); try testing.expect(!isValidAppId("foo" ** 86)); + try testing.expect(!isValidAppId("foo..bar")); + try testing.expect(!isValidAppId("0foo.bar")); } From 113c196078bc21c0dfd6d15d04203a255839a091 Mon Sep 17 00:00:00 2001 From: alex-huff Date: Sun, 25 May 2025 13:20:29 -0500 Subject: [PATCH 284/642] gtk: use 'gio.Application.idIsValid' instead of 'isValidAppId' --- src/apprt/gtk/App.zig | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 9e5037d5c..55c0be5e0 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -288,7 +288,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // can develop Ghostty in Ghostty. const app_id: [:0]const u8 = app_id: { if (config.class) |class| { - if (isValidAppId(class)) { + if (gio.Application.idIsValid(class) != 0) { break :app_id class; } else { log.warn("invalid 'class' in config, ignoring", .{}); @@ -1688,35 +1688,3 @@ fn initActions(self: *App) void { action_map.addAction(action.as(gio.Action)); } } - -fn isValidAppId(app_id: [:0]const u8) bool { - if (app_id.len > 255) return false; - - var hasSep = false; - var lastWasSep = true; - for (app_id) |char| { - switch (char) { - 'a'...'z', 'A'...'Z', '_', '-' => {}, - '0'...'9', '.' => if (lastWasSep) return false, - else => return false, - } - lastWasSep = char == '.'; - hasSep = hasSep or lastWasSep; - } - return hasSep and !lastWasSep; -} - -test "isValidAppId" { - try testing.expect(isValidAppId("foo.bar")); - try testing.expect(isValidAppId("foo.bar.baz")); - try testing.expect(isValidAppId("f00.bar")); - try testing.expect(isValidAppId("foo-bar._baz")); - try testing.expect(!isValidAppId("foo")); - try testing.expect(!isValidAppId("foo.bar?")); - try testing.expect(!isValidAppId("foo.")); - try testing.expect(!isValidAppId(".foo")); - try testing.expect(!isValidAppId("")); - try testing.expect(!isValidAppId("foo" ** 86)); - try testing.expect(!isValidAppId("foo..bar")); - try testing.expect(!isValidAppId("0foo.bar")); -} From 19db2e2755c9abb4946fbb2dbadea85516703138 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 22:25:23 -0600 Subject: [PATCH 285/642] CircBuf: non-allocating rotateToZero We can call `std.mem.rotate` for this. --- src/datastruct/circ_buf.zig | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/datastruct/circ_buf.zig b/src/datastruct/circ_buf.zig index 065bf6a1d..646a00940 100644 --- a/src/datastruct/circ_buf.zig +++ b/src/datastruct/circ_buf.zig @@ -152,7 +152,7 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { /// If larger, new values will be set to the default value. pub fn resize(self: *Self, alloc: Allocator, size: usize) Allocator.Error!void { // Rotate to zero so it is aligned. - try self.rotateToZero(alloc); + try self.rotateToZero(); // Reallocate, this adds to the end so we're ready to go. const prev_len = self.len(); @@ -173,29 +173,16 @@ pub fn CircBuf(comptime T: type, comptime default: T) type { } /// Rotate the data so that it is zero-aligned. - fn rotateToZero(self: *Self, alloc: Allocator) Allocator.Error!void { - // TODO: this does this in the worst possible way by allocating. - // rewrite to not allocate, its possible, I'm just lazy right now. - + fn rotateToZero(self: *Self) Allocator.Error!void { // If we're already at zero then do nothing. if (self.tail == 0) return; - var buf = try alloc.alloc(T, self.storage.len); - defer { - self.head = if (self.full) 0 else self.len(); - self.tail = 0; - alloc.free(self.storage); - self.storage = buf; - } + // We use std.mem.rotate to rotate our storage in-place. + std.mem.rotate(T, self.storage, self.tail); - if (!self.full and self.head >= self.tail) { - fastmem.copy(T, buf, self.storage[self.tail..self.head]); - return; - } - - const middle = self.storage.len - self.tail; - fastmem.copy(T, buf, self.storage[self.tail..]); - fastmem.copy(T, buf[middle..], self.storage[0..self.head]); + // Then fix up our head and tail. + self.head = self.len() % self.storage.len; + self.tail = 0; } /// Returns if the buffer is currently empty. To check if its @@ -589,7 +576,7 @@ test "CircBuf rotateToZero" { defer buf.deinit(alloc); _ = buf.getPtrSlice(0, 11); - try buf.rotateToZero(alloc); + try buf.rotateToZero(); } test "CircBuf rotateToZero offset" { @@ -611,7 +598,7 @@ test "CircBuf rotateToZero offset" { try testing.expect(buf.tail > 0 and buf.head >= buf.tail); // Rotate to zero - try buf.rotateToZero(alloc); + try buf.rotateToZero(); try testing.expectEqual(@as(usize, 0), buf.tail); try testing.expectEqual(@as(usize, 1), buf.head); } @@ -645,7 +632,7 @@ test "CircBuf rotateToZero wraps" { } // Rotate to zero - try buf.rotateToZero(alloc); + try buf.rotateToZero(); try testing.expectEqual(@as(usize, 0), buf.tail); try testing.expectEqual(@as(usize, 3), buf.head); { @@ -681,7 +668,7 @@ test "CircBuf rotateToZero full no wrap" { } // Rotate to zero - try buf.rotateToZero(alloc); + try buf.rotateToZero(); try testing.expect(buf.full); try testing.expectEqual(@as(usize, 0), buf.tail); try testing.expectEqual(@as(usize, 0), buf.head); From 25a708ed9831083db9555da16137df7d98c38de3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 22:51:14 -0600 Subject: [PATCH 286/642] terminal/style: compare packed styles directly, no cast needed Woohoo, Zig 0.14! --- src/terminal/style.zig | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/terminal/style.zig b/src/terminal/style.zig index 7f176561b..34b07772a 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -87,10 +87,9 @@ pub const Style = struct { /// True if the style is equal to another style. pub fn eql(self: Style, other: Style) bool { - const packed_self = PackedStyle.fromStyle(self); - const packed_other = PackedStyle.fromStyle(other); - // TODO: in Zig 0.14, equating packed structs is allowed. Remove this work around. - return @as(u128, @bitCast(packed_self)) == @as(u128, @bitCast(packed_other)); + // We convert the styles to packed structs and compare as integers + // because this is much faster than comparing each field separately. + return PackedStyle.fromStyle(self) == PackedStyle.fromStyle(other); } /// Returns the bg color for a cell with this style given the cell From 98309e3226dd7589b70bc974cd6ba6efd60954c8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 26 May 2025 10:47:43 -0500 Subject: [PATCH 287/642] nix: update to Nix 25.05 and Zig 0.14.1 Update to Nix 25.05 which gets us GTK 4.18, libadwaita 1.7, and Zig 0.14.1. Since Nix updated to Zig 0.14.1, the devshell has been switched to Zig 0.14.1 from zig-overlay as well. Fixes #7305 --- flake.lock | 54 +++++++++++++++++------------------------------------- flake.nix | 42 +++++++++++++++++------------------------- 2 files changed, 34 insertions(+), 62 deletions(-) diff --git a/flake.lock b/flake.lock index df09a9666..4b8ce405c 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -34,44 +34,24 @@ "type": "github" } }, - "nixpkgs-stable": { + "nixpkgs": { "locked": { - "lastModified": 1741992157, - "narHash": "sha256-nlIfTsTrMSksEJc1f7YexXiPVuzD1gOfeN1ggwZyUoc=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "da4b122f63095ca1199bd4d526f9e26426697689", - "type": "github" + "lastModified": 1748189127, + "narHash": "sha256-zRDR+EbbeObu4V2X5QCd2Bk5eltfDlCr5yvhBwUT6pY=", + "rev": "7c43f080a7f28b2774f3b3f43234ca11661bf334", + "type": "tarball", + "url": "https://releases.nixos.org/nixos/25.05/nixos-25.05.802491.7c43f080a7f2/nixexprs.tar.xz" }, "original": { - "owner": "nixos", - "ref": "release-24.11", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-unstable": { - "locked": { - "lastModified": 1741865919, - "narHash": "sha256-4thdbnP6dlbdq+qZWTsm4ffAwoS8Tiq1YResB+RP6WE=", - "owner": "nixos", - "repo": "nixpkgs", - "rev": "573c650e8a14b2faa0041645ab18aed7e60f0c9a", - "type": "github" - }, - "original": { - "owner": "nixos", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" + "type": "tarball", + "url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz" } }, "root": { "inputs": { "flake-compat": "flake-compat", "flake-utils": "flake-utils", - "nixpkgs-stable": "nixpkgs-stable", - "nixpkgs-unstable": "nixpkgs-unstable", + "nixpkgs": "nixpkgs", "zig": "zig", "zon2nix": "zon2nix" } @@ -98,15 +78,15 @@ "flake-utils" ], "nixpkgs": [ - "nixpkgs-stable" + "nixpkgs" ] }, "locked": { - "lastModified": 1741825901, - "narHash": "sha256-aeopo+aXg5I2IksOPFN79usw7AeimH1+tjfuMzJHFdk=", + "lastModified": 1748261582, + "narHash": "sha256-3i0IL3s18hdDlbsf0/E+5kyPRkZwGPbSFngq5eToiAA=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "0b14285e283f5a747f372fb2931835dd937c4383", + "rev": "aafb1b093fb838f7a02613b719e85ec912914221", "type": "github" }, "original": { @@ -121,7 +101,7 @@ "flake-utils" ], "nixpkgs": [ - "nixpkgs-unstable" + "nixpkgs" ] }, "locked": { diff --git a/flake.nix b/flake.nix index d4c6aa6ca..6794afb11 100644 --- a/flake.nix +++ b/flake.nix @@ -2,12 +2,10 @@ description = "👻"; inputs = { - nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - # We want to stay as up to date as possible but need to be careful that the # glibc versions used by our dependencies from Nix are compatible with the # system glibc that the user is building for. - nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11"; + nixpkgs.url = "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz"; flake-utils.url = "github:numtide/flake-utils"; # Used for shell.nix @@ -19,7 +17,7 @@ zig = { url = "github:mitchellh/zig-overlay"; inputs = { - nixpkgs.follows = "nixpkgs-stable"; + nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; flake-compat.follows = ""; }; @@ -28,7 +26,7 @@ zon2nix = { url = "github:jcollie/zon2nix?ref=56c159be489cc6c0e73c3930bd908ddc6fe89613"; inputs = { - nixpkgs.follows = "nixpkgs-unstable"; + nixpkgs.follows = "nixpkgs"; flake-utils.follows = "flake-utils"; }; }; @@ -36,24 +34,19 @@ outputs = { self, - nixpkgs-unstable, - nixpkgs-stable, + nixpkgs, zig, zon2nix, ... }: - builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} ( + builtins.foldl' nixpkgs.lib.recursiveUpdate {} ( builtins.map ( system: let - pkgs-stable = nixpkgs-stable.legacyPackages.${system}; - pkgs-unstable = nixpkgs-unstable.legacyPackages.${system}; + pkgs = nixpkgs.legacyPackages.${system}; in { - devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix { - zig = zig.packages.${system}."0.14.0"; - wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {}; - uv = pkgs-unstable.uv; - # remove once blueprint-compiler 0.16.0 is in the stable nixpkgs - blueprint-compiler = pkgs-unstable.blueprint-compiler; + devShell.${system} = pkgs.callPackage ./nix/devShell.nix { + zig = zig.packages.${system}."0.14.1"; + wraptest = pkgs.callPackage ./nix/wraptest.nix {}; zon2nix = zon2nix; }; @@ -64,30 +57,29 @@ revision = self.shortRev or self.dirtyShortRev or "dirty"; }; in rec { - deps = pkgs-unstable.callPackage ./build.zig.zon.nix {}; - ghostty-debug = pkgs-unstable.callPackage ./nix/package.nix (mkArgs "Debug"); - ghostty-releasesafe = pkgs-unstable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe"); - ghostty-releasefast = pkgs-unstable.callPackage ./nix/package.nix (mkArgs "ReleaseFast"); + deps = pkgs.callPackage ./build.zig.zon.nix {}; + ghostty-debug = pkgs.callPackage ./nix/package.nix (mkArgs "Debug"); + ghostty-releasesafe = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseSafe"); + ghostty-releasefast = pkgs.callPackage ./nix/package.nix (mkArgs "ReleaseFast"); ghostty = ghostty-releasefast; default = ghostty; }; - formatter.${system} = pkgs-stable.alejandra; + formatter.${system} = pkgs.alejandra; apps.${system} = let runVM = ( module: let vm = import ./nix/vm/create.nix { - inherit system module; - nixpkgs = nixpkgs-unstable; + inherit system module nixpkgs; overlay = self.overlays.debug; }; - program = pkgs-unstable.writeShellScript "run-ghostty-vm" '' + program = pkgs.writeShellScript "run-ghostty-vm" '' SHARED_DIR=$(pwd) export SHARED_DIR - ${pkgs-unstable.lib.getExe vm.config.system.build.vm} "$@" + ${pkgs.lib.getExe vm.config.system.build.vm} "$@" ''; in { type = "app"; From 48b6807ac979324292c8d36aa837b7352da627ba Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 26 May 2025 11:12:30 -0500 Subject: [PATCH 288/642] nix: fix typos --- macos/Sources/Ghostty/Ghostty.App.swift | 4 ++-- typos.toml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 6736449a4..d8fdaa3ec 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -745,7 +745,7 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } guard let mode = FullscreenMode.from(ghostty: raw) else { - Ghostty.logger.warning("unknow fullscreen mode raw=\(raw.rawValue)") + Ghostty.logger.warning("unknown fullscreen mode raw=\(raw.rawValue)") return } NotificationCenter.default.post( @@ -1082,7 +1082,7 @@ extension Ghostty { guard let surface = target.target.surface else { return } guard let surfaceView = self.surfaceView(from: surface) else { return } guard let window = surfaceView.window as? TerminalWindow else { return } - + switch (mode) { case .on: window.level = .floating diff --git a/typos.toml b/typos.toml index 4f4bf7ee7..fafc38858 100644 --- a/typos.toml +++ b/typos.toml @@ -49,6 +49,8 @@ grey = "gray" greyscale = "grayscale" DECID = "DECID" flate = "flate" +typ = "typ" +kend = "kend" [type.po] extend-glob = ["*.po"] From 695e0b3e5780919a6549c827bb282c1a8d516253 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 26 May 2025 11:43:52 -0500 Subject: [PATCH 289/642] nix: temporarily remove snapcraft from the devshell --- nix/devShell.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/devShell.nix b/nix/devShell.nix index b87c23dd1..f4ea62235 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -16,7 +16,7 @@ python3, qemu, scdoc, - snapcraft, + # snapcraft, valgrind, #, vulkan-loader # unused vttest, @@ -134,7 +134,7 @@ in appstream flatpak-builder gdb - snapcraft + # snapcraft valgrind wraptest From 2905b4727980b8d1109f333e0d78db7674eb0e48 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 19:39:39 -0600 Subject: [PATCH 290/642] font: use labeled switch continue pattern for feature string parser In this case it does result in a little repeated code for reading bytes, but I find the control flow easier to follow, so it's worth it IMO. --- src/font/shaper/feature.zig | 300 ++++++++++++++++-------------------- 1 file changed, 133 insertions(+), 167 deletions(-) diff --git a/src/font/shaper/feature.zig b/src/font/shaper/feature.zig index 8e70d51da..c2d49234d 100644 --- a/src/font/shaper/feature.zig +++ b/src/font/shaper/feature.zig @@ -35,190 +35,156 @@ pub const Feature = struct { /// /// Ref: https://harfbuzz.github.io/harfbuzz-hb-common.html#hb-feature-from-string pub fn fromReader(reader: anytype) ?Feature { - var tag: [4]u8 = undefined; + var tag_buf: [4]u8 = undefined; + var tag: []u8 = tag_buf[0..0]; var value: ?u32 = null; - // TODO: when we move to Zig 0.14 this can be replaced with a - // labeled switch continue pattern rather than this loop. - var state: union(enum) { + state: switch ((enum { /// Initial state. - start: void, - /// Parsing the tag, data is index. - tag: u2, + start, + /// Parsing the tag. + tag, /// In the space between the tag and the value. - space: void, + space, /// Parsing an integer parameter directly in to `value`. - int: void, + int, /// Parsing a boolean keyword parameter ("on"/"off"). - bool: void, + bool, /// Encountered an unrecoverable syntax error, advancing to boundary. - err: void, - /// Done parsing feature. - done: void, - } = .start; - while (true) { - // If we hit the end of the stream we just pretend it's a comma. - const byte = reader.readByte() catch ','; - switch (state) { - // If we're done then we skip whitespace until we see a ','. - .done => switch (byte) { - ' ', '\t' => continue, - ',' => break, - // If we see something other than whitespace or a ',' - // then this is an error since the intent is unclear. - else => { - state = .err; - continue; - }, + err, + /// Done parsing feature, skip whitespace until end. + done, + }).start) { + // If we're done then we skip whitespace until we see a ','. + .done => while (true) switch (reader.readByte() catch ',') { + ' ', '\t' => continue, + ',' => break, + // If we see something other than whitespace or a ',' + // then this is an error since the intent is unclear. + else => continue :state .err, + }, + + // If we're fast-forwarding from an error we just wanna + // stop at the first boundary and ignore all other bytes. + .err => { + reader.skipUntilDelimiterOrEof(',') catch {}; + return null; + }, + + .start => while (true) switch (reader.readByte() catch ',') { + // Ignore leading whitespace. + ' ', '\t' => continue, + // Empty feature string. + ',' => return null, + // '+' prefix to explicitly enable feature. + '+' => { + value = 1; + continue :state .tag; }, + // '-' prefix to explicitly disable feature. + '-' => { + value = 0; + continue :state .tag; + }, + // Quote mark introducing a tag. + '"', '\'' => { + continue :state .tag; + }, + // First letter of tag. + else => |byte| { + tag.len = 1; + tag[0] = byte; + continue :state .tag; + }, + }, - // If we're fast-forwarding from an error we just wanna - // stop at the first boundary and ignore all other bytes. - .err => if (byte == ',') return null, + .tag => while (true) switch (reader.readByte() catch ',') { + // If the tag is interrupted by a comma it's invalid. + ',' => return null, + // Ignore quote marks. This does technically ignore cases like + // "'k'e'r'n' = 0", but it's unambiguous so if someone really + // wants to do that in their config then... sure why not. + '"', '\'' => continue, + // In all other cases we add the byte to our tag. + else => |byte| { + tag.len += 1; + tag[tag.len - 1] = byte; + if (tag.len == 4) continue :state .space; + }, + }, - .start => switch (byte) { - // Ignore leading whitespace. - ' ', '\t' => continue, - // Empty feature string. - ',' => return null, - // '+' prefix to explicitly enable feature. - '+' => { - value = 1; - state = .{ .tag = 0 }; - continue; - }, - // '-' prefix to explicitly disable feature. - '-' => { + .space => while (true) switch (reader.readByte() catch ',') { + ' ', '\t' => continue, + // Ignore quote marks since we might have a + // closing quote from the tag still ahead. + '"', '\'' => continue, + // Allow an '=' (which we can safely ignore) + // only if we don't already have a value due + // to a '+' or '-' prefix. + '=' => if (value != null) continue :state .err, + ',' => { + // Specifying only a tag turns a feature on. + if (value == null) value = 1; + break; + }, + '0'...'9' => |byte| { + // If we already have value because of a + // '+' or '-' prefix then this is an error. + if (value != null) continue :state .err; + value = byte - '0'; + continue :state .int; + }, + 'o', 'O' => { + // If we already have value because of a + // '+' or '-' prefix then this is an error. + if (value != null) continue :state .err; + continue :state .bool; + }, + else => continue :state .err, + }, + + .int => while (true) switch (reader.readByte() catch ',') { + ',' => break, + '0'...'9' => |byte| { + // If our value gets too big while + // parsing we consider it an error. + value = std.math.mul(u32, value.?, 10) catch { + continue :state .err; + }; + value.? += byte - '0'; + }, + else => continue :state .err, + }, + + .bool => while (true) switch (reader.readByte() catch ',') { + ',' => return null, + 'n', 'N' => { + // "ofn" + if (value != null) { + assert(value == 0); + continue :state .err; + } + value = 1; + continue :state .done; + }, + 'f', 'F' => { + // To make sure we consume two 'f's. + if (value == null) { value = 0; - state = .{ .tag = 0 }; - continue; - }, - // Quote mark introducing a tag. - '"', '\'' => { - state = .{ .tag = 0 }; - continue; - }, - // First letter of tag. - else => { - tag[0] = byte; - state = .{ .tag = 1 }; - continue; - }, + } else { + assert(value == 0); + continue :state .done; + } }, - - .tag => |*i| switch (byte) { - // If the tag is interrupted by a comma it's invalid. - ',' => return null, - // Ignore quote marks. - '"', '\'' => continue, - // A prefix of '+' or '-' - // In all other cases we add the byte to our tag. - else => { - tag[i.*] = byte; - if (i.* == 3) { - state = .space; - continue; - } - i.* += 1; - }, - }, - - .space => switch (byte) { - ' ', '\t' => continue, - // Ignore quote marks since we might have a - // closing quote from the tag still ahead. - '"', '\'' => continue, - // Allow an '=' (which we can safely ignore) - // only if we don't already have a value due - // to a '+' or '-' prefix. - '=' => if (value != null) { - state = .err; - continue; - }, - ',' => { - // Specifying only a tag turns a feature on. - if (value == null) value = 1; - break; - }, - '0'...'9' => { - // If we already have value because of a - // '+' or '-' prefix then this is an error. - if (value != null) { - state = .err; - continue; - } - value = byte - '0'; - state = .int; - continue; - }, - 'o', 'O' => { - // If we already have value because of a - // '+' or '-' prefix then this is an error. - if (value != null) { - state = .err; - continue; - } - state = .bool; - continue; - }, - else => { - state = .err; - continue; - }, - }, - - .int => switch (byte) { - ',' => break, - '0'...'9' => { - // If our value gets too big while - // parsing we consider it an error. - value = std.math.mul(u32, value.?, 10) catch { - state = .err; - continue; - }; - value.? += byte - '0'; - }, - else => { - state = .err; - continue; - }, - }, - - .bool => switch (byte) { - ',' => return null, - 'n', 'N' => { - // "ofn" - if (value != null) { - assert(value == 0); - state = .err; - continue; - } - value = 1; - state = .done; - continue; - }, - 'f', 'F' => { - // To make sure we consume two 'f's. - if (value == null) { - value = 0; - } else { - assert(value == 0); - state = .done; - continue; - } - }, - else => { - state = .err; - continue; - }, - }, - } + else => continue :state .err, + }, } assert(value != null); + assert(tag.len == 4); return .{ - .tag = tag, + .tag = tag_buf, .value = value.?, }; } From 2fe2ccdbde58557f17ba1787551dfb0666b6a147 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 19:56:35 -0600 Subject: [PATCH 291/642] font/sprite: use decl literals in box drawing code Cleaner and less visual noise, easy change to make, there are many other areas in the code which would benefit from decl literals as well, but this is an area that benefits a lot from them and is self-contained. --- src/font/sprite/Box.zig | 310 ++++++++++++++++++++-------------------- 1 file changed, 155 insertions(+), 155 deletions(-) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index 68acdabe5..b1ebfe3a9 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -516,40 +516,40 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void 0x257f => self.draw_lines(canvas, .{ .up = .heavy, .down = .light }), // '▀' UPPER HALF BLOCK - 0x2580 => self.draw_block(canvas, Alignment.upper, 1, half), + 0x2580 => self.draw_block(canvas, .upper, 1, half), // '▁' LOWER ONE EIGHTH BLOCK - 0x2581 => self.draw_block(canvas, Alignment.lower, 1, one_eighth), + 0x2581 => self.draw_block(canvas, .lower, 1, one_eighth), // '▂' LOWER ONE QUARTER BLOCK - 0x2582 => self.draw_block(canvas, Alignment.lower, 1, one_quarter), + 0x2582 => self.draw_block(canvas, .lower, 1, one_quarter), // '▃' LOWER THREE EIGHTHS BLOCK - 0x2583 => self.draw_block(canvas, Alignment.lower, 1, three_eighths), + 0x2583 => self.draw_block(canvas, .lower, 1, three_eighths), // '▄' LOWER HALF BLOCK - 0x2584 => self.draw_block(canvas, Alignment.lower, 1, half), + 0x2584 => self.draw_block(canvas, .lower, 1, half), // '▅' LOWER FIVE EIGHTHS BLOCK - 0x2585 => self.draw_block(canvas, Alignment.lower, 1, five_eighths), + 0x2585 => self.draw_block(canvas, .lower, 1, five_eighths), // '▆' LOWER THREE QUARTERS BLOCK - 0x2586 => self.draw_block(canvas, Alignment.lower, 1, three_quarters), + 0x2586 => self.draw_block(canvas, .lower, 1, three_quarters), // '▇' LOWER SEVEN EIGHTHS BLOCK - 0x2587 => self.draw_block(canvas, Alignment.lower, 1, seven_eighths), + 0x2587 => self.draw_block(canvas, .lower, 1, seven_eighths), // '█' FULL BLOCK 0x2588 => self.draw_full_block(canvas), // '▉' LEFT SEVEN EIGHTHS BLOCK - 0x2589 => self.draw_block(canvas, Alignment.left, seven_eighths, 1), + 0x2589 => self.draw_block(canvas, .left, seven_eighths, 1), // '▊' LEFT THREE QUARTERS BLOCK - 0x258a => self.draw_block(canvas, Alignment.left, three_quarters, 1), + 0x258a => self.draw_block(canvas, .left, three_quarters, 1), // '▋' LEFT FIVE EIGHTHS BLOCK - 0x258b => self.draw_block(canvas, Alignment.left, five_eighths, 1), + 0x258b => self.draw_block(canvas, .left, five_eighths, 1), // '▌' LEFT HALF BLOCK - 0x258c => self.draw_block(canvas, Alignment.left, half, 1), + 0x258c => self.draw_block(canvas, .left, half, 1), // '▍' LEFT THREE EIGHTHS BLOCK - 0x258d => self.draw_block(canvas, Alignment.left, three_eighths, 1), + 0x258d => self.draw_block(canvas, .left, three_eighths, 1), // '▎' LEFT ONE QUARTER BLOCK - 0x258e => self.draw_block(canvas, Alignment.left, one_quarter, 1), + 0x258e => self.draw_block(canvas, .left, one_quarter, 1), // '▏' LEFT ONE EIGHTH BLOCK - 0x258f => self.draw_block(canvas, Alignment.left, one_eighth, 1), + 0x258f => self.draw_block(canvas, .left, one_eighth, 1), // '▐' RIGHT HALF BLOCK - 0x2590 => self.draw_block(canvas, Alignment.right, half, 1), + 0x2590 => self.draw_block(canvas, .right, half, 1), // '░' 0x2591 => self.draw_light_shade(canvas), // '▒' @@ -557,9 +557,9 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void // '▓' 0x2593 => self.draw_dark_shade(canvas), // '▔' UPPER ONE EIGHTH BLOCK - 0x2594 => self.draw_block(canvas, Alignment.upper, 1, one_eighth), + 0x2594 => self.draw_block(canvas, .upper, 1, one_eighth), // '▕' RIGHT ONE EIGHTH BLOCK - 0x2595 => self.draw_block(canvas, Alignment.right, one_eighth, 1), + 0x2595 => self.draw_block(canvas, .right, one_eighth, 1), // '▖' 0x2596 => self.draw_quadrant(canvas, .{ .bl = true }), // '▗' @@ -588,35 +588,35 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void octant_min...octant_max => self.draw_octant(canvas, cp), // '🬼' - 0x1fb3c => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb3c => try self.draw_smooth_mosaic(canvas, .from( \\... \\... \\#.. \\##. )), // '🬽' - 0x1fb3d => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb3d => try self.draw_smooth_mosaic(canvas, .from( \\... \\... \\#\. \\### )), // '🬾' - 0x1fb3e => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb3e => try self.draw_smooth_mosaic(canvas, .from( \\... \\#.. \\#\. \\##. )), // '🬿' - 0x1fb3f => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb3f => try self.draw_smooth_mosaic(canvas, .from( \\... \\#.. \\##. \\### )), // '🭀' - 0x1fb40 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb40 => try self.draw_smooth_mosaic(canvas, .from( \\#.. \\#.. \\##. @@ -624,42 +624,42 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭁' - 0x1fb41 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb41 => try self.draw_smooth_mosaic(canvas, .from( \\/## \\### \\### \\### )), // '🭂' - 0x1fb42 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb42 => try self.draw_smooth_mosaic(canvas, .from( \\./# \\### \\### \\### )), // '🭃' - 0x1fb43 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb43 => try self.draw_smooth_mosaic(canvas, .from( \\.## \\.## \\### \\### )), // '🭄' - 0x1fb44 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb44 => try self.draw_smooth_mosaic(canvas, .from( \\..# \\.## \\### \\### )), // '🭅' - 0x1fb45 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb45 => try self.draw_smooth_mosaic(canvas, .from( \\.## \\.## \\.## \\### )), // '🭆' - 0x1fb46 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb46 => try self.draw_smooth_mosaic(canvas, .from( \\... \\./# \\### @@ -667,35 +667,35 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭇' - 0x1fb47 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb47 => try self.draw_smooth_mosaic(canvas, .from( \\... \\... \\..# \\.## )), // '🭈' - 0x1fb48 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb48 => try self.draw_smooth_mosaic(canvas, .from( \\... \\... \\./# \\### )), // '🭉' - 0x1fb49 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb49 => try self.draw_smooth_mosaic(canvas, .from( \\... \\..# \\./# \\.## )), // '🭊' - 0x1fb4a => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb4a => try self.draw_smooth_mosaic(canvas, .from( \\... \\..# \\.## \\### )), // '🭋' - 0x1fb4b => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb4b => try self.draw_smooth_mosaic(canvas, .from( \\..# \\..# \\.## @@ -703,42 +703,42 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭌' - 0x1fb4c => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb4c => try self.draw_smooth_mosaic(canvas, .from( \\##\ \\### \\### \\### )), // '🭍' - 0x1fb4d => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb4d => try self.draw_smooth_mosaic(canvas, .from( \\#\. \\### \\### \\### )), // '🭎' - 0x1fb4e => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb4e => try self.draw_smooth_mosaic(canvas, .from( \\##. \\##. \\### \\### )), // '🭏' - 0x1fb4f => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb4f => try self.draw_smooth_mosaic(canvas, .from( \\#.. \\##. \\### \\### )), // '🭐' - 0x1fb50 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb50 => try self.draw_smooth_mosaic(canvas, .from( \\##. \\##. \\##. \\### )), // '🭑' - 0x1fb51 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb51 => try self.draw_smooth_mosaic(canvas, .from( \\... \\#\. \\### @@ -746,35 +746,35 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭒' - 0x1fb52 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb52 => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\### \\\## )), // '🭓' - 0x1fb53 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb53 => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\### \\.\# )), // '🭔' - 0x1fb54 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb54 => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\.## \\.## )), // '🭕' - 0x1fb55 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb55 => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\.## \\..# )), // '🭖' - 0x1fb56 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb56 => try self.draw_smooth_mosaic(canvas, .from( \\### \\.## \\.## @@ -782,35 +782,35 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭗' - 0x1fb57 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb57 => try self.draw_smooth_mosaic(canvas, .from( \\##. \\#.. \\... \\... )), // '🭘' - 0x1fb58 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb58 => try self.draw_smooth_mosaic(canvas, .from( \\### \\#/. \\... \\... )), // '🭙' - 0x1fb59 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb59 => try self.draw_smooth_mosaic(canvas, .from( \\##. \\#/. \\#.. \\... )), // '🭚' - 0x1fb5a => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb5a => try self.draw_smooth_mosaic(canvas, .from( \\### \\##. \\#.. \\... )), // '🭛' - 0x1fb5b => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb5b => try self.draw_smooth_mosaic(canvas, .from( \\##. \\##. \\#.. @@ -818,42 +818,42 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭜' - 0x1fb5c => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb5c => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\#/. \\... )), // '🭝' - 0x1fb5d => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb5d => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\### \\##/ )), // '🭞' - 0x1fb5e => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb5e => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\### \\#/. )), // '🭟' - 0x1fb5f => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb5f => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\##. \\##. )), // '🭠' - 0x1fb60 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb60 => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\##. \\#.. )), // '🭡' - 0x1fb61 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb61 => try self.draw_smooth_mosaic(canvas, .from( \\### \\##. \\##. @@ -861,42 +861,42 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void )), // '🭢' - 0x1fb62 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb62 => try self.draw_smooth_mosaic(canvas, .from( \\.## \\..# \\... \\... )), // '🭣' - 0x1fb63 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb63 => try self.draw_smooth_mosaic(canvas, .from( \\### \\.\# \\... \\... )), // '🭤' - 0x1fb64 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb64 => try self.draw_smooth_mosaic(canvas, .from( \\.## \\.\# \\..# \\... )), // '🭥' - 0x1fb65 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb65 => try self.draw_smooth_mosaic(canvas, .from( \\### \\.## \\..# \\... )), // '🭦' - 0x1fb66 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb66 => try self.draw_smooth_mosaic(canvas, .from( \\.## \\.## \\..# \\..# )), // '🭧' - 0x1fb67 => try self.draw_smooth_mosaic(canvas, SmoothMosaic.from( + 0x1fb67 => try self.draw_smooth_mosaic(canvas, .from( \\### \\### \\.\# @@ -959,79 +959,79 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void 0x1fb7b => self.draw_horizontal_one_eighth_block_n(canvas, 6), // '🮂' UPPER ONE QUARTER BLOCK - 0x1fb82 => self.draw_block(canvas, Alignment.upper, 1, one_quarter), + 0x1fb82 => self.draw_block(canvas, .upper, 1, one_quarter), // '🮃' UPPER THREE EIGHTHS BLOCK - 0x1fb83 => self.draw_block(canvas, Alignment.upper, 1, three_eighths), + 0x1fb83 => self.draw_block(canvas, .upper, 1, three_eighths), // '🮄' UPPER FIVE EIGHTHS BLOCK - 0x1fb84 => self.draw_block(canvas, Alignment.upper, 1, five_eighths), + 0x1fb84 => self.draw_block(canvas, .upper, 1, five_eighths), // '🮅' UPPER THREE QUARTERS BLOCK - 0x1fb85 => self.draw_block(canvas, Alignment.upper, 1, three_quarters), + 0x1fb85 => self.draw_block(canvas, .upper, 1, three_quarters), // '🮆' UPPER SEVEN EIGHTHS BLOCK - 0x1fb86 => self.draw_block(canvas, Alignment.upper, 1, seven_eighths), + 0x1fb86 => self.draw_block(canvas, .upper, 1, seven_eighths), // '🭼' LEFT AND LOWER ONE EIGHTH BLOCK 0x1fb7c => { - self.draw_block(canvas, Alignment.left, one_eighth, 1); - self.draw_block(canvas, Alignment.lower, 1, one_eighth); + self.draw_block(canvas, .left, one_eighth, 1); + self.draw_block(canvas, .lower, 1, one_eighth); }, // '🭽' LEFT AND UPPER ONE EIGHTH BLOCK 0x1fb7d => { - self.draw_block(canvas, Alignment.left, one_eighth, 1); - self.draw_block(canvas, Alignment.upper, 1, one_eighth); + self.draw_block(canvas, .left, one_eighth, 1); + self.draw_block(canvas, .upper, 1, one_eighth); }, // '🭾' RIGHT AND UPPER ONE EIGHTH BLOCK 0x1fb7e => { - self.draw_block(canvas, Alignment.right, one_eighth, 1); - self.draw_block(canvas, Alignment.upper, 1, one_eighth); + self.draw_block(canvas, .right, one_eighth, 1); + self.draw_block(canvas, .upper, 1, one_eighth); }, // '🭿' RIGHT AND LOWER ONE EIGHTH BLOCK 0x1fb7f => { - self.draw_block(canvas, Alignment.right, one_eighth, 1); - self.draw_block(canvas, Alignment.lower, 1, one_eighth); + self.draw_block(canvas, .right, one_eighth, 1); + self.draw_block(canvas, .lower, 1, one_eighth); }, // '🮀' UPPER AND LOWER ONE EIGHTH BLOCK 0x1fb80 => { - self.draw_block(canvas, Alignment.upper, 1, one_eighth); - self.draw_block(canvas, Alignment.lower, 1, one_eighth); + self.draw_block(canvas, .upper, 1, one_eighth); + self.draw_block(canvas, .lower, 1, one_eighth); }, // '🮁' 0x1fb81 => self.draw_horizontal_one_eighth_1358_block(canvas), // '🮇' RIGHT ONE QUARTER BLOCK - 0x1fb87 => self.draw_block(canvas, Alignment.right, one_quarter, 1), + 0x1fb87 => self.draw_block(canvas, .right, one_quarter, 1), // '🮈' RIGHT THREE EIGHTHS BLOCK - 0x1fb88 => self.draw_block(canvas, Alignment.right, three_eighths, 1), + 0x1fb88 => self.draw_block(canvas, .right, three_eighths, 1), // '🮉' RIGHT FIVE EIGHTHS BLOCK - 0x1fb89 => self.draw_block(canvas, Alignment.right, five_eighths, 1), + 0x1fb89 => self.draw_block(canvas, .right, five_eighths, 1), // '🮊' RIGHT THREE QUARTERS BLOCK - 0x1fb8a => self.draw_block(canvas, Alignment.right, three_quarters, 1), + 0x1fb8a => self.draw_block(canvas, .right, three_quarters, 1), // '🮋' RIGHT SEVEN EIGHTHS BLOCK - 0x1fb8b => self.draw_block(canvas, Alignment.right, seven_eighths, 1), + 0x1fb8b => self.draw_block(canvas, .right, seven_eighths, 1), // '🮌' - 0x1fb8c => self.draw_block_shade(canvas, Alignment.left, half, 1, .medium), + 0x1fb8c => self.draw_block_shade(canvas, .left, half, 1, .medium), // '🮍' - 0x1fb8d => self.draw_block_shade(canvas, Alignment.right, half, 1, .medium), + 0x1fb8d => self.draw_block_shade(canvas, .right, half, 1, .medium), // '🮎' - 0x1fb8e => self.draw_block_shade(canvas, Alignment.upper, 1, half, .medium), + 0x1fb8e => self.draw_block_shade(canvas, .upper, 1, half, .medium), // '🮏' - 0x1fb8f => self.draw_block_shade(canvas, Alignment.lower, 1, half, .medium), + 0x1fb8f => self.draw_block_shade(canvas, .lower, 1, half, .medium), // '🮐' 0x1fb90 => self.draw_medium_shade(canvas), // '🮑' 0x1fb91 => { self.draw_medium_shade(canvas); - self.draw_block(canvas, Alignment.upper, 1, half); + self.draw_block(canvas, .upper, 1, half); }, // '🮒' 0x1fb92 => { self.draw_medium_shade(canvas); - self.draw_block(canvas, Alignment.lower, 1, half); + self.draw_block(canvas, .lower, 1, half); }, // '🮔' 0x1fb94 => { self.draw_medium_shade(canvas); - self.draw_block(canvas, Alignment.right, half, 1); + self.draw_block(canvas, .right, half, 1); }, // '🮕' 0x1fb95 => self.draw_checkerboard_fill(canvas, 0), @@ -1117,194 +1117,194 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void }, // '🯎' - 0x1fbce => self.draw_block(canvas, Alignment.left, two_thirds, 1), + 0x1fbce => self.draw_block(canvas, .left, two_thirds, 1), // '🯏' - 0x1fbcf => self.draw_block(canvas, Alignment.left, one_third, 1), + 0x1fbcf => self.draw_block(canvas, .left, one_third, 1), // '🯐' 0x1fbd0 => self.draw_cell_diagonal( canvas, - Alignment.middle_right, - Alignment.lower_left, + .middle_right, + .lower_left, ), // '🯑' 0x1fbd1 => self.draw_cell_diagonal( canvas, - Alignment.upper_right, - Alignment.middle_left, + .upper_right, + .middle_left, ), // '🯒' 0x1fbd2 => self.draw_cell_diagonal( canvas, - Alignment.upper_left, - Alignment.middle_right, + .upper_left, + .middle_right, ), // '🯓' 0x1fbd3 => self.draw_cell_diagonal( canvas, - Alignment.middle_left, - Alignment.lower_right, + .middle_left, + .lower_right, ), // '🯔' 0x1fbd4 => self.draw_cell_diagonal( canvas, - Alignment.upper_left, - Alignment.lower_center, + .upper_left, + .lower_center, ), // '🯕' 0x1fbd5 => self.draw_cell_diagonal( canvas, - Alignment.upper_center, - Alignment.lower_right, + .upper_center, + .lower_right, ), // '🯖' 0x1fbd6 => self.draw_cell_diagonal( canvas, - Alignment.upper_right, - Alignment.lower_center, + .upper_right, + .lower_center, ), // '🯗' 0x1fbd7 => self.draw_cell_diagonal( canvas, - Alignment.upper_center, - Alignment.lower_left, + .upper_center, + .lower_left, ), // '🯘' 0x1fbd8 => { self.draw_cell_diagonal( canvas, - Alignment.upper_left, - Alignment.middle_center, + .upper_left, + .middle_center, ); self.draw_cell_diagonal( canvas, - Alignment.middle_center, - Alignment.upper_right, + .middle_center, + .upper_right, ); }, // '🯙' 0x1fbd9 => { self.draw_cell_diagonal( canvas, - Alignment.upper_right, - Alignment.middle_center, + .upper_right, + .middle_center, ); self.draw_cell_diagonal( canvas, - Alignment.middle_center, - Alignment.lower_right, + .middle_center, + .lower_right, ); }, // '🯚' 0x1fbda => { self.draw_cell_diagonal( canvas, - Alignment.lower_left, - Alignment.middle_center, + .lower_left, + .middle_center, ); self.draw_cell_diagonal( canvas, - Alignment.middle_center, - Alignment.lower_right, + .middle_center, + .lower_right, ); }, // '🯛' 0x1fbdb => { self.draw_cell_diagonal( canvas, - Alignment.upper_left, - Alignment.middle_center, + .upper_left, + .middle_center, ); self.draw_cell_diagonal( canvas, - Alignment.middle_center, - Alignment.lower_left, + .middle_center, + .lower_left, ); }, // '🯜' 0x1fbdc => { self.draw_cell_diagonal( canvas, - Alignment.upper_left, - Alignment.lower_center, + .upper_left, + .lower_center, ); self.draw_cell_diagonal( canvas, - Alignment.lower_center, - Alignment.upper_right, + .lower_center, + .upper_right, ); }, // '🯝' 0x1fbdd => { self.draw_cell_diagonal( canvas, - Alignment.upper_right, - Alignment.middle_left, + .upper_right, + .middle_left, ); self.draw_cell_diagonal( canvas, - Alignment.middle_left, - Alignment.lower_right, + .middle_left, + .lower_right, ); }, // '🯞' 0x1fbde => { self.draw_cell_diagonal( canvas, - Alignment.lower_left, - Alignment.upper_center, + .lower_left, + .upper_center, ); self.draw_cell_diagonal( canvas, - Alignment.upper_center, - Alignment.lower_right, + .upper_center, + .lower_right, ); }, // '🯟' 0x1fbdf => { self.draw_cell_diagonal( canvas, - Alignment.upper_left, - Alignment.middle_right, + .upper_left, + .middle_right, ); self.draw_cell_diagonal( canvas, - Alignment.middle_right, - Alignment.lower_left, + .middle_right, + .lower_left, ); }, // '🯠' - 0x1fbe0 => self.draw_circle(canvas, Alignment.top, false), + 0x1fbe0 => self.draw_circle(canvas, .top, false), // '🯡' - 0x1fbe1 => self.draw_circle(canvas, Alignment.right, false), + 0x1fbe1 => self.draw_circle(canvas, .right, false), // '🯢' - 0x1fbe2 => self.draw_circle(canvas, Alignment.bottom, false), + 0x1fbe2 => self.draw_circle(canvas, .bottom, false), // '🯣' - 0x1fbe3 => self.draw_circle(canvas, Alignment.left, false), + 0x1fbe3 => self.draw_circle(canvas, .left, false), // '🯤' - 0x1fbe4 => self.draw_block(canvas, Alignment.upper_center, 0.5, 0.5), + 0x1fbe4 => self.draw_block(canvas, .upper_center, 0.5, 0.5), // '🯥' - 0x1fbe5 => self.draw_block(canvas, Alignment.lower_center, 0.5, 0.5), + 0x1fbe5 => self.draw_block(canvas, .lower_center, 0.5, 0.5), // '🯦' - 0x1fbe6 => self.draw_block(canvas, Alignment.middle_left, 0.5, 0.5), + 0x1fbe6 => self.draw_block(canvas, .middle_left, 0.5, 0.5), // '🯧' - 0x1fbe7 => self.draw_block(canvas, Alignment.middle_right, 0.5, 0.5), + 0x1fbe7 => self.draw_block(canvas, .middle_right, 0.5, 0.5), // '🯨' - 0x1fbe8 => self.draw_circle(canvas, Alignment.top, true), + 0x1fbe8 => self.draw_circle(canvas, .top, true), // '🯩' - 0x1fbe9 => self.draw_circle(canvas, Alignment.right, true), + 0x1fbe9 => self.draw_circle(canvas, .right, true), // '🯪' - 0x1fbea => self.draw_circle(canvas, Alignment.bottom, true), + 0x1fbea => self.draw_circle(canvas, .bottom, true), // '🯫' - 0x1fbeb => self.draw_circle(canvas, Alignment.left, true), + 0x1fbeb => self.draw_circle(canvas, .left, true), // '🯬' - 0x1fbec => self.draw_circle(canvas, Alignment.top_right, true), + 0x1fbec => self.draw_circle(canvas, .top_right, true), // '🯭' - 0x1fbed => self.draw_circle(canvas, Alignment.bottom_left, true), + 0x1fbed => self.draw_circle(canvas, .bottom_left, true), // '🯮' - 0x1fbee => self.draw_circle(canvas, Alignment.bottom_right, true), + 0x1fbee => self.draw_circle(canvas, .bottom_right, true), // '🯯' - 0x1fbef => self.draw_circle(canvas, Alignment.top_left, true), + 0x1fbef => self.draw_circle(canvas, .top_left, true), // (Below:) // Branch drawing character set, used for drawing git-like From 2384bd69cc25db7228dcb2e90ea1d296bbf0ba84 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 21:39:15 -0600 Subject: [PATCH 292/642] style: use decl literals This commit changes a LOT of areas of the code to use decl literals instead of redundantly referring to the type. These changes were mostly driven by some regex searches and then manual adjustment on a case-by-case basis. I almost certainly missed quite a few places where decl literals could be used, but this is a good first step in converting things, and other instances can be addressed when they're discovered. I tested GLFW+Metal and building the framework on macOS and tested a GTK build on Linux, so I'm 99% sure I didn't introduce any syntax errors or other problems with this. (fingers crossed) --- pkg/fontconfig/pattern.zig | 4 +- pkg/glfw/Monitor.zig | 2 +- pkg/glfw/opengl.zig | 2 +- src/Command.zig | 2 +- src/Surface.zig | 2 +- src/apprt/embedded.zig | 12 +++--- src/apprt/gtk/ClipboardConfirmationWindow.zig | 14 +++---- src/apprt/gtk/ConfigErrorsDialog.zig | 6 +-- src/apprt/gtk/ResizeOverlay.zig | 4 +- src/apprt/gtk/Split.zig | 2 +- src/apprt/gtk/Surface.zig | 2 +- src/apprt/gtk/Window.zig | 8 ++-- src/apprt/gtk/inspector.zig | 2 +- src/apprt/gtk/winproto/x11.zig | 2 +- src/build/SharedDeps.zig | 8 ++-- src/cli/args.zig | 4 +- src/config/Config.zig | 8 ++-- src/config/formatter.zig | 2 +- src/crash/sentry_envelope.zig | 2 +- src/font/CodepointResolver.zig | 16 +++---- src/font/Collection.zig | 18 ++++---- src/font/DeferredFace.zig | 2 +- src/font/SharedGrid.zig | 2 +- src/font/SharedGridSet.zig | 16 +++---- src/font/face/coretext.zig | 2 +- src/font/face/freetype_convert.zig | 2 +- src/font/shaper/coretext.zig | 10 ++--- src/font/shaper/feature.zig | 2 +- src/font/shaper/harfbuzz.zig | 8 ++-- src/font/sprite/canvas.zig | 2 +- src/global.zig | 2 +- src/input/Binding.zig | 4 +- src/input/KeyEncoder.zig | 2 +- src/inspector/termio.zig | 2 +- src/os/args.zig | 2 +- src/renderer/Thread.zig | 2 +- src/renderer/cursor.zig | 2 +- src/renderer/link.zig | 2 +- src/renderer/metal/cell.zig | 2 +- src/renderer/size.zig | 4 +- src/terminal/PageList.zig | 12 +++--- src/terminal/Parser.zig | 4 +- src/terminal/Screen.zig | 12 +++--- src/terminal/Selection.zig | 8 ++-- src/terminal/StringMap.zig | 2 +- src/terminal/Terminal.zig | 10 ++--- src/terminal/bitmap_allocator.zig | 8 ++-- src/terminal/hash_map.zig | 42 +++++++++---------- src/terminal/kitty/graphics_command.zig | 18 ++++---- src/terminal/kitty/graphics_exec.zig | 2 +- src/terminal/osc.zig | 4 +- src/terminal/page.zig | 20 ++++----- src/terminal/search.zig | 4 +- src/terminal/sgr.zig | 4 +- src/terminal/style.zig | 8 ++-- src/terminal/x11_color.zig | 2 +- src/unicode/props.zig | 2 +- 57 files changed, 177 insertions(+), 177 deletions(-) diff --git a/pkg/fontconfig/pattern.zig b/pkg/fontconfig/pattern.zig index e0ec27a69..3a623e223 100644 --- a/pkg/fontconfig/pattern.zig +++ b/pkg/fontconfig/pattern.zig @@ -44,7 +44,7 @@ pub const Pattern = opaque { &val, ))).toError(); - return Value.init(&val); + return .init(&val); } pub fn delete(self: *Pattern, prop: Property) bool { @@ -138,7 +138,7 @@ pub const Pattern = opaque { return Entry{ .result = @enumFromInt(result), .binding = @enumFromInt(binding), - .value = Value.init(&value), + .value = .init(&value), }; } }; diff --git a/pkg/glfw/Monitor.zig b/pkg/glfw/Monitor.zig index 4accb23cd..3b194965a 100644 --- a/pkg/glfw/Monitor.zig +++ b/pkg/glfw/Monitor.zig @@ -281,7 +281,7 @@ pub inline fn setGamma(self: Monitor, gamma: f32) void { /// see also: monitor_gamma pub inline fn getGammaRamp(self: Monitor) ?GammaRamp { internal_debug.assertInitialized(); - if (c.glfwGetGammaRamp(self.handle)) |ramp| return GammaRamp.fromC(ramp.*); + if (c.glfwGetGammaRamp(self.handle)) |ramp| return .fromC(ramp.*); return null; } diff --git a/pkg/glfw/opengl.zig b/pkg/glfw/opengl.zig index 04bc3a65c..8fe2efbed 100644 --- a/pkg/glfw/opengl.zig +++ b/pkg/glfw/opengl.zig @@ -47,7 +47,7 @@ pub inline fn makeContextCurrent(window: ?Window) void { /// see also: context_current, glfwMakeContextCurrent pub inline fn getCurrentContext() ?Window { internal_debug.assertInitialized(); - if (c.glfwGetCurrentContext()) |handle| return Window.from(handle); + if (c.glfwGetCurrentContext()) |handle| return .from(handle); return null; } diff --git a/src/Command.zig b/src/Command.zig index e17c1b370..281dcce40 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -370,7 +370,7 @@ pub fn wait(self: Command, block: bool) !Exit { } }; - return Exit.init(res.status); + return .init(res.status); } /// Sets command->data to data. diff --git a/src/Surface.zig b/src/Surface.zig index f9e232340..32f7487d3 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -463,7 +463,7 @@ pub fn init( // Create our terminal grid with the initial size const app_mailbox: App.Mailbox = .{ .rt_app = rt_app, .mailbox = &app.mailbox }; var renderer_impl = try Renderer.init(alloc, .{ - .config = try Renderer.DerivedConfig.init(alloc, config), + .config = try .init(alloc, config), .font_grid = font_grid, .size = size, .surface_mailbox = .{ .surface = self, .app = app_mailbox }, diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 7bc84bcad..97466e9b5 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -423,7 +423,7 @@ pub const Surface = struct { pub fn init(self: *Surface, app: *App, opts: Options) !void { self.* = .{ .app = app, - .platform = try Platform.init(opts.platform_tag, opts.platform), + .platform = try .init(opts.platform_tag, opts.platform), .userdata = opts.userdata, .core_surface = undefined, .content_scale = .{ @@ -522,7 +522,7 @@ pub const Surface = struct { const alloc = self.app.core_app.alloc; const inspector = try alloc.create(Inspector); errdefer alloc.destroy(inspector); - inspector.* = try Inspector.init(self); + inspector.* = try .init(self); self.inspector = inspector; return inspector; } @@ -1180,7 +1180,7 @@ pub const CAPI = struct { // Create our runtime app var app = try global.alloc.create(App); errdefer global.alloc.destroy(app); - app.* = try App.init(core_app, config, opts.*); + app.* = try .init(core_app, config, opts.*); errdefer app.terminate(); return app; @@ -1949,7 +1949,7 @@ pub const CAPI = struct { } export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { - return ptr.initMetal(objc.Object.fromId(device)); + return ptr.initMetal(.fromId(device)); } export fn ghostty_inspector_metal_render( @@ -1958,8 +1958,8 @@ pub const CAPI = struct { descriptor: objc.c.id, ) void { return ptr.renderMetal( - objc.Object.fromId(command_buffer), - objc.Object.fromId(descriptor), + .fromId(command_buffer), + .fromId(descriptor), ) catch |err| { log.err("error rendering inspector err={}", .{err}); return; diff --git a/src/apprt/gtk/ClipboardConfirmationWindow.zig b/src/apprt/gtk/ClipboardConfirmationWindow.zig index f10fc79ac..fab1aa893 100644 --- a/src/apprt/gtk/ClipboardConfirmationWindow.zig +++ b/src/apprt/gtk/ClipboardConfirmationWindow.zig @@ -69,16 +69,16 @@ fn init( request: apprt.ClipboardRequest, is_secure_input: bool, ) !void { - var builder = switch (DialogType) { + var builder: Builder = switch (DialogType) { adw.AlertDialog => switch (request) { - .osc_52_read => Builder.init("ccw-osc-52-read", 1, 5), - .osc_52_write => Builder.init("ccw-osc-52-write", 1, 5), - .paste => Builder.init("ccw-paste", 1, 5), + .osc_52_read => .init("ccw-osc-52-read", 1, 5), + .osc_52_write => .init("ccw-osc-52-write", 1, 5), + .paste => .init("ccw-paste", 1, 5), }, adw.MessageDialog => switch (request) { - .osc_52_read => Builder.init("ccw-osc-52-read", 1, 2), - .osc_52_write => Builder.init("ccw-osc-52-write", 1, 2), - .paste => Builder.init("ccw-paste", 1, 2), + .osc_52_read => .init("ccw-osc-52-read", 1, 2), + .osc_52_write => .init("ccw-osc-52-write", 1, 2), + .paste => .init("ccw-paste", 1, 2), }, else => unreachable, }; diff --git a/src/apprt/gtk/ConfigErrorsDialog.zig b/src/apprt/gtk/ConfigErrorsDialog.zig index ccc5599ad..da70ccce1 100644 --- a/src/apprt/gtk/ConfigErrorsDialog.zig +++ b/src/apprt/gtk/ConfigErrorsDialog.zig @@ -32,9 +32,9 @@ pub fn maybePresent(app: *App, window: ?*Window) void { const config_errors_dialog = config_errors_dialog: { if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog; - var builder = switch (DialogType) { - adw.AlertDialog => Builder.init("config-errors-dialog", 1, 5), - adw.MessageDialog => Builder.init("config-errors-dialog", 1, 2), + var builder: Builder = switch (DialogType) { + adw.AlertDialog => .init("config-errors-dialog", 1, 5), + adw.MessageDialog => .init("config-errors-dialog", 1, 2), else => unreachable, }; diff --git a/src/apprt/gtk/ResizeOverlay.zig b/src/apprt/gtk/ResizeOverlay.zig index 767cf097d..2ab59624a 100644 --- a/src/apprt/gtk/ResizeOverlay.zig +++ b/src/apprt/gtk/ResizeOverlay.zig @@ -50,12 +50,12 @@ first: bool = true, pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void { self.* = .{ .surface = surface, - .config = DerivedConfig.init(config), + .config = .init(config), }; } pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void { - self.config = DerivedConfig.init(config); + self.config = .init(config); } /// De-initialize the ResizeOverlay. This removes any pending idlers/timers that diff --git a/src/apprt/gtk/Split.zig b/src/apprt/gtk/Split.zig index 9caa9ab56..fb719c3c9 100644 --- a/src/apprt/gtk/Split.zig +++ b/src/apprt/gtk/Split.zig @@ -138,7 +138,7 @@ pub fn init( .container = container, .top_left = .{ .surface = tl }, .bottom_right = .{ .surface = br }, - .orientation = Orientation.fromDirection(direction), + .orientation = .fromDirection(direction), }; // Replace the previous containers element with our split. This allows a diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index bcb78e087..30a3d28f7 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1191,7 +1191,7 @@ pub fn mouseOverLink(self: *Surface, uri_: ?[]const u8) void { return; } - self.url_widget = URLWidget.init(self.overlay, uriZ); + self.url_widget = .init(self.overlay, uriZ); } pub fn supportsClipboard( diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 4a5926a97..aa1f0a4b1 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -136,7 +136,7 @@ pub fn init(self: *Window, app: *App) !void { self.* = .{ .app = app, .last_config = @intFromPtr(&app.config), - .config = DerivedConfig.init(&app.config), + .config = .init(&app.config), .window = undefined, .headerbar = undefined, .tab_overview = null, @@ -148,7 +148,7 @@ pub fn init(self: *Window, app: *App) !void { }; // Create the window - self.window = adw.ApplicationWindow.new(app.app.as(gtk.Application)); + self.window = .new(app.app.as(gtk.Application)); const gtk_window = self.window.as(gtk.Window); const gtk_widget = self.window.as(gtk.Widget); errdefer gtk_window.destroy(); @@ -333,7 +333,7 @@ pub fn init(self: *Window, app: *App) !void { } // Setup our toast overlay if we have one - self.toast_overlay = adw.ToastOverlay.new(); + self.toast_overlay = .new(); self.toast_overlay.setChild(self.notebook.asWidget()); box.append(self.toast_overlay.as(gtk.Widget)); @@ -463,7 +463,7 @@ pub fn updateConfig( if (self.last_config == this_config) return; self.last_config = this_config; - self.config = DerivedConfig.init(config); + self.config = .init(config); // We always resync our appearance whenever the config changes. try self.syncAppearance(); diff --git a/src/apprt/gtk/inspector.zig b/src/apprt/gtk/inspector.zig index e3e61e258..3adeb9711 100644 --- a/src/apprt/gtk/inspector.zig +++ b/src/apprt/gtk/inspector.zig @@ -138,7 +138,7 @@ const Window = struct { }; // Create the window - self.window = gtk.ApplicationWindow.new(inspector.surface.app.app.as(gtk.Application)); + self.window = .new(inspector.surface.app.app.as(gtk.Application)); errdefer self.window.as(gtk.Window).destroy(); self.window.as(gtk.Window).setTitle(i18n._("Ghostty: Terminal Inspector")); diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index c2b6bf416..387905b18 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -109,7 +109,7 @@ pub const App = struct { return .{ .display = xlib_display, .base_event_code = base_event_code, - .atoms = Atoms.init(gdk_x11_display), + .atoms = .init(gdk_x11_display), }; } diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 0df261600..512975ac0 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -24,9 +24,9 @@ pub const LazyPathList = std.ArrayList(std.Build.LazyPath); pub fn init(b: *std.Build, cfg: *const Config) !SharedDeps { var result: SharedDeps = .{ .config = cfg, - .help_strings = try HelpStrings.init(b, cfg), - .unicode_tables = try UnicodeTables.init(b), - .framedata = try GhosttyFrameData.init(b), + .help_strings = try .init(b, cfg), + .unicode_tables = try .init(b), + .framedata = try .init(b), // Setup by retarget .options = undefined, @@ -72,7 +72,7 @@ fn initTarget( target: std.Build.ResolvedTarget, ) !void { // Update our metallib - self.metallib = MetallibStep.create(b, .{ + self.metallib = .create(b, .{ .name = "Ghostty", .target = target, .sources = &.{b.path("src/renderer/shaders/cell.metal")}, diff --git a/src/cli/args.zig b/src/cli/args.zig index 4860cdd74..68972a622 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -84,7 +84,7 @@ pub fn parse( // If the arena is unset, we create it. We mark that we own it // only so that we can clean it up on error. if (dst._arena == null) { - dst._arena = ArenaAllocator.init(alloc); + dst._arena = .init(alloc); arena_owned = true; } @@ -481,7 +481,7 @@ pub fn parseAutoStruct(comptime T: type, alloc: Allocator, v: []const u8) !T { // Keep track of which fields were set so we can error if a required // field was not set. const FieldSet = std.StaticBitSet(info.fields.len); - var fields_set: FieldSet = FieldSet.initEmpty(); + var fields_set: FieldSet = .initEmpty(); // We split each value by "," var iter = std.mem.splitSequence(u8, v, ","); diff --git a/src/config/Config.zig b/src/config/Config.zig index 6f1e89d41..8d08113bc 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2531,7 +2531,7 @@ pub fn load(alloc_gpa: Allocator) !Config { pub fn default(alloc_gpa: Allocator) Allocator.Error!Config { // Build up our basic config var result: Config = .{ - ._arena = ArenaAllocator.init(alloc_gpa), + ._arena = .init(alloc_gpa), }; errdefer result.deinit(); const alloc = result._arena.?.allocator(); @@ -3332,7 +3332,7 @@ pub fn parseManuallyHook( /// be deallocated while shallow clones exist. pub fn shallowClone(self: *const Config, alloc_gpa: Allocator) Config { var result = self.*; - result._arena = ArenaAllocator.init(alloc_gpa); + result._arena = .init(alloc_gpa); return result; } @@ -5975,7 +5975,7 @@ pub const QuickTerminalSize = struct { it.next() orelse return error.ValueRequired, cli.args.whitespace, ); - self.primary = try Size.parse(primary); + self.primary = try .parse(primary); self.secondary = secondary: { const secondary = std.mem.trim( @@ -5983,7 +5983,7 @@ pub const QuickTerminalSize = struct { it.next() orelse break :secondary null, cli.args.whitespace, ); - break :secondary try Size.parse(secondary); + break :secondary try .parse(secondary); }; if (it.next()) |_| return error.TooManyArguments; diff --git a/src/config/formatter.zig b/src/config/formatter.zig index ca3da1d91..cabf80953 100644 --- a/src/config/formatter.zig +++ b/src/config/formatter.zig @@ -153,7 +153,7 @@ pub const FileFormatter = struct { // If we're change-tracking then we need the default config to // compare against. var default: ?Config = if (self.changed) - try Config.default(self.alloc) + try .default(self.alloc) else null; defer if (default) |*v| v.deinit(); diff --git a/src/crash/sentry_envelope.zig b/src/crash/sentry_envelope.zig index 70eb99f51..6b675554c 100644 --- a/src/crash/sentry_envelope.zig +++ b/src/crash/sentry_envelope.zig @@ -331,7 +331,7 @@ pub const Item = union(enum) { // Decode the item. self.* = switch (encoded.type) { - .attachment => .{ .attachment = try Attachment.decode( + .attachment => .{ .attachment = try .decode( alloc, encoded, ) }, diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 37093b59a..16536300c 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -37,7 +37,7 @@ collection: Collection, /// The set of statuses and whether they're enabled or not. This defaults /// to true. This can be changed at runtime with no ill effect. -styles: StyleStatus = StyleStatus.initFill(true), +styles: StyleStatus = .initFill(true), /// If discovery is available, we'll look up fonts where we can't find /// the codepoint. This can be set after initialization. @@ -140,7 +140,7 @@ pub fn getIndex( // handle this. if (self.sprite) |sprite| { if (sprite.hasCodepoint(cp, p)) { - return Collection.Index.initSpecial(.sprite); + return .initSpecial(.sprite); } } @@ -388,7 +388,7 @@ test getIndex { { errdefer c.deinit(alloc); - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -398,7 +398,7 @@ test getIndex { _ = try c.add( alloc, .regular, - .{ .loaded = try Face.init( + .{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, @@ -408,7 +408,7 @@ test getIndex { _ = try c.add( alloc, .regular, - .{ .loaded = try Face.init( + .{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, @@ -467,17 +467,17 @@ test "getIndex disabled font style" { var c = Collection.init(); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) }); - _ = try c.add(alloc, .bold, .{ .loaded = try Face.init( + _ = try c.add(alloc, .bold, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, ) }); - _ = try c.add(alloc, .italic, .{ .loaded = try Face.init( + _ = try c.add(alloc, .italic, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 59f89d402..8533331bc 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -55,7 +55,7 @@ load_options: ?LoadOptions = null, pub fn init() Collection { // Initialize our styles array, preallocating some space that is // likely to be used. - return .{ .faces = StyleArray.initFill(.{}) }; + return .{ .faces = .initFill(.{}) }; } pub fn deinit(self: *Collection, alloc: Allocator) void { @@ -707,7 +707,7 @@ test "add full" { defer c.deinit(alloc); for (0..Index.Special.start - 1) |_| { - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12 } }, @@ -755,7 +755,7 @@ test getFace { var c = init(); defer c.deinit(alloc); - const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( + const idx = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -779,7 +779,7 @@ test getIndex { var c = init(); defer c.deinit(alloc); - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -811,7 +811,7 @@ test completeStyles { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -838,7 +838,7 @@ test setSize { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -861,7 +861,7 @@ test hasCodepoint { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( + const idx = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -885,7 +885,7 @@ test "hasCodepoint emoji default graphical" { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - const idx = try c.add(alloc, .regular, .{ .loaded = try Face.init( + const idx = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, @@ -908,7 +908,7 @@ test "metrics" { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 8794ccea9..f9ce0bff5 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -254,7 +254,7 @@ fn loadWebCanvas( opts: font.face.Options, ) !Face { const wc = self.wc.?; - return try Face.initNamed(wc.alloc, wc.font_str, opts, wc.presentation); + return try .initNamed(wc.alloc, wc.font_str, opts, wc.presentation); } /// Returns true if this face can satisfy the given codepoint and diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 72e97fad8..35770f920 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -319,7 +319,7 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { switch (mode) { .normal => { - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 8ad30629e..858d7930f 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -126,7 +126,7 @@ pub fn ref( .ref = 1, }; - grid.* = try SharedGrid.init(self.alloc, resolver: { + grid.* = try .init(self.alloc, resolver: { // Build our collection. This is the expensive operation that // involves finding fonts, loading them (maybe, some are deferred), // etc. @@ -258,7 +258,7 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try Face.init( + .{ .fallback_loaded = try .init( self.font_lib, font.embedded.regular, load_options.faceOptions(), @@ -267,7 +267,7 @@ fn collection( _ = try c.add( self.alloc, .bold, - .{ .fallback_loaded = try Face.init( + .{ .fallback_loaded = try .init( self.font_lib, font.embedded.bold, load_options.faceOptions(), @@ -276,7 +276,7 @@ fn collection( _ = try c.add( self.alloc, .italic, - .{ .fallback_loaded = try Face.init( + .{ .fallback_loaded = try .init( self.font_lib, font.embedded.italic, load_options.faceOptions(), @@ -285,7 +285,7 @@ fn collection( _ = try c.add( self.alloc, .bold_italic, - .{ .fallback_loaded = try Face.init( + .{ .fallback_loaded = try .init( self.font_lib, font.embedded.bold_italic, load_options.faceOptions(), @@ -318,7 +318,7 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try Face.init( + .{ .fallback_loaded = try .init( self.font_lib, font.embedded.emoji, load_options.faceOptions(), @@ -327,7 +327,7 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try Face.init( + .{ .fallback_loaded = try .init( self.font_lib, font.embedded.emoji_text, load_options.faceOptions(), @@ -391,7 +391,7 @@ fn discover(self: *SharedGridSet) !?*Discover { // If we initialized, use it if (self.font_discover) |*v| return v; - self.font_discover = Discover.init(); + self.font_discover = .init(); return &self.font_discover.?; } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 639eae43c..06bba661f 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -97,7 +97,7 @@ pub const Face = struct { errdefer if (comptime harfbuzz_shaper) hb_font.destroy(); const color: ?ColorState = if (traits.color_glyphs) - try ColorState.init(ct_font) + try .init(ct_font) else null; errdefer if (color) |v| v.deinit(); diff --git a/src/font/face/freetype_convert.zig b/src/font/face/freetype_convert.zig index 6df350bfa..3a7cf8c98 100644 --- a/src/font/face/freetype_convert.zig +++ b/src/font/face/freetype_convert.zig @@ -30,7 +30,7 @@ fn genMap() Map { // Initialize to no converter var i: usize = 0; while (i < freetype.c.FT_PIXEL_MODE_MAX) : (i += 1) { - result[i] = AtlasArray.initFill(null); + result[i] = .initFill(null); } // Map our converters diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index f2ac5b85d..8e2c45c69 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -191,7 +191,7 @@ pub const Shaper = struct { // Create the CF release thread. var cf_release_thread = try alloc.create(CFReleaseThread); errdefer alloc.destroy(cf_release_thread); - cf_release_thread.* = try CFReleaseThread.init(alloc); + cf_release_thread.* = try .init(alloc); errdefer cf_release_thread.deinit(); // Start the CF release thread. @@ -1768,7 +1768,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { c.load_options = .{ .library = lib }; // Setup group - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12 } }, @@ -1776,7 +1776,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, @@ -1795,7 +1795,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { errdefer face.deinit(); _ = try c.add(alloc, .regular, .{ .deferred = face }); } - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, @@ -1803,7 +1803,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { const grid_ptr = try alloc.create(SharedGrid); errdefer alloc.destroy(grid_ptr); - grid_ptr.* = try SharedGrid.init(alloc, .{ .collection = c }); + grid_ptr.* = try .init(alloc, .{ .collection = c }); errdefer grid_ptr.*.deinit(alloc); var shaper = try Shaper.init(alloc, .{}); diff --git a/src/font/shaper/feature.zig b/src/font/shaper/feature.zig index c2d49234d..66d0cb1f7 100644 --- a/src/font/shaper/feature.zig +++ b/src/font/shaper/feature.zig @@ -21,7 +21,7 @@ pub const Feature = struct { pub fn fromString(str: []const u8) ?Feature { var fbs = std.io.fixedBufferStream(str); const reader = fbs.reader(); - return Feature.fromReader(reader); + return .fromReader(reader); } /// Parse a single font feature setting from a std.io.Reader, with a version diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index eb8130f79..361cbbe93 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -1227,7 +1227,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { c.load_options = .{ .library = lib }; // Setup group - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12 } }, @@ -1235,7 +1235,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { if (comptime !font.options.backend.hasCoretext()) { // Coretext doesn't support Noto's format - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, @@ -1254,7 +1254,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { errdefer face.deinit(); _ = try c.add(alloc, .regular, .{ .deferred = face }); } - _ = try c.add(alloc, .regular, .{ .loaded = try Face.init( + _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, @@ -1262,7 +1262,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { const grid_ptr = try alloc.create(SharedGrid); errdefer alloc.destroy(grid_ptr); - grid_ptr.* = try SharedGrid.init(alloc, .{ .collection = c }); + grid_ptr.* = try .init(alloc, .{ .collection = c }); errdefer grid_ptr.*.deinit(alloc); var shaper = try Shaper.init(alloc, .{}); diff --git a/src/font/sprite/canvas.zig b/src/font/sprite/canvas.zig index ed00aef12..a5ca7b290 100644 --- a/src/font/sprite/canvas.zig +++ b/src/font/sprite/canvas.zig @@ -150,7 +150,7 @@ pub const Canvas = struct { /// Acquires a z2d drawing context, caller MUST deinit context. pub fn getContext(self: *Canvas) z2d.Context { - return z2d.Context.init(self.alloc, &self.sfc); + return .init(self.alloc, &self.sfc); } /// Draw and fill a single pixel diff --git a/src/global.zig b/src/global.zig index 375c10538..d11dd775b 100644 --- a/src/global.zig +++ b/src/global.zig @@ -139,7 +139,7 @@ pub const GlobalState = struct { std.log.info("libxev default backend={s}", .{@tagName(xev.backend)}); // As early as possible, initialize our resource limits. - self.rlimits = ResourceLimits.init(); + self.rlimits = .init(); // Initialize our crash reporting. crash.init(self.alloc) catch |err| { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 59adc7149..3818d99a6 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -71,7 +71,7 @@ pub const Parser = struct { // parse the action now. return .{ .trigger_it = .{ .input = input[0..eql_idx] }, - .action = try Action.parse(input[eql_idx + 1 ..]), + .action = try .parse(input[eql_idx + 1 ..]), .flags = flags, }; } @@ -158,7 +158,7 @@ const SequenceIterator = struct { const rem = self.input[self.i..]; const idx = std.mem.indexOf(u8, rem, ">") orelse rem.len; defer self.i += idx + 1; - return try Trigger.parse(rem[0..idx]); + return try .parse(rem[0..idx]); } /// Returns true if there are no more triggers to parse. diff --git a/src/input/KeyEncoder.zig b/src/input/KeyEncoder.zig index 41634f2f1..b5f18b5a2 100644 --- a/src/input/KeyEncoder.zig +++ b/src/input/KeyEncoder.zig @@ -164,7 +164,7 @@ fn kitty( var seq: KittySequence = .{ .key = entry.code, .final = entry.final, - .mods = KittyMods.fromInput( + .mods = .fromInput( self.event.action, self.event.key, all_mods, diff --git a/src/inspector/termio.zig b/src/inspector/termio.zig index 6aa6628ab..5ab9d3cd4 100644 --- a/src/inspector/termio.zig +++ b/src/inspector/termio.zig @@ -308,7 +308,7 @@ pub const VTHandler = struct { current_seq: usize = 1, /// Exclude certain actions by tag. - filter_exclude: ActionTagSet = ActionTagSet.initMany(&.{.print}), + filter_exclude: ActionTagSet = .initMany(&.{.print}), filter_text: *cimgui.c.ImGuiTextFilter, const ActionTagSet = std.EnumSet(terminal.Parser.Action.Tag); diff --git a/src/os/args.zig b/src/os/args.zig index 9f7401c94..a531a418b 100644 --- a/src/os/args.zig +++ b/src/os/args.zig @@ -12,7 +12,7 @@ const macos = @import("macos"); /// but handles macOS using NSProcessInfo instead of libc argc/argv. pub fn iterator(allocator: Allocator) ArgIterator.InitError!ArgIterator { //if (true) return try std.process.argsWithAllocator(allocator); - return ArgIterator.initWithAllocator(allocator); + return .initWithAllocator(allocator); } /// Duck-typed to std.process.ArgIterator diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 46ef8609b..1e9c29b26 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -155,7 +155,7 @@ pub fn init( return .{ .alloc = alloc, - .config = DerivedConfig.init(config), + .config = .init(config), .loop = loop, .wakeup = wakeup_h, .stop = stop_h, diff --git a/src/renderer/cursor.zig b/src/renderer/cursor.zig index d8769d9e2..287b83450 100644 --- a/src/renderer/cursor.zig +++ b/src/renderer/cursor.zig @@ -62,7 +62,7 @@ pub fn style( } // Otherwise, we use whatever style the terminal wants. - return Style.fromTerminal(state.terminal.screen.cursor.cursor_style); + return .fromTerminal(state.terminal.screen.cursor.cursor_style); } test "cursor: default uses configured style" { diff --git a/src/renderer/link.zig b/src/renderer/link.zig index 994190ec8..410fb8632 100644 --- a/src/renderer/link.zig +++ b/src/renderer/link.zig @@ -179,7 +179,7 @@ pub const Set = struct { if (current) |*sel| { sel.endPtr().* = cell_pin; } else { - current = terminal.Selection.init( + current = .init( cell_pin, cell_pin, false, diff --git a/src/renderer/metal/cell.zig b/src/renderer/metal/cell.zig index 61b8887fd..e1bcb7b9f 100644 --- a/src/renderer/metal/cell.zig +++ b/src/renderer/metal/cell.zig @@ -44,7 +44,7 @@ fn ArrayListPool(comptime T: type) type { }; for (self.lists) |*list| { - list.* = try ArrayListT.initCapacity(alloc, initial_capacity); + list.* = try .initCapacity(alloc, initial_capacity); } return self; diff --git a/src/renderer/size.zig b/src/renderer/size.zig index 83e921a26..b26c1581e 100644 --- a/src/renderer/size.zig +++ b/src/renderer/size.zig @@ -22,7 +22,7 @@ pub const Size = struct { /// taking the screen size, removing padding, and dividing by the cell /// dimensions. pub fn grid(self: Size) GridSize { - return GridSize.init(self.screen.subPadding(self.padding), self.cell); + return .init(self.screen.subPadding(self.padding), self.cell); } /// The size of the terminal. This is the same as the screen without @@ -39,7 +39,7 @@ pub const Size = struct { self.padding = explicit; // Now we can calculate the balanced padding - self.padding = Padding.balanced( + self.padding = .balanced( self.screen, self.grid(), self.cell, diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 95519fe99..300af8e13 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -287,8 +287,8 @@ fn initPages( // Initialize the first set of pages to contain our viewport so that // the top of the first page is always the active area. node.* = .{ - .data = Page.initBuf( - OffsetBuf.init(page_buf), + .data = .initBuf( + .init(page_buf), Page.layout(cap), ), }; @@ -472,7 +472,7 @@ pub fn clone( }; // Setup our pools - break :alloc try MemoryPool.init( + break :alloc try .init( alloc, std.heap.page_allocator, page_count, @@ -1201,7 +1201,7 @@ const ReflowCursor = struct { node.data.size.rows = 1; list.pages.insertAfter(self.node, node); - self.* = ReflowCursor.init(node); + self.* = .init(node); self.new_rows = new_rows; } @@ -1817,7 +1817,7 @@ pub fn grow(self: *PageList) !?*List.Node { @memset(buf, 0); // Initialize our new page and reinsert it as the last - first.data = Page.initBuf(OffsetBuf.init(buf), layout); + first.data = .initBuf(.init(buf), layout); first.data.size.rows = 1; self.pages.insertAfter(last, first); @@ -1989,7 +1989,7 @@ fn createPageExt( // to undefined, 0xAA. if (comptime std.debug.runtime_safety) @memset(page_buf, 0); - page.* = .{ .data = Page.initBuf(OffsetBuf.init(page_buf), layout) }; + page.* = .{ .data = .initBuf(.init(page_buf), layout) }; page.data.size.rows = 0; if (total_size) |v| { diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 4e74f04ba..14ed6d6df 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -217,7 +217,7 @@ intermediates_idx: u8 = 0, /// Param tracking, building params: [MAX_PARAMS]u16 = undefined, -params_sep: Action.CSI.SepList = Action.CSI.SepList.initEmpty(), +params_sep: Action.CSI.SepList = .initEmpty(), params_idx: u8 = 0, param_acc: u16 = 0, param_acc_idx: u8 = 0, @@ -395,7 +395,7 @@ fn doAction(self: *Parser, action: TransitionAction, c: u8) ?Action { pub fn clear(self: *Parser) void { self.intermediates_idx = 0; self.params_idx = 0; - self.params_sep = Action.CSI.SepList.initEmpty(); + self.params_sep = .initEmpty(); self.param_acc = 0; self.param_acc_idx = 0; } diff --git a/src/terminal/Screen.zig b/src/terminal/Screen.zig index 9ab4b23e2..2688b03a7 100644 --- a/src/terminal/Screen.zig +++ b/src/terminal/Screen.zig @@ -171,7 +171,7 @@ pub const SavedCursor = struct { /// State required for all charset operations. pub const CharsetState = struct { /// The list of graphical charsets by slot - charsets: CharsetArray = CharsetArray.initFill(charsets.Charset.utf8), + charsets: CharsetArray = .initFill(charsets.Charset.utf8), /// GL is the slot to use when using a 7-bit printable char (up to 127) /// GR used for 8-bit printable chars. @@ -2433,7 +2433,7 @@ pub fn selectLine(self: *const Screen, opts: SelectLine) ?Selection { return null; }; - return Selection.init(start, end, false); + return .init(start, end, false); } /// Return the selection for all contents on the screen. Surrounding @@ -2489,7 +2489,7 @@ pub fn selectAll(self: *Screen) ?Selection { return null; }; - return Selection.init(start, end, false); + return .init(start, end, false); } /// Select the nearest word to start point that is between start_pt and @@ -2624,7 +2624,7 @@ pub fn selectWord(self: *Screen, pin: Pin) ?Selection { break :start prev; }; - return Selection.init(start, end, false); + return .init(start, end, false); } /// Select the command output under the given point. The limits of the output @@ -2724,7 +2724,7 @@ pub fn selectOutput(self: *Screen, pin: Pin) ?Selection { break :boundary it_prev; }; - return Selection.init(start, end, false); + return .init(start, end, false); } /// Returns the selection bounds for the prompt at the given point. If the @@ -2805,7 +2805,7 @@ pub fn selectPrompt(self: *Screen, pin: Pin) ?Selection { break :end it_prev; }; - return Selection.init(start, end, false); + return .init(start, end, false); } pub const LineIterator = struct { diff --git a/src/terminal/Selection.zig b/src/terminal/Selection.zig index a90595d20..267f223d5 100644 --- a/src/terminal/Selection.zig +++ b/src/terminal/Selection.zig @@ -228,7 +228,7 @@ pub fn order(self: Selection, s: *const Screen) Order { /// Note that only forward and reverse are useful desired orders for this /// function. All other orders act as if forward order was desired. pub fn ordered(self: Selection, s: *const Screen, desired: Order) Selection { - if (self.order(s) == desired) return Selection.init( + if (self.order(s) == desired) return .init( self.start(), self.end(), self.rectangle, @@ -237,9 +237,9 @@ pub fn ordered(self: Selection, s: *const Screen, desired: Order) Selection { const tl = self.topLeft(s); const br = self.bottomRight(s); return switch (desired) { - .forward => Selection.init(tl, br, self.rectangle), - .reverse => Selection.init(br, tl, self.rectangle), - else => Selection.init(tl, br, self.rectangle), + .forward => .init(tl, br, self.rectangle), + .reverse => .init(br, tl, self.rectangle), + else => .init(tl, br, self.rectangle), }; } diff --git a/src/terminal/StringMap.zig b/src/terminal/StringMap.zig index 9892c13df..dde69d25e 100644 --- a/src/terminal/StringMap.zig +++ b/src/terminal/StringMap.zig @@ -80,7 +80,7 @@ pub const Match = struct { const end_idx: usize = @intCast(self.region.ends()[0] - 1); const start_pt = self.map.map[self.offset + start_idx]; const end_pt = self.map.map[self.offset + end_idx]; - return Selection.init(start_pt, end_pt, false); + return .init(start_pt, end_pt, false); } }; diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index efb9684eb..595fee1ba 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -79,7 +79,7 @@ default_palette: color.Palette = color.default, color_palette: struct { const Mask = std.StaticBitSet(@typeInfo(color.Palette).array.len); colors: color.Palette = color.default, - mask: Mask = Mask.initEmpty(), + mask: Mask = .initEmpty(), } = .{}, /// The previous printed character. This is used for the repeat previous @@ -210,9 +210,9 @@ pub fn init( .cols = cols, .rows = rows, .active_screen = .primary, - .screen = try Screen.init(alloc, cols, rows, opts.max_scrollback), - .secondary_screen = try Screen.init(alloc, cols, rows, 0), - .tabstops = try Tabstops.init(alloc, cols, TABSTOP_INTERVAL), + .screen = try .init(alloc, cols, rows, opts.max_scrollback), + .secondary_screen = try .init(alloc, cols, rows, 0), + .tabstops = try .init(alloc, cols, TABSTOP_INTERVAL), .scrolling_region = .{ .top = 0, .bottom = rows - 1, @@ -2454,7 +2454,7 @@ pub fn resize( // Resize our tabstops if (self.cols != cols) { self.tabstops.deinit(alloc); - self.tabstops = try Tabstops.init(alloc, cols, 8); + self.tabstops = try .init(alloc, cols, 8); } // If we're making the screen smaller, dealloc the unused items. diff --git a/src/terminal/bitmap_allocator.zig b/src/terminal/bitmap_allocator.zig index f96d39831..68d968768 100644 --- a/src/terminal/bitmap_allocator.zig +++ b/src/terminal/bitmap_allocator.zig @@ -403,7 +403,7 @@ test "BitmapAllocator alloc sequentially" { const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size); defer alloc.free(buf); - var bm = Alloc.init(OffsetBuf.init(buf), layout); + var bm = Alloc.init(.init(buf), layout); const ptr = try bm.alloc(u8, buf, 1); ptr[0] = 'A'; @@ -429,7 +429,7 @@ test "BitmapAllocator alloc non-byte" { const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size); defer alloc.free(buf); - var bm = Alloc.init(OffsetBuf.init(buf), layout); + var bm = Alloc.init(.init(buf), layout); const ptr = try bm.alloc(u21, buf, 1); ptr[0] = 'A'; @@ -453,7 +453,7 @@ test "BitmapAllocator alloc non-byte multi-chunk" { const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size); defer alloc.free(buf); - var bm = Alloc.init(OffsetBuf.init(buf), layout); + var bm = Alloc.init(.init(buf), layout); const ptr = try bm.alloc(u21, buf, 6); try testing.expectEqual(@as(usize, 6), ptr.len); for (ptr) |*v| v.* = 'A'; @@ -478,7 +478,7 @@ test "BitmapAllocator alloc large" { const buf = try alloc.alignedAlloc(u8, Alloc.base_align, layout.total_size); defer alloc.free(buf); - var bm = Alloc.init(OffsetBuf.init(buf), layout); + var bm = Alloc.init(.init(buf), layout); const ptr = try bm.alloc(u8, buf, 129); ptr[0] = 'A'; bm.free(buf, ptr); diff --git a/src/terminal/hash_map.zig b/src/terminal/hash_map.zig index 0cc17a747..9a16be3b2 100644 --- a/src/terminal/hash_map.zig +++ b/src/terminal/hash_map.zig @@ -893,7 +893,7 @@ test "HashMap basic usage" { const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); const count = 5; var i: u32 = 0; @@ -927,7 +927,7 @@ test "HashMap ensureTotalCapacity" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); const initial_capacity = map.capacity(); try testing.expect(initial_capacity >= 20); @@ -947,7 +947,7 @@ test "HashMap ensureUnusedCapacity with tombstones" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: i32 = 0; while (i < 100) : (i += 1) { @@ -965,7 +965,7 @@ test "HashMap clearRetainingCapacity" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); map.clearRetainingCapacity(); @@ -996,7 +996,7 @@ test "HashMap ensureTotalCapacity with existing elements" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); try map.put(0, 0); try expectEqual(map.count(), 1); @@ -1015,7 +1015,7 @@ test "HashMap remove" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: u32 = 0; while (i < 16) : (i += 1) { @@ -1053,7 +1053,7 @@ test "HashMap reverse removes" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: u32 = 0; while (i < 16) : (i += 1) { @@ -1081,7 +1081,7 @@ test "HashMap multiple removes on same metadata" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: u32 = 0; while (i < 16) : (i += 1) { @@ -1124,7 +1124,7 @@ test "HashMap put and remove loop in random order" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var keys = std.ArrayList(u32).init(alloc); defer keys.deinit(); @@ -1162,7 +1162,7 @@ test "HashMap put" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: u32 = 0; while (i < 16) : (i += 1) { @@ -1193,7 +1193,7 @@ test "HashMap put full load" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); for (0..cap) |i| try map.put(i, i); for (0..cap) |i| try expectEqual(map.get(i).?, i); @@ -1209,7 +1209,7 @@ test "HashMap putAssumeCapacity" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: u32 = 0; while (i < 20) : (i += 1) { @@ -1244,7 +1244,7 @@ test "HashMap repeat putAssumeCapacity/remove" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); const limit = cap; @@ -1280,7 +1280,7 @@ test "HashMap getOrPut" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: u32 = 0; while (i < 10) : (i += 1) { @@ -1309,7 +1309,7 @@ test "HashMap basic hash map usage" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); try testing.expect((try map.fetchPut(1, 11)) == null); try testing.expect((try map.fetchPut(2, 22)) == null); @@ -1360,7 +1360,7 @@ test "HashMap ensureUnusedCapacity" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); try map.ensureUnusedCapacity(32); try testing.expectError(error.OutOfMemory, map.ensureUnusedCapacity(cap + 1)); @@ -1374,7 +1374,7 @@ test "HashMap removeByPtr" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); var i: i32 = undefined; i = 0; @@ -1405,7 +1405,7 @@ test "HashMap removeByPtr 0 sized key" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); try map.put(0, 0); @@ -1429,7 +1429,7 @@ test "HashMap repeat fetchRemove" { const layout = Map.layoutForCapacity(cap); const buf = try alloc.alignedAlloc(u8, Map.base_align, layout.total_size); defer alloc.free(buf); - var map = Map.init(OffsetBuf.init(buf), layout); + var map = Map.init(.init(buf), layout); map.putAssumeCapacity(0, {}); map.putAssumeCapacity(1, {}); @@ -1457,7 +1457,7 @@ test "OffsetHashMap basic usage" { const layout = OffsetMap.layout(cap); const buf = try alloc.alignedAlloc(u8, OffsetMap.base_align, layout.total_size); defer alloc.free(buf); - var offset_map = OffsetMap.init(OffsetBuf.init(buf), layout); + var offset_map = OffsetMap.init(.init(buf), layout); var map = offset_map.map(buf.ptr); const count = 5; @@ -1492,7 +1492,7 @@ test "OffsetHashMap remake map" { const layout = OffsetMap.layout(cap); const buf = try alloc.alignedAlloc(u8, OffsetMap.base_align, layout.total_size); defer alloc.free(buf); - var offset_map = OffsetMap.init(OffsetBuf.init(buf), layout); + var offset_map = OffsetMap.init(.init(buf), layout); { var map = offset_map.map(buf.ptr); diff --git a/src/terminal/kitty/graphics_command.zig b/src/terminal/kitty/graphics_command.zig index 61ba33a4d..adc6edafe 100644 --- a/src/terminal/kitty/graphics_command.zig +++ b/src/terminal/kitty/graphics_command.zig @@ -155,17 +155,17 @@ pub const Parser = struct { break :action c; }; const control: Command.Control = switch (action) { - 'q' => .{ .query = try Transmission.parse(self.kv) }, - 't' => .{ .transmit = try Transmission.parse(self.kv) }, + 'q' => .{ .query = try .parse(self.kv) }, + 't' => .{ .transmit = try .parse(self.kv) }, 'T' => .{ .transmit_and_display = .{ - .transmission = try Transmission.parse(self.kv), - .display = try Display.parse(self.kv), + .transmission = try .parse(self.kv), + .display = try .parse(self.kv), } }, - 'p' => .{ .display = try Display.parse(self.kv) }, - 'd' => .{ .delete = try Delete.parse(self.kv) }, - 'f' => .{ .transmit_animation_frame = try AnimationFrameLoading.parse(self.kv) }, - 'a' => .{ .control_animation = try AnimationControl.parse(self.kv) }, - 'c' => .{ .compose_animation = try AnimationFrameComposition.parse(self.kv) }, + 'p' => .{ .display = try .parse(self.kv) }, + 'd' => .{ .delete = try .parse(self.kv) }, + 'f' => .{ .transmit_animation_frame = try .parse(self.kv) }, + 'a' => .{ .control_animation = try .parse(self.kv) }, + 'c' => .{ .compose_animation = try .parse(self.kv) }, else => return error.InvalidFormat, }; diff --git a/src/terminal/kitty/graphics_exec.zig b/src/terminal/kitty/graphics_exec.zig index 25c819b10..f917c104a 100644 --- a/src/terminal/kitty/graphics_exec.zig +++ b/src/terminal/kitty/graphics_exec.zig @@ -324,7 +324,7 @@ fn loadAndAddImage( } break :loading loading.*; - } else try LoadingImage.init(alloc, cmd); + } else try .init(alloc, cmd); // We only want to deinit on error. If we're chunking, then we don't // want to deinit at all. If we're not chunking, then we'll deinit diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index ce7afdf64..932964137 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1354,8 +1354,8 @@ pub const Parser = struct { } switch (self.command) { - .report_color => |*c| c.terminator = Terminator.init(terminator_ch), - .kitty_color_protocol => |*c| c.terminator = Terminator.init(terminator_ch), + .report_color => |*c| c.terminator = .init(terminator_ch), + .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), else => {}, } diff --git a/src/terminal/page.zig b/src/terminal/page.zig index acb757592..d7f252af1 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -241,23 +241,23 @@ pub const Page = struct { l.styles_layout, .{}, ), - .string_alloc = StringAlloc.init( + .string_alloc = .init( buf.add(l.string_alloc_start), l.string_alloc_layout, ), - .grapheme_alloc = GraphemeAlloc.init( + .grapheme_alloc = .init( buf.add(l.grapheme_alloc_start), l.grapheme_alloc_layout, ), - .grapheme_map = GraphemeMap.init( + .grapheme_map = .init( buf.add(l.grapheme_map_start), l.grapheme_map_layout, ), - .hyperlink_map = hyperlink.Map.init( + .hyperlink_map = .init( buf.add(l.hyperlink_map_start), l.hyperlink_map_layout, ), - .hyperlink_set = hyperlink.Set.init( + .hyperlink_set = .init( buf.add(l.hyperlink_set_start), l.hyperlink_set_layout, .{}, @@ -280,7 +280,7 @@ pub const Page = struct { // We zero the page memory as u64 instead of u8 because // we can and it's empirically quite a bit faster. @memset(@as([*]u64, @ptrCast(self.memory))[0 .. self.memory.len / 8], 0); - self.* = initBuf(OffsetBuf.init(self.memory), layout(self.capacity)); + self.* = initBuf(.init(self.memory), layout(self.capacity)); } pub const IntegrityError = error{ @@ -2260,7 +2260,7 @@ test "Page appendGrapheme small" { defer page.deinit(); const rac = page.getRowAndCell(0, 0); - rac.cell.* = Cell.init(0x09); + rac.cell.* = .init(0x09); // One try page.appendGrapheme(rac.row, rac.cell, 0x0A); @@ -2289,7 +2289,7 @@ test "Page appendGrapheme larger than chunk" { defer page.deinit(); const rac = page.getRowAndCell(0, 0); - rac.cell.* = Cell.init(0x09); + rac.cell.* = .init(0x09); const count = grapheme_chunk_len * 10; for (0..count) |i| { @@ -2312,11 +2312,11 @@ test "Page clearGrapheme not all cells" { defer page.deinit(); const rac = page.getRowAndCell(0, 0); - rac.cell.* = Cell.init(0x09); + rac.cell.* = .init(0x09); try page.appendGrapheme(rac.row, rac.cell, 0x0A); const rac2 = page.getRowAndCell(1, 0); - rac2.cell.* = Cell.init(0x09); + rac2.cell.* = .init(0x09); try page.appendGrapheme(rac2.row, rac2.cell, 0x0A); // Clear it diff --git a/src/terminal/search.zig b/src/terminal/search.zig index 56b181c48..2f87f894b 100644 --- a/src/terminal/search.zig +++ b/src/terminal/search.zig @@ -365,7 +365,7 @@ const SlidingWindow = struct { } self.assertIntegrity(); - return Selection.init(tl, br, false); + return .init(tl, br, false); } /// Convert a data index into a pin. @@ -417,7 +417,7 @@ const SlidingWindow = struct { // Initialize our metadata for the node. var meta: Meta = .{ .node = node, - .cell_map = Page.CellMap.init(alloc), + .cell_map = .init(alloc), }; errdefer meta.deinit(); diff --git a/src/terminal/sgr.zig b/src/terminal/sgr.zig index 2bc32c5f9..e4b85fbdd 100644 --- a/src/terminal/sgr.zig +++ b/src/terminal/sgr.zig @@ -98,7 +98,7 @@ pub const Attribute = union(enum) { /// Parser parses the attributes from a list of SGR parameters. pub const Parser = struct { params: []const u16, - params_sep: SepList = SepList.initEmpty(), + params_sep: SepList = .initEmpty(), idx: usize = 0, /// Next returns the next attribute or null if there are no more attributes. @@ -376,7 +376,7 @@ fn testParse(params: []const u16) Attribute { } fn testParseColon(params: []const u16) Attribute { - var p: Parser = .{ .params = params, .params_sep = SepList.initFull() }; + var p: Parser = .{ .params = params, .params_sep = .initFull() }; return p.next().?; } diff --git a/src/terminal/style.zig b/src/terminal/style.zig index 34b07772a..f35a4e1f7 100644 --- a/src/terminal/style.zig +++ b/src/terminal/style.zig @@ -302,9 +302,9 @@ pub const Style = struct { .underline = std.meta.activeTag(style.underline_color), }, .data = .{ - .fg = Data.fromColor(style.fg_color), - .bg = Data.fromColor(style.bg_color), - .underline = Data.fromColor(style.underline_color), + .fg = .fromColor(style.fg_color), + .bg = .fromColor(style.bg_color), + .underline = .fromColor(style.underline_color), }, .flags = style.flags, }; @@ -349,7 +349,7 @@ test "Set basic usage" { const style: Style = .{ .flags = .{ .bold = true } }; const style2: Style = .{ .flags = .{ .italic = true } }; - var set = Set.init(OffsetBuf.init(buf), layout, .{}); + var set = Set.init(.init(buf), layout, .{}); // Add style const id = try set.add(buf, style); diff --git a/src/terminal/x11_color.zig b/src/terminal/x11_color.zig index 88bc30f09..977cd4538 100644 --- a/src/terminal/x11_color.zig +++ b/src/terminal/x11_color.zig @@ -33,7 +33,7 @@ fn colorMap() !ColorMap { } assert(i == len); - return ColorMap.initComptime(kvs); + return .initComptime(kvs); } /// This is the rgb.txt file from the X11 project. This was last sourced diff --git a/src/unicode/props.zig b/src/unicode/props.zig index 8c7621b79..99c57aa0a 100644 --- a/src/unicode/props.zig +++ b/src/unicode/props.zig @@ -125,7 +125,7 @@ pub fn get(cp: u21) Properties { return .{ .width = @intCast(@min(2, @max(0, zg_width))), - .grapheme_boundary_class = GraphemeBoundaryClass.init(cp), + .grapheme_boundary_class = .init(cp), }; } From 468bfce09167eefc63721959369435d5e7f9d479 Mon Sep 17 00:00:00 2001 From: Kat <65649991+00-kat@users.noreply.github.com> Date: Tue, 27 May 2025 22:40:01 +1000 Subject: [PATCH 293/642] Correct `$XDG_CONFIG_DIR` to `$XDG_CONFIG_HOME` in theme documentation. --- src/cli/list_themes.zig | 2 +- src/config/Config.zig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/list_themes.zig b/src/cli/list_themes.zig index 54f4c0969..4bb8a74eb 100644 --- a/src/cli/list_themes.zig +++ b/src/cli/list_themes.zig @@ -77,7 +77,7 @@ const ThemeListElement = struct { /// Two different directories will be searched for themes. /// /// The first directory is the `themes` subdirectory of your Ghostty -/// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or +/// configuration directory. This is `$XDG_CONFIG_HOME/ghostty/themes` or /// `~/.config/ghostty/themes`. /// /// The second directory is the `themes` subdirectory of the Ghostty resources diff --git a/src/config/Config.zig b/src/config/Config.zig index 6f1e89d41..4eb419f87 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -410,7 +410,7 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ /// include path separators unless it is an absolute pathname. /// /// The first directory is the `themes` subdirectory of your Ghostty -/// configuration directory. This is `$XDG_CONFIG_DIR/ghostty/themes` or +/// configuration directory. This is `$XDG_CONFIG_HOME/ghostty/themes` or /// `~/.config/ghostty/themes`. /// /// The second directory is the `themes` subdirectory of the Ghostty resources From 1ce654494504dedde11b0bbe7ae1493a9db45c4d Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 19:00:28 -0600 Subject: [PATCH 294/642] Wrap comment at 80 cols --- src/terminal/point.zig | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/terminal/point.zig b/src/terminal/point.zig index 12b71014b..f2544f90c 100644 --- a/src/terminal/point.zig +++ b/src/terminal/point.zig @@ -3,10 +3,12 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const size = @import("size.zig"); -/// The possible reference locations for a point. When someone says "(42, 80)" in the context of a terminal, that could mean multiple -/// things: it is in the current visible viewport? the current active -/// area of the screen where the cursor is? the entire scrollback history? -/// etc. This tag is used to differentiate those cases. +/// The possible reference locations for a point. When someone says "(42, 80)" +/// in the context of a terminal, that could mean multiple things: it is in the +/// current visible viewport? the current active area of the screen where the +/// cursor is? the entire scrollback history? etc. +/// +/// This tag is used to differentiate those cases. pub const Tag = enum { /// Top-left is part of the active area where a running program can /// jump the cursor and make changes. The active area is the "editable" From 58592d3f65aff9d055ce264fac5e50776233aa52 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 19:01:39 -0600 Subject: [PATCH 295/642] GTK: Don't clamp cursorpos, allow negative values Other apprts don't do this, so this should be consistent. --- src/apprt/gtk/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 30a3d28f7..1ee00ff1b 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -1563,7 +1563,7 @@ fn gtkMouseMotion( const scaled = self.scaledCoordinates(x, y); const pos: apprt.CursorPos = .{ - .x = @floatCast(@max(0, scaled.x)), + .x = @floatCast(scaled.x), .y = @floatCast(scaled.y), }; From ecdac8c8c1993b9daaf27745a8ed5583e4ac57d1 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 25 May 2025 19:24:29 -0600 Subject: [PATCH 296/642] terminal: rework selection logic in core surface This logic is cleaner and produces better behavior when selecting by dragging the mouse outside the bounds of the surface, previously when doing this on the left side of the surface selections would include the first cell of the next row, this is no longer the case. This introduces methods on PageList.Pin which move a pin left or right while wrapping to the prev/next row, or clamping to the ends of the row. These need unit tests. --- src/Surface.zig | 274 +++++++++++++++++++------------------- src/terminal/PageList.zig | 68 ++++++++++ 2 files changed, 205 insertions(+), 137 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 32f7487d3..3726c37c7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3678,163 +3678,163 @@ fn dragLeftClickSingle( drag_pin: terminal.Pin, xpos: f64, ) !void { - // NOTE(mitchellh): This logic super sucks. There has to be an easier way - // to calculate this, but this is good for a v1. Selection isn't THAT - // common so its not like this performance heavy code is running that - // often. - // TODO: unit test this, this logic sucks + // TODO: Unit tests for this logic, maybe extract it out to a pure + // function so that it can be tested without mocking state. - // If we were selecting, and we switched directions, then we restart - // calculations because it forces us to reconsider if the first cell is - // selected. - self.checkResetSelSwitch(drag_pin); - - // Our logic for determining if the starting cell is selected: + // Explanation: // - // - The "xboundary" is 60% the width of a cell from the left. We choose - // 60% somewhat arbitrarily based on feeling. - // - If we started our click left of xboundary, backwards selections - // can NEVER select the current char. - // - If we started our click right of xboundary, backwards selections - // ALWAYS selected the current char, but we must move the cursor - // left of the xboundary. - // - Inverted logic for forwards selections. + // # Normal selections // + // ## Left-to-right selections + // - The clicked cell is included if it was clicked to the left of its + // threshold point and the drag location is right of the threshold point. + // - The cell under the cursor (the "drag cell") is included if the drag + // location is right of its threshold point. + // + // ## Right-to-left selections + // - The clicked cell is included if it was clicked to the right of its + // threshold point and the drag location is left of the threshold point. + // - The cell under the cursor (the "drag cell") is included if the drag + // location is left of its threshold point. + // + // # Rectangular selections + // + // Rectangular selections are handled similarly, except that + // entire columns are considered rather than individual cells. - // Our clicking point const click_pin = self.mouse.left_click_pin.?.*; - // the boundary point at which we consider selection or non-selection - const cell_width_f64: f64 = @floatFromInt(self.size.cell.width); - const cell_xboundary = cell_width_f64 * 0.6; + // We only include cells in the selection if the threshold point lies + // between the start and end points of the selection. A threshold of + // 60% of the cell width was chosen empirically because it felt good. + const threshold_point: u32 = @intFromFloat( + @as(f64, @floatFromInt(self.size.cell.width)) * 0.6, + ); - // first xpos of the clicked cell adjusted for padding - const left_padding_f64: f64 = @as(f64, @floatFromInt(self.size.padding.left)); - const cell_xstart = @as(f64, @floatFromInt(click_pin.x)) * cell_width_f64; - const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart - left_padding_f64; + // We use this to clamp the pixel positions below. + const max_x = self.size.grid().columns * self.size.cell.width - 1; - // If this is the same cell, then we only start the selection if weve - // moved past the boundary point the opposite direction from where we - // started. - if (click_pin.eql(drag_pin)) { - // Ensuring to adjusting the cursor position for padding - const cell_xpos = xpos - cell_xstart - left_padding_f64; - const selected: bool = if (cell_start_xpos < cell_xboundary) - cell_xpos >= cell_xboundary - else - cell_xpos < cell_xboundary; + // We need to know how far across in the cell the drag pos is, so + // we subtract the padding and then take it modulo the cell width. + const drag_x = @min( + max_x, + @as(u32, @intFromFloat(@max(0.0, xpos))) -| self.size.padding.left, + ); + const drag_x_frac = drag_x % self.size.cell.width; - try self.setSelection(if (selected) terminal.Selection.init( - drag_pin, - drag_pin, - SurfaceMouse.isRectangleSelectState(self.mouse.mods), - ) else null); + // We figure out the fractional part of the click x position similarly. + // + // NOTE: This click_x position may be incorrect for the current location + // of the click pin, since it's a tracked pin that can move, so we + // should only use this for the fractional position not absolute. + const click_x = @min( + max_x, + @as(u32, @intFromFloat(@max(0.0, self.mouse.left_click_xpos))) -| + self.size.padding.left, + ); + const click_x_frac = click_x % self.size.cell.width; - return; - } + // Whether or not this is a rectangular selection. + const rectangle_selection = + SurfaceMouse.isRectangleSelectState(self.mouse.mods); - // If this is a different cell and we haven't started selection, - // we determine the starting cell first. - if (self.io.terminal.screen.selection == null) { - // - If we're moving to a point before the start, then we select - // the starting cell if we started after the boundary, else - // we start selection of the prior cell. - // - Inverse logic for a point after the start. - const start: terminal.Pin = if (dragLeftClickBefore( - drag_pin, - click_pin, - self.mouse.mods, - )) start: { - if (cell_start_xpos >= cell_xboundary) break :start click_pin; - if (click_pin.x > 0) break :start click_pin.left(1); - var start = click_pin.up(1) orelse click_pin; - start.x = self.io.terminal.screen.pages.cols - 1; - break :start start; - } else start: { - if (cell_start_xpos < cell_xboundary) break :start click_pin; - if (click_pin.x < self.io.terminal.screen.pages.cols - 1) - break :start click_pin.right(1); - var start = click_pin.down(1) orelse click_pin; - start.x = 0; - break :start start; - }; + // Whether the click pin and drag pin are equal. + const same_pin = drag_pin.eql(click_pin); - try self.setSelection(terminal.Selection.init( - start, - drag_pin, - SurfaceMouse.isRectangleSelectState(self.mouse.mods), - )); - return; - } + // Whether or not the end point of our selection is before the start point. + const end_before_start = ebs: { + if (same_pin) { + break :ebs drag_x_frac < click_x_frac; + } - // TODO: detect if selection point is passed the point where we've - // actually written data before and disallow it. - - // We moved! Set the selection end point. The start point should be - // set earlier. - assert(self.io.terminal.screen.selection != null); - const sel = self.io.terminal.screen.selection.?; - try self.setSelection(terminal.Selection.init( - sel.start(), - drag_pin, - sel.rectangle, - )); -} - -// Resets the selection if we switched directions, depending on the select -// mode. See dragLeftClickSingle for more details. -fn checkResetSelSwitch( - self: *Surface, - drag_pin: terminal.Pin, -) void { - const screen = &self.io.terminal.screen; - const sel = screen.selection orelse return; - const sel_start = sel.start(); - const sel_end = sel.end(); - - var reset: bool = false; - if (sel.rectangle) { - // When we're in rectangle mode, we reset the selection relative to - // the click point depending on the selection mode we're in, with - // the exception of single-column selections, which we always reset - // on if we drift. - if (sel_start.x == sel_end.x) { - reset = drag_pin.x != sel_start.x; - } else { - reset = switch (sel.order(screen)) { - .forward => drag_pin.x < sel_start.x or drag_pin.before(sel_start), - .reverse => drag_pin.x > sel_start.x or sel_start.before(drag_pin), - .mirrored_forward => drag_pin.x > sel_start.x or drag_pin.before(sel_start), - .mirrored_reverse => drag_pin.x < sel_start.x or sel_start.before(drag_pin), + // Special handling for rectangular selections, we only use x position. + if (rectangle_selection) { + break :ebs switch (std.math.order(drag_pin.x, click_pin.x)) { + .eq => drag_x_frac < click_x_frac, + .lt => true, + .gt => false, }; } - } else { - // Normal select uses simpler logic that is just based on the - // selection start/end. - reset = if (sel_end.before(sel_start)) - sel_start.before(drag_pin) + + break :ebs drag_pin.before(click_pin); + }; + + // Whether or not the the click pin cell + // should be included in the selection. + const include_click_cell = if (end_before_start) + click_x_frac >= threshold_point + else + click_x_frac < threshold_point; + + // Whether or not the the drag pin cell + // should be included in the selection. + const include_drag_cell = if (end_before_start) + drag_x_frac < threshold_point + else + drag_x_frac >= threshold_point; + + // If the click cell should be included in the selection then it's the + // start, otherwise we get the previous or next cell to it depending on + // the type and direction of the selection. + const start_pin = + if (include_click_cell) + click_pin + else if (end_before_start) + if (rectangle_selection) + click_pin.leftClamp(1) + else + click_pin.leftWrap(1) orelse click_pin + else if (rectangle_selection) + click_pin.rightClamp(1) else - drag_pin.before(sel_start); + click_pin.rightWrap(1) orelse click_pin; + + // Likewise for the end pin with the drag cell. + const end_pin = + if (include_drag_cell) + drag_pin + else if (end_before_start) + if (rectangle_selection) + drag_pin.rightClamp(1) + else + drag_pin.rightWrap(1) orelse drag_pin + else if (rectangle_selection) + drag_pin.leftClamp(1) + else + drag_pin.leftWrap(1) orelse drag_pin; + + // If the click cell is the same as the drag cell and the click cell + // shouldn't be included, or if the cells are adjacent such that the + // start or end pin becomes the other cell, and that cell should not + // be included, then we have no selection, so we set it to null. + // + // If in rectangular selection mode, we compare columns as well. + // + // TODO(qwerasd): this can/should probably be refactored, it's a bit + // repetitive and does excess work in rectangle mode. + if ((!include_click_cell and same_pin) or + (!include_click_cell and rectangle_selection and click_pin.x == drag_pin.x) or + (!include_click_cell and end_pin.eql(click_pin)) or + (!include_click_cell and rectangle_selection and end_pin.x == click_pin.x) or + (!include_drag_cell and start_pin.eql(drag_pin)) or + (!include_drag_cell and rectangle_selection and start_pin.x == drag_pin.x)) + { + try self.setSelection(null); + return; } - // Nullifying a selection can't fail. - if (reset) self.setSelection(null) catch unreachable; -} + // TODO: Clamp selection to the screen area, don't + // let it extend past the last written row. -// Handles how whether or not the drag screen point is before the click point. -// When we are in rectangle select, we only interpret the x axis to determine -// where to start the selection (before or after the click point). See -// dragLeftClickSingle for more details. -fn dragLeftClickBefore( - drag_pin: terminal.Pin, - click_pin: terminal.Pin, - mods: input.Mods, -) bool { - if (mods.ctrlOrSuper() and mods.alt) { - return drag_pin.x < click_pin.x; - } + try self.setSelection( + terminal.Selection.init( + start_pin, + end_pin, + rectangle_selection, + ), + ); - return drag_pin.before(click_pin); + return; } /// Call to notify Ghostty that the color scheme for the terminal has diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index 300af8e13..a0eb3edd1 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -3572,6 +3572,74 @@ pub const Pin = struct { return result; } + /// Move the pin left n columns, stopping at the start of the row. + pub fn leftClamp(self: Pin, n: size.CellCountInt) Pin { + var result = self; + result.x -|= n; + return result; + } + + /// Move the pin right n columns, stopping at the end of the row. + pub fn rightClamp(self: Pin, n: size.CellCountInt) Pin { + var result = self; + result.x = @min(self.x +| n, self.node.data.size.cols - 1); + return result; + } + + /// Move the pin left n cells, wrapping to the previous row as needed. + /// + /// If the offset goes beyond the top of the screen, returns null. + /// + /// TODO: Unit tests. + pub fn leftWrap(self: Pin, n: usize) ?Pin { + // NOTE: This assumes that all pages have the same width, which may + // be violated under certain circumstances by incomplete reflow. + const cols = self.node.data.size.cols; + const remaining_in_row = self.x; + + if (n <= remaining_in_row) return self.left(n); + + const extra_after_remaining = n - remaining_in_row; + + const rows_off = 1 + extra_after_remaining / cols; + + switch (self.upOverflow(rows_off)) { + .offset => |v| { + var result = v; + result.x = @intCast(cols - extra_after_remaining % cols); + return result; + }, + .overflow => return null, + } + } + + /// Move the pin right n cells, wrapping to the next row as needed. + /// + /// If the offset goes beyond the bottom of the screen, returns null. + /// + /// TODO: Unit tests. + pub fn rightWrap(self: Pin, n: usize) ?Pin { + // NOTE: This assumes that all pages have the same width, which may + // be violated under certain circumstances by incomplete reflow. + const cols = self.node.data.size.cols; + const remaining_in_row = cols - self.x - 1; + + if (n <= remaining_in_row) return self.right(n); + + const extra_after_remaining = n - remaining_in_row; + + const rows_off = 1 + extra_after_remaining / cols; + + switch (self.downOverflow(rows_off)) { + .offset => |v| { + var result = v; + result.x = @intCast(extra_after_remaining % cols - 1); + return result; + }, + .overflow => return null, + } + } + /// Move the pin down a certain number of rows, or return null if /// the pin goes beyond the end of the screen. pub fn down(self: Pin, n: usize) ?Pin { From 4d11673318e91d020655b1c4313de96c1084be47 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 12:33:36 -0600 Subject: [PATCH 297/642] unit test mouse selection logic Adds many test cases for expected behavior of the selection logic, this will allow changes to be made more confidently in the future without fear of regressions. --- src/Surface.zig | 1164 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1128 insertions(+), 36 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3726c37c7..ffe39d46d 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3676,11 +3676,29 @@ fn dragLeftClickTriple( fn dragLeftClickSingle( self: *Surface, drag_pin: terminal.Pin, - xpos: f64, + drag_x: f64, ) !void { - // TODO: Unit tests for this logic, maybe extract it out to a pure - // function so that it can be tested without mocking state. + // This logic is in a separate function so that it can be unit tested. + try self.setSelection(mouseSelection( + self.mouse.left_click_pin.?.*, + drag_pin, + @intFromFloat(@max(0.0, self.mouse.left_click_xpos)), + @intFromFloat(@max(0.0, drag_x)), + self.mouse.mods, + self.size, + )); +} +/// Calculates the appropriate selection given pins and pixel x positions for +/// the click point and the drag point, as well as mouse mods and screen size. +fn mouseSelection( + click_pin: terminal.Pin, + drag_pin: terminal.Pin, + click_x: u32, + drag_x: u32, + mods: input.Mods, + size: rendererpkg.Size, +) ?terminal.Selection { // Explanation: // // # Normal selections @@ -3702,41 +3720,25 @@ fn dragLeftClickSingle( // Rectangular selections are handled similarly, except that // entire columns are considered rather than individual cells. - const click_pin = self.mouse.left_click_pin.?.*; - // We only include cells in the selection if the threshold point lies // between the start and end points of the selection. A threshold of // 60% of the cell width was chosen empirically because it felt good. - const threshold_point: u32 = @intFromFloat( - @as(f64, @floatFromInt(self.size.cell.width)) * 0.6, - ); + const threshold_point: u32 = @intFromFloat(@round( + @as(f64, @floatFromInt(size.cell.width)) * 0.6, + )); // We use this to clamp the pixel positions below. - const max_x = self.size.grid().columns * self.size.cell.width - 1; + const max_x = size.grid().columns * size.cell.width - 1; // We need to know how far across in the cell the drag pos is, so // we subtract the padding and then take it modulo the cell width. - const drag_x = @min( - max_x, - @as(u32, @intFromFloat(@max(0.0, xpos))) -| self.size.padding.left, - ); - const drag_x_frac = drag_x % self.size.cell.width; + const drag_x_frac = @min(max_x, drag_x -| size.padding.left) % size.cell.width; // We figure out the fractional part of the click x position similarly. - // - // NOTE: This click_x position may be incorrect for the current location - // of the click pin, since it's a tracked pin that can move, so we - // should only use this for the fractional position not absolute. - const click_x = @min( - max_x, - @as(u32, @intFromFloat(@max(0.0, self.mouse.left_click_xpos))) -| - self.size.padding.left, - ); - const click_x_frac = click_x % self.size.cell.width; + const click_x_frac = @min(max_x, click_x -| size.padding.left) % size.cell.width; // Whether or not this is a rectangular selection. - const rectangle_selection = - SurfaceMouse.isRectangleSelectState(self.mouse.mods); + const rectangle_selection = SurfaceMouse.isRectangleSelectState(mods); // Whether the click pin and drag pin are equal. const same_pin = drag_pin.eql(click_pin); @@ -3819,22 +3821,17 @@ fn dragLeftClickSingle( (!include_drag_cell and start_pin.eql(drag_pin)) or (!include_drag_cell and rectangle_selection and start_pin.x == drag_pin.x)) { - try self.setSelection(null); - return; + return null; } // TODO: Clamp selection to the screen area, don't // let it extend past the last written row. - try self.setSelection( - terminal.Selection.init( - start_pin, - end_pin, - rectangle_selection, - ), + return terminal.Selection.init( + start_pin, + end_pin, + rectangle_selection, ); - - return; } /// Call to notify Ghostty that the color scheme for the terminal has @@ -4819,3 +4816,1098 @@ fn presentSurface(self: *Surface) !void { {}, ); } + +test "Surface: selection logic" { + // Our screen size is 10x5 cells that are + // 10x20 px, with 5px padding on all sides. + const size: rendererpkg.Size = .{ + .cell = .{ .width = 10, .height = 20 }, + .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, + .screen = .{ .width = 110, .height = 110 }, + }; + var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); + defer screen.deinit(); + + // We are testing normal selection logic here so no mods. + const mods: input.Mods = .{}; + + const expectEqual = std.testing.expectEqualDeep; + + // LTR, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, empty selection (single cell on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (single cell on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (between two cells, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // --- RTL + + // RTL, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, empty selection (single cell on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (single cell on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + const drag_pin = click_pin; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (between two cells, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 3 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 3 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // -- Wrapping + + // LTR, wrap excluded cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + const start_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 3 }, + }) orelse unreachable; + const end_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 3 }, + }) orelse unreachable; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, wrap excluded cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + const start_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 3 }, + }) orelse unreachable; + const end_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 3 }, + }) orelse unreachable; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = false, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } +} + +test "Surface: rectangle selection logic" { + // Our screen size is 10x5 cells that are + // 10x20 px, with 5px padding on all sides. + const size: rendererpkg.Size = .{ + .cell = .{ .width = 10, .height = 20 }, + .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, + .screen = .{ .width = 110, .height = 110 }, + }; + var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); + defer screen.deinit(); + + // We hold ctrl and alt so that this test is platform-agnostic. + const mods: input.Mods = .{ + .ctrl = true, + .alt = true, + }; + + try std.testing.expect(SurfaceMouse.isRectangleSelectState(mods)); + + const expectEqual = std.testing.expectEqualDeep; + + // LTR, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At rightmost px in cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin.right(1); + const end_pin = drag_pin.left(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // LTR, empty selection (single column on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (single column on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // LTR, empty selection (between two columns, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // --- RTL + + // RTL, including click and drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including click pin cell but not drag pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin; + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including drag pin cell but not click pin cell + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, including neither click nor drag pin cells + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 5, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At the rightmost px of the cell. + + const start_pin = click_pin.left(1); + const end_pin = drag_pin.right(1); + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, empty selection (single column on only left half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 1; // At px 1 within the cell. + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 1; // At px 0 within the cell. + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (single column on only right half) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 2; // 1px left of right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // RTL, empty selection (between two cells, not crossing threshold) + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 4, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 3, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + try expectEqual( + null, + mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + ), + ); + } + + // -- Wrapping + + // LTR, do not wrap + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } + + // RTL, do not wrap + { + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = 0, .y = 4 }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = 9, .y = 2 }, + }) orelse unreachable; + + const click_x = + @as(u32, click_pin.x) * size.cell.width + size.padding.left + + 0; // At px 0 within the cell + const drag_x = + @as(u32, drag_pin.x) * size.cell.width + size.padding.left + + size.cell.width - 1; // At right edge of cell + + const start_pin = click_pin; + const end_pin = drag_pin; + + try expectEqual(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = true, + }, mouseSelection( + click_pin, + drag_pin, + click_x, + drag_x, + mods, + size, + )); + } +} From 6aa84d0e92ad4b88692a6da8c2834821574b5773 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 26 May 2025 14:31:59 -0600 Subject: [PATCH 298/642] test: introduce helper function for mouse selection tests Removes a lot of repeated code and makes the test cases easier to understand at a glance. --- src/Surface.zig | 1448 +++++++++++++---------------------------------- 1 file changed, 390 insertions(+), 1058 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index ffe39d46d..8b4f58496 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4817,7 +4817,29 @@ fn presentSurface(self: *Surface) !void { ); } -test "Surface: selection logic" { +/// Utility function for the unit tests for mouse selection logic. +/// +/// Tests a click and drag on a 10x5 cell grid, x positions are given in +/// fractional cells, e.g. 3.1 would be 10% through the cell at x = 3. +/// +/// NOTE: The size tested with has 10px wide cells, meaning only one digit +/// after the decimal place has any meaning, e.g. 3.14 is equal to 3.1. +/// +/// The provided start_x/y and end_x/y are the expected start and end points +/// of the resulting selection. +fn testMouseSelection( + click_x: f64, + click_y: u32, + drag_x: f64, + drag_y: u32, + start_x: terminal.size.CellCountInt, + start_y: u32, + end_x: terminal.size.CellCountInt, + end_y: u32, + rect: bool, +) !void { + assert(builtin.is_test); + // Our screen size is 10x5 cells that are // 10x20 px, with 5px padding on all sides. const size: rendererpkg.Size = .{ @@ -4828,1086 +4850,396 @@ test "Surface: selection logic" { var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); defer screen.deinit(); - // We are testing normal selection logic here so no mods. - const mods: input.Mods = .{}; + // We hold both ctrl and alt for rectangular + // select so that this test is platform agnostic. + const mods: input.Mods = .{ + .ctrl = rect, + .alt = rect, + }; - const expectEqual = std.testing.expectEqualDeep; + try std.testing.expectEqual(rect, SurfaceMouse.isRectangleSelectState(mods)); - // LTR, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(click_x)), .y = click_y }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(drag_x)), .y = drag_y }, + }) orelse unreachable; - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. + const cell_width_f64: f64 = @floatFromInt(size.cell.width); + const click_x_pos: u32 = + @as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) + + size.padding.left; + const drag_x_pos: u32 = + @as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) + + size.padding.left; - const start_pin = click_pin; - const end_pin = drag_pin; + const start_pin = screen.pages.pin(.{ + .viewport = .{ .x = start_x, .y = start_y }, + }) orelse unreachable; + const end_pin = screen.pages.pin(.{ + .viewport = .{ .x = end_x, .y = end_y }, + }) orelse unreachable; - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( + try std.testing.expectEqualDeep(terminal.Selection{ + .bounds = .{ .untracked = .{ + .start = start_pin, + .end = end_pin, + } }, + .rectangle = rect, + }, mouseSelection( + click_pin, + drag_pin, + click_x_pos, + drag_x_pos, + mods, + size, + )); +} + +/// Like `testMouseSelection` but checks that the resulting selection is null. +/// +/// See `testMouseSelection` for more details. +fn testMouseSelectionIsNull( + click_x: f64, + click_y: u32, + drag_x: f64, + drag_y: u32, + rect: bool, +) !void { + assert(builtin.is_test); + + // Our screen size is 10x5 cells that are + // 10x20 px, with 5px padding on all sides. + const size: rendererpkg.Size = .{ + .cell = .{ .width = 10, .height = 20 }, + .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, + .screen = .{ .width = 110, .height = 110 }, + }; + var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); + defer screen.deinit(); + + // We hold both ctrl and alt for rectangular + // select so that this test is platform agnostic. + const mods: input.Mods = .{ + .ctrl = rect, + .alt = rect, + }; + + try std.testing.expectEqual(rect, SurfaceMouse.isRectangleSelectState(mods)); + + const click_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(click_x)), .y = click_y }, + }) orelse unreachable; + const drag_pin = screen.pages.pin(.{ + .viewport = .{ .x = @intFromFloat(@floor(drag_x)), .y = drag_y }, + }) orelse unreachable; + + const cell_width_f64: f64 = @floatFromInt(size.cell.width); + const click_x_pos: u32 = + @as(u32, @intFromFloat(@floor(click_x * cell_width_f64))) + + size.padding.left; + const drag_x_pos: u32 = + @as(u32, @intFromFloat(@floor(drag_x * cell_width_f64))) + + size.padding.left; + + try std.testing.expectEqual( + null, + mouseSelection( click_pin, drag_pin, - click_x, - drag_x, + click_x_pos, + drag_x_pos, mods, size, - )); - } + ), + ); +} - // LTR, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; +test "Surface: selection logic" { + // We disable format to make these easier to + // read by pairing sets of coordinates per line. + // zig fmt: off - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. + // -- LTR + // single cell selection + try testMouseSelection( + 3.0, 3, // click + 3.9, 3, // drag + 3, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including click and drag pin cells + try testMouseSelection( + 3.0, 3, // click + 5.9, 3, // drag + 3, 3, // expected start + 5, 3, // expected end + false, // regular selection + ); + // including click pin cell but not drag pin cell + try testMouseSelection( + 3.0, 3, // click + 5.0, 3, // drag + 3, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // including drag pin cell but not click pin cell + try testMouseSelection( + 3.9, 3, // click + 5.9, 3, // drag + 4, 3, // expected start + 5, 3, // expected end + false, // regular selection + ); + // including neither click nor drag pin cells + try testMouseSelection( + 3.9, 3, // click + 5.0, 3, // drag + 4, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // empty selection (single cell on only left half) + try testMouseSelectionIsNull( + 3.0, 3, // click + 3.1, 3, // drag + false, // regular selection + ); + // empty selection (single cell on only right half) + try testMouseSelectionIsNull( + 3.8, 3, // click + 3.9, 3, // drag + false, // regular selection + ); + // empty selection (between two cells, not crossing threshold) + try testMouseSelectionIsNull( + 3.9, 3, // click + 4.0, 3, // drag + false, // regular selection + ); - const start_pin = click_pin; - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, empty selection (single cell on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (single cell on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (between two cells, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // --- RTL - - // RTL, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin; - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, empty selection (single cell on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (single cell on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - const drag_pin = click_pin; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (between two cells, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 3 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 3 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } + // -- RTL + // single cell selection + try testMouseSelection( + 3.9, 3, // click + 3.0, 3, // drag + 3, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including click and drag pin cells + try testMouseSelection( + 5.9, 3, // click + 3.0, 3, // drag + 5, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including click pin cell but not drag pin cell + try testMouseSelection( + 5.9, 3, // click + 3.9, 3, // drag + 5, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // including drag pin cell but not click pin cell + try testMouseSelection( + 5.0, 3, // click + 3.0, 3, // drag + 4, 3, // expected start + 3, 3, // expected end + false, // regular selection + ); + // including neither click nor drag pin cells + try testMouseSelection( + 5.0, 3, // click + 3.9, 3, // drag + 4, 3, // expected start + 4, 3, // expected end + false, // regular selection + ); + // empty selection (single cell on only left half) + try testMouseSelectionIsNull( + 3.1, 3, // click + 3.0, 3, // drag + false, // regular selection + ); + // empty selection (single cell on only right half) + try testMouseSelectionIsNull( + 3.9, 3, // click + 3.8, 3, // drag + false, // regular selection + ); + // empty selection (between two cells, not crossing threshold) + try testMouseSelectionIsNull( + 4.0, 3, // click + 3.9, 3, // drag + false, // regular selection + ); // -- Wrapping - // LTR, wrap excluded cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - const start_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 3 }, - }) orelse unreachable; - const end_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 3 }, - }) orelse unreachable; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - + try testMouseSelection( + 9.9, 2, // click + 0.0, 4, // drag + 0, 3, // expected start + 9, 3, // expected end + false, // regular selection + ); // RTL, wrap excluded cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - const start_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 3 }, - }) orelse unreachable; - const end_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 3 }, - }) orelse unreachable; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = false, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } + try testMouseSelection( + 0.0, 4, // click + 9.9, 2, // drag + 9, 3, // expected start + 0, 3, // expected end + false, // regular selection + ); } test "Surface: rectangle selection logic" { - // Our screen size is 10x5 cells that are - // 10x20 px, with 5px padding on all sides. - const size: rendererpkg.Size = .{ - .cell = .{ .width = 10, .height = 20 }, - .padding = .{ .left = 5, .top = 5, .right = 5, .bottom = 5 }, - .screen = .{ .width = 110, .height = 110 }, - }; - var screen = try terminal.Screen.init(std.testing.allocator, 10, 5, 0); - defer screen.deinit(); + // We disable format to make these easier to + // read by pairing sets of coordinates per line. + // zig fmt: off - // We hold ctrl and alt so that this test is platform-agnostic. - const mods: input.Mods = .{ - .ctrl = true, - .alt = true, - }; + // -- LTR + // single column selection + try testMouseSelection( + 3.0, 2, // click + 3.9, 4, // drag + 3, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including click and drag pin columns + try testMouseSelection( + 3.0, 2, // click + 5.9, 4, // drag + 3, 2, // expected start + 5, 4, // expected end + true, //rectangle selection + ); + // including click pin column but not drag pin column + try testMouseSelection( + 3.0, 2, // click + 5.0, 4, // drag + 3, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // including drag pin column but not click pin column + try testMouseSelection( + 3.9, 2, // click + 5.9, 4, // drag + 4, 2, // expected start + 5, 4, // expected end + true, //rectangle selection + ); + // including neither click nor drag pin columns + try testMouseSelection( + 3.9, 2, // click + 5.0, 4, // drag + 4, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // empty selection (single column on only left half) + try testMouseSelectionIsNull( + 3.0, 2, // click + 3.1, 4, // drag + true, //rectangle selection + ); + // empty selection (single column on only right half) + try testMouseSelectionIsNull( + 3.8, 2, // click + 3.9, 4, // drag + true, //rectangle selection + ); + // empty selection (between two columns, not crossing threshold) + try testMouseSelectionIsNull( + 3.9, 2, // click + 4.0, 4, // drag + true, //rectangle selection + ); - try std.testing.expect(SurfaceMouse.isRectangleSelectState(mods)); - - const expectEqual = std.testing.expectEqualDeep; - - // LTR, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin; - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At rightmost px in cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin.right(1); - const end_pin = drag_pin.left(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // LTR, empty selection (single column on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (single column on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // LTR, empty selection (between two columns, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // --- RTL - - // RTL, including click and drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including click pin cell but not drag pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin; - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including drag pin cell but not click pin cell - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, including neither click nor drag pin cells - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 5, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At the rightmost px of the cell. - - const start_pin = click_pin.left(1); - const end_pin = drag_pin.right(1); - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - - // RTL, empty selection (single column on only left half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 1; // At px 1 within the cell. - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 1; // At px 0 within the cell. - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (single column on only right half) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 2; // 1px left of right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } - - // RTL, empty selection (between two cells, not crossing threshold) - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 4, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 3, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - try expectEqual( - null, - mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - ), - ); - } + // -- RTL + // single column selection + try testMouseSelection( + 3.9, 2, // click + 3.0, 4, // drag + 3, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including click and drag pin columns + try testMouseSelection( + 5.9, 2, // click + 3.0, 4, // drag + 5, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including click pin column but not drag pin column + try testMouseSelection( + 5.9, 2, // click + 3.9, 4, // drag + 5, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // including drag pin column but not click pin column + try testMouseSelection( + 5.0, 2, // click + 3.0, 4, // drag + 4, 2, // expected start + 3, 4, // expected end + true, //rectangle selection + ); + // including neither click nor drag pin columns + try testMouseSelection( + 5.0, 2, // click + 3.9, 4, // drag + 4, 2, // expected start + 4, 4, // expected end + true, //rectangle selection + ); + // empty selection (single column on only left half) + try testMouseSelectionIsNull( + 3.1, 2, // click + 3.0, 4, // drag + true, //rectangle selection + ); + // empty selection (single column on only right half) + try testMouseSelectionIsNull( + 3.9, 2, // click + 3.8, 4, // drag + true, //rectangle selection + ); + // empty selection (between two columns, not crossing threshold) + try testMouseSelectionIsNull( + 4.0, 2, // click + 3.9, 4, // drag + true, //rectangle selection + ); // -- Wrapping - // LTR, do not wrap - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } - + try testMouseSelection( + 9.9, 2, // click + 0.0, 4, // drag + 9, 2, // expected start + 0, 4, // expected end + true, //rectangle selection + ); // RTL, do not wrap - { - const click_pin = screen.pages.pin(.{ - .viewport = .{ .x = 0, .y = 4 }, - }) orelse unreachable; - const drag_pin = screen.pages.pin(.{ - .viewport = .{ .x = 9, .y = 2 }, - }) orelse unreachable; - - const click_x = - @as(u32, click_pin.x) * size.cell.width + size.padding.left + - 0; // At px 0 within the cell - const drag_x = - @as(u32, drag_pin.x) * size.cell.width + size.padding.left + - size.cell.width - 1; // At right edge of cell - - const start_pin = click_pin; - const end_pin = drag_pin; - - try expectEqual(terminal.Selection{ - .bounds = .{ .untracked = .{ - .start = start_pin, - .end = end_pin, - } }, - .rectangle = true, - }, mouseSelection( - click_pin, - drag_pin, - click_x, - drag_x, - mods, - size, - )); - } + try testMouseSelection( + 0.0, 4, // click + 9.9, 2, // drag + 0, 4, // expected start + 9, 2, // expected end + true, //rectangle selection + ); } From ba02f0ae22b06fa7e0f1a5b7b38f073c935b8c1e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 27 May 2025 09:45:31 -0700 Subject: [PATCH 299/642] decl literal --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 8b4f58496..0a2885dff 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3827,7 +3827,7 @@ fn mouseSelection( // TODO: Clamp selection to the screen area, don't // let it extend past the last written row. - return terminal.Selection.init( + return .init( start_pin, end_pin, rectangle_selection, From 21c97aa9d64769061351df82eee7e0b7a27de71e Mon Sep 17 00:00:00 2001 From: Jonatan Borkowski Date: Sun, 25 May 2025 22:22:07 +0200 Subject: [PATCH 300/642] add support for buffer switching with CSI ? 47 h/l --- src/terminal/modes.zig | 1 + src/termio/stream_handler.zig | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index 60ecc7698..b36266b32 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -206,6 +206,7 @@ const entries: []const ModeEntry = &.{ .{ .name = "cursor_visible", .value = 25, .default = true }, .{ .name = "enable_mode_3", .value = 40 }, .{ .name = "reverse_wrap", .value = 45 }, + .{ .name = "alt_screen_legacy", .value = 47 }, .{ .name = "keypad_keys", .value = 66 }, .{ .name = "enable_left_and_right_margin", .value = 69 }, .{ .name = "mouse_event_normal", .value = 1000 }, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 299c7cd45..ffd00e14d 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -582,6 +582,16 @@ pub const StreamHandler = struct { self.terminal.scrolling_region.right = self.terminal.cols - 1; }, + .alt_screen_legacy => { + if (enabled) + self.terminal.alternateScreen(.{}) + else + self.terminal.primaryScreen(.{}); + + // Schedule a render since we changed screens + try self.queueRender(); + }, + .alt_screen => { const opts: terminal.Terminal.AlternateScreenOptions = .{ .cursor_save = false, From 6f7e9d5bea9840627888a6c3c9e89056680ec721 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 27 May 2025 21:55:28 -0600 Subject: [PATCH 301/642] code style: use `@splat` where possible As of Zig 0.14.0, `@splat` can be used for array types, which eliminates a lot of redundant syntax and makes things generally cleaner. I've explicitly avoided applying this change in the renderer files for now since it would just create rebasing conflicts in my renderer rework branch which I'll be PR-ing pretty soon. --- src/Surface.zig | 2 +- src/datastruct/cache_table.zig | 2 +- src/font/SharedGridSet.zig | 2 +- src/font/sprite/Box.zig | 2 +- src/terminal/Tabstops.zig | 2 +- src/terminal/Terminal.zig | 2 +- src/terminal/kitty/key.zig | 4 ++-- src/terminal/ref_counted_set.zig | 6 +++--- src/terminfo/Source.zig | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0a2885dff..01639964b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -160,7 +160,7 @@ pub const InputEffect = enum { /// Mouse state for the surface. const Mouse = struct { /// The last tracked mouse button state by button. - click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max, + click_state: [input.MouseButton.max]input.MouseButtonState = @splat(.release), /// The last mods state when the last mouse button (whatever it was) was /// pressed or release. diff --git a/src/datastruct/cache_table.zig b/src/datastruct/cache_table.zig index 40d36cc24..fbfb30d71 100644 --- a/src/datastruct/cache_table.zig +++ b/src/datastruct/cache_table.zig @@ -70,7 +70,7 @@ pub fn CacheTable( /// become a pointless check, but hopefully branch prediction picks /// up on it at that point. The memory cost isn't too bad since it's /// just bytes, so should be a fraction the size of the main table. - lengths: [bucket_count]u8 = [_]u8{0} ** bucket_count, + lengths: [bucket_count]u8 = @splat(0), /// An instance of the context structure. /// Must be initialized before calling any operations. diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 858d7930f..e3e61907b 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -498,7 +498,7 @@ pub const Key = struct { /// each style. For example, bold is from /// offsets[@intFromEnum(.bold) - 1] to /// offsets[@intFromEnum(.bold)]. - style_offsets: StyleOffsets = .{0} ** style_offsets_len, + style_offsets: StyleOffsets = @splat(0), /// The codepoint map configuration. codepoint_map: CodepointMap = .{}, diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index b1ebfe3a9..f3942b83d 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -2517,7 +2517,7 @@ fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { const octants: [octants_len]Octant = comptime octants: { @setEvalBranchQuota(10_000); - var result: [octants_len]Octant = .{Octant{}} ** octants_len; + var result: [octants_len]Octant = @splat(.{}); var i: usize = 0; const data = @embedFile("octants.txt"); diff --git a/src/terminal/Tabstops.zig b/src/terminal/Tabstops.zig index 5a54fb28b..4ab5133d9 100644 --- a/src/terminal/Tabstops.zig +++ b/src/terminal/Tabstops.zig @@ -44,7 +44,7 @@ const masks = blk: { cols: usize = 0, /// Preallocated tab stops. -prealloc_stops: [prealloc_count]Unit = [1]Unit{0} ** prealloc_count, +prealloc_stops: [prealloc_count]Unit = @splat(0), /// Dynamically expanded stops above prealloc stops. dynamic_stops: []Unit = &[0]Unit{}, diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index 595fee1ba..bb6702201 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2329,7 +2329,7 @@ pub fn printAttributes(self: *Terminal, buf: []u8) ![]const u8 { try writer.writeByte('0'); const pen = self.screen.cursor.style; - var attrs = [_]u8{0} ** 8; + var attrs: [8]u8 = @splat(0); var i: usize = 0; if (pen.flags.bold) { diff --git a/src/terminal/kitty/key.zig b/src/terminal/kitty/key.zig index 8bafcb7dc..0883c90f2 100644 --- a/src/terminal/kitty/key.zig +++ b/src/terminal/kitty/key.zig @@ -8,7 +8,7 @@ const std = @import("std"); pub const FlagStack = struct { const len = 8; - flags: [len]Flags = .{Flags{}} ** len, + flags: [len]Flags = @splat(.{}), idx: u3 = 0, /// Return the current stack value @@ -51,7 +51,7 @@ pub const FlagStack = struct { // could send a huge number of pop commands to waste cpu. if (n >= self.flags.len) { self.idx = 0; - self.flags = .{Flags{}} ** len; + self.flags = @splat(.{}); return; } diff --git a/src/terminal/ref_counted_set.zig b/src/terminal/ref_counted_set.zig index 8023461f3..153e331a6 100644 --- a/src/terminal/ref_counted_set.zig +++ b/src/terminal/ref_counted_set.zig @@ -115,7 +115,7 @@ pub fn RefCountedSet( /// input. We handle this gracefully by returning an error /// anywhere where we're about to insert if there's any /// item with a PSL in the last slot of the stats array. - psl_stats: [32]Id = [_]Id{0} ** 32, + psl_stats: [32]Id = @splat(0), /// The backing store of items items: Offset(Item), @@ -663,7 +663,7 @@ pub fn RefCountedSet( const table = self.table.ptr(base); const items = self.items.ptr(base); - var psl_stats: [32]Id = [_]Id{0} ** 32; + var psl_stats: [32]Id = @splat(0); for (items[0..self.layout.cap], 0..) |item, id| { if (item.meta.bucket < std.math.maxInt(Id)) { @@ -676,7 +676,7 @@ pub fn RefCountedSet( assert(std.mem.eql(Id, &psl_stats, &self.psl_stats)); - psl_stats = [_]Id{0} ** 32; + psl_stats = @splat(0); for (table[0..self.layout.table_cap], 0..) |id, bucket| { const item = items[id]; diff --git a/src/terminfo/Source.zig b/src/terminfo/Source.zig index 8ffd9cabb..7692e6f54 100644 --- a/src/terminfo/Source.zig +++ b/src/terminfo/Source.zig @@ -74,7 +74,7 @@ pub fn xtgettcapMap(comptime self: Source) std.StaticStringMap([]const u8) { // We have all of our capabilities plus To, TN, and RGB which aren't // in the capabilities list but are query-able. const len = self.capabilities.len + 3; - var kvs: [len]KV = .{.{ "", "" }} ** len; + var kvs: [len]KV = @splat(.{ "", "" }); // We first build all of our entries with raw K=V pairs. kvs[0] = .{ "TN", self.names[0] }; From d1501a492530b99c00a11fdd58d5ec411ac7c937 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 27 May 2025 22:15:43 -0700 Subject: [PATCH 302/642] fix: properly intialize key event in GlobalEventTap --- .../Sources/Features/Global Keybinds/GlobalEventTap.swift | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index 935c2fb03..644285c9a 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -141,12 +141,7 @@ fileprivate func cgEventFlagsChangedHandler( guard let event: NSEvent = .init(cgEvent: cgEvent) else { return result } // Build our event input and call ghostty - var key_ev = ghostty_input_key_s() - key_ev.action = GHOSTTY_ACTION_PRESS - key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags) - key_ev.keycode = UInt32(event.keyCode) - key_ev.text = nil - key_ev.composing = false + var key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) if (ghostty_app_key(ghostty, key_ev)) { GlobalEventTap.logger.info("global key event handled event=\(event)") return nil From 9c1abf487e0f05cebc4f3e932e1a5a8cdb5aa6a6 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 17:15:40 -0500 Subject: [PATCH 303/642] OSC: start adding structure to allow multiple color operations per OSC --- src/terminal/osc.zig | 418 ++++++++++++++++++++++++++++------ src/terminal/stream.zig | 7 + src/termio/stream_handler.zig | 175 ++++++++++++++ 3 files changed, 526 insertions(+), 74 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 932964137..7729eaa6b 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -109,6 +109,13 @@ pub const Command = union(enum) { value: []const u8, }, + /// OSC color operations + color_operation: struct { + source: ColorOperationSource, + operations: std.ArrayListUnmanaged(ColorOperation) = .empty, + terminator: Terminator = .st, + }, + /// OSC 4, OSC 10, and OSC 11 color report. report_color: struct { /// OSC 4 requests a palette color, OSC 10 requests the foreground @@ -182,6 +189,32 @@ pub const Command = union(enum) { /// Wait input (OSC 9;5) wait_input: void, + pub const ColorOperationSource = enum(u16) { + osc_4 = 4, + osc_10 = 10, + osc_11 = 11, + osc_12 = 12, + osc_104 = 104, + + pub fn format( + self: ColorOperationSource, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.print("{d}", .{@intFromEnum(self)}); + } + }; + + pub const ColorOperation = union(enum) { + set: struct { + kind: ColorKind, + color: RGB, + }, + reset: ColorKind, + report: ColorKind, + }; + pub const ColorKind = union(enum) { palette: u8, foreground, @@ -234,6 +267,15 @@ pub const Terminator = enum { .bel => "\x07", }; } + + pub fn format( + self: Terminator, + comptime _: []const u8, + _: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try writer.writeAll(self.string()); + } }; pub const Parser = struct { @@ -288,6 +330,7 @@ pub const Parser = struct { @"0", @"1", @"10", + @"104", @"11", @"12", @"13", @@ -327,17 +370,16 @@ pub const Parser = struct { clipboard_kind_end, // Get/set color palette index - color_palette_index, - color_palette_index_end, + osc_4, + + // Reset color palette index + osc_104, // Hyperlinks hyperlink_param_key, hyperlink_param_value, hyperlink_uri, - // Reset color palette index - reset_color_palette_index, - // rxvt extension. Only used for OSC 777 and only the value "notify" is // supported rxvt_extension, @@ -423,6 +465,10 @@ pub const Parser = struct { v.list.deinit(); self.command = default; }, + .color_operation => |*v| { + v.operations.deinit(self.alloc.?); + self.command = default; + }, else => {}, } } @@ -503,18 +549,26 @@ pub const Parser = struct { .@"10" => switch (c) { ';' => self.state = .query_fg_color, - '4' => { - self.command = .{ .reset_color = .{ - .kind = .{ .palette = 0 }, - .value = "", - } }; + '4' => self.state = .@"104", + else => self.state = .invalid, + }, - self.state = .reset_color_palette_index; + .@"104" => switch (c) { + ';' => osc_104: { + if (self.alloc == null) { + log.info("OSC 104 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_104; + } + self.state = .osc_104; + self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, + .osc_104 => {}, + .@"11" => switch (c) { ';' => self.state = .query_bg_color, '0' => { @@ -621,65 +675,20 @@ pub const Parser = struct { }, .@"4" => switch (c) { - ';' => { - self.state = .color_palette_index; - self.buf_start = self.buf_idx; - }, - else => self.state = .invalid, - }, - - .color_palette_index => switch (c) { - '0'...'9' => {}, - ';' => blk: { - const str = self.buf[self.buf_start .. self.buf_idx - 1]; - if (str.len == 0) { + ';' => osc_4: { + if (self.alloc == null) { + log.info("OSC 4 requires an allocator, but none was provided", .{}); self.state = .invalid; - break :blk; + break :osc_4; } - - if (std.fmt.parseUnsigned(u8, str, 10)) |num| { - self.state = .color_palette_index_end; - self.temp_state = .{ .num = num }; - } else |err| switch (err) { - error.Overflow => self.state = .invalid, - error.InvalidCharacter => unreachable, - } - }, - else => self.state = .invalid, - }, - - .color_palette_index_end => switch (c) { - '?' => { - self.command = .{ .report_color = .{ - .kind = .{ .palette = @intCast(self.temp_state.num) }, - } }; - + self.state = .osc_4; + self.buf_start = self.buf_idx; self.complete = true; }, - else => { - self.command = .{ .set_color = .{ - .kind = .{ .palette = @intCast(self.temp_state.num) }, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, + else => self.state = .invalid, }, - .reset_color_palette_index => switch (c) { - ';' => { - self.state = .string; - self.temp_state = .{ .str = &self.command.reset_color.value }; - self.buf_start = self.buf_idx; - self.complete = false; - }, - else => { - self.state = .invalid; - self.complete = false; - }, - }, + .osc_4 => {}, .@"5" => switch (c) { '2' => self.state = .@"52", @@ -1327,6 +1336,104 @@ pub const Parser = struct { self.temp_state.str.* = list.items; } + fn parseOSC4(self: *Parser) void { + assert(self.state == .osc_4); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = .osc_4, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { + log.warn("unable to allocate memory for OSC 4 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + + const str = self.buf[self.buf_start..self.buf_idx]; + var it = std.mem.splitScalar(u8, str, ';'); + while (it.next()) |index_str| { + const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => { + log.warn("invalid palette index spec in OSC 4: {s}", .{index_str}); + // skip any spec + _ = it.next(); + continue; + }, + }; + const spec_str = it.next() orelse continue; + if (std.mem.eql(u8, spec_str, "?")) { + self.command.color_operation.operations.append( + alloc, + .{ + .report = .{ .palette = index }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } else { + const color = RGB.parse(spec_str) catch |err| { + log.warn("invalid color specification {s} in OSC 4: {}", .{ spec_str, err }); + continue; + }; + self.command.color_operation.operations.append( + alloc, + .{ + .set = .{ + .kind = .{ + .palette = index, + }, + .color = color, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + } + } + + fn parseOSC104(self: *Parser) void { + assert(self.state == .osc_104); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = .osc_104, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { + log.warn("unable to allocate memory for OSC 104 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + + const str = self.buf[self.buf_start..self.buf_idx]; + var it = std.mem.splitScalar(u8, str, ';'); + while (it.next()) |index_str| { + const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => { + log.warn("invalid palette index spec in OSC 104: {s}", .{index_str}); + continue; + }, + }; + self.command.color_operation.operations.append( + alloc, + .{ + .reset = .{ .palette = index }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + } + /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine @@ -1350,12 +1457,15 @@ pub const Parser = struct { .allocable_string => self.endAllocableString(), .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), + .osc_4 => self.parseOSC4(), + .osc_104 => self.parseOSC104(), else => {}, } switch (self.command) { .report_color => |*c| c.terminator = .init(terminator_ch), .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), + .color_operation => |*c| c.terminator = .init(terminator_ch), else => {}, } @@ -1729,32 +1839,192 @@ test "OSC: set background color" { try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); } -test "OSC: get palette color" { +test "OSC: OSC4: get palette color 1" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "4;1;?"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, cmd.report_color.kind); - try testing.expectEqual(cmd.report_color.terminator, .st); + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); + try testing.expectEqual(cmd.color_operation.terminator, .st); } -test "OSC: set palette color" { +test "OSC: OSC4: get palette color 2" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;1;?;2;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 2 }, op.report); + } + try testing.expectEqual(cmd.color_operation.terminator, .st); +} + +test "OSC: OSC4: set palette color 1" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "4;17;rgb:aa/bb/cc"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, cmd.set_color.kind); - try testing.expectEqualStrings(cmd.set_color.value, "rgb:aa/bb/cc"); + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); +} + +test "OSC: OSC4: set palette color 2" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;17;rgb:aa/bb/cc;1;rgb:00/11/22"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0x00, .g = 0x11, .b = 0x22 }, + op.set.color, + ); + } +} + +test "OSC: OSC4: mix get/set palette color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;17;rgb:aa/bb/cc;254;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .report); + try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); + } +} + +test "OSC: OSC104: reset palette color 1" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;17"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + } +} + +test "OSC: OSC104: reset palette color 2" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;17;111"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 2); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + } + { + const op = cmd.color_operation.operations.items[1]; + try testing.expect(op == .reset); + try testing.expectEqual(Command.ColorKind{ .palette = 111 }, op.reset); + } } test "OSC: conemu sleep" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 76fa6c129..2a1ae80c9 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1555,6 +1555,13 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, + .color_operation => |v| { + if (@hasDecl(T, "handleColorOperation")) { + try self.handler.handleColorOperation(v.source, v.operations.items, v.terminator); + return; + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + .report_color => |v| { if (@hasDecl(T, "reportColor")) { try self.handler.reportColor(v.kind, v.terminator); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ffd00e14d..57a1eeacf 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1195,6 +1195,181 @@ pub const StreamHandler = struct { } } + pub fn handleColorOperation( + self: *StreamHandler, + source: terminal.osc.Command.ColorOperationSource, + operations: []terminal.osc.Command.ColorOperation, + terminator: terminal.osc.Terminator, + ) !void { + var buffer: [1024]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buffer); + const alloc = fba.allocator(); + + var response: std.ArrayListUnmanaged(u8) = .empty; + const writer = response.writer(alloc); + + var report: bool = false; + + try writer.print("\x1b]{}", .{source}); + + for (operations) |op| { + switch (op) { + .set => |set| { + switch (set.kind) { + .palette => |i| { + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = set.color; + self.terminal.color_palette.mask.set(i); + }, + .foreground => { + self.foreground_color = set.color; + _ = self.renderer_mailbox.push(.{ + .foreground_color = set.color, + }, .{ .forever = {} }); + }, + .background => { + self.background_color = set.color; + _ = self.renderer_mailbox.push(.{ + .background_color = set.color, + }, .{ .forever = {} }); + }, + .cursor => { + self.cursor_color = set.color; + _ = self.renderer_mailbox.push(.{ + .cursor_color = set.color, + }, .{ .forever = {} }); + }, + } + + // Notify the surface of the color change + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = set.kind, + .color = set.color, + } }); + }, + + .reset => |kind| { + switch (kind) { + .palette => |i| { + const mask = &self.terminal.color_palette.mask; + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + mask.unset(i); + + self.surfaceMessageWriter(.{ + .color_change = .{ + .kind = .{ .palette = @intCast(i) }, + .color = self.terminal.color_palette.colors[i], + }, + }); + }, + .foreground => { + self.foreground_color = null; + _ = self.renderer_mailbox.push(.{ + .foreground_color = self.foreground_color, + }, .{ .forever = {} }); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .foreground, + .color = self.default_foreground_color, + } }); + }, + .background => { + self.background_color = null; + _ = self.renderer_mailbox.push(.{ + .background_color = self.background_color, + }, .{ .forever = {} }); + + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .background, + .color = self.default_background_color, + } }); + }, + .cursor => { + self.cursor_color = null; + + _ = self.renderer_mailbox.push(.{ + .cursor_color = self.cursor_color, + }, .{ .forever = {} }); + + if (self.default_cursor_color) |color| { + self.surfaceMessageWriter(.{ .color_change = .{ + .kind = .cursor, + .color = color, + } }); + } + }, + } + }, + + .report => |kind| report: { + if (self.osc_color_report_format == .none) break :report; + + report = true; + + const color = switch (kind) { + .palette => |i| self.terminal.color_palette.colors[i], + .foreground => self.foreground_color orelse self.default_foreground_color, + .background => self.background_color orelse self.default_background_color, + .cursor => self.cursor_color orelse + self.default_cursor_color orelse + self.foreground_color orelse + self.default_foreground_color, + }; + + switch (self.osc_color_report_format) { + .@"16-bit" => switch (kind) { + .palette => |i| try writer.print( + ";{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .{ + i, + @as(u16, color.r) * 257, + @as(u16, color.g) * 257, + @as(u16, color.b) * 257, + }, + ), + else => try writer.print( + ";rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .{ + @as(u16, color.r) * 257, + @as(u16, color.g) * 257, + @as(u16, color.b) * 257, + }, + ), + }, + + .@"8-bit" => switch (kind) { + .palette => |i| try writer.print( + ";{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + i, + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), + }, + ), + else => try writer.print( + ";rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .{ + @as(u16, color.r), + @as(u16, color.g), + @as(u16, color.b), + }, + ), + }, + + .none => unreachable, + } + }, + } + } + if (report) { + try writer.writeAll(terminator.string()); + const msg: termio.Message = .{ .write_stable = response.items }; + self.messageWriter(msg); + } + } + /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, /// default foreground color, and background color respectively. pub fn reportColor( From 5ec1c15ecfe5e2a29f4e0c9cbe116aeed7ad62d5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 18:05:59 -0500 Subject: [PATCH 304/642] OSC: add more tests --- src/terminal/osc.zig | 170 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 153 insertions(+), 17 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7729eaa6b..778196160 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1357,8 +1357,8 @@ pub const Parser = struct { while (it.next()) |index_str| { const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { error.Overflow, error.InvalidCharacter => { - log.warn("invalid palette index spec in OSC 4: {s}", .{index_str}); - // skip any spec + log.warn("invalid color palette index in OSC 4: {s} {}", .{ index_str, err }); + // skip any color spec _ = it.next(); continue; }, @@ -1376,7 +1376,7 @@ pub const Parser = struct { }; } else { const color = RGB.parse(spec_str) catch |err| { - log.warn("invalid color specification {s} in OSC 4: {}", .{ spec_str, err }); + log.warn("invalid color specification in OSC 4: {s} {}", .{ spec_str, err }); continue; }; self.command.color_operation.operations.append( @@ -1418,7 +1418,7 @@ pub const Parser = struct { while (it.next()) |index_str| { const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { error.Overflow, error.InvalidCharacter => { - log.warn("invalid palette index spec in OSC 104: {s}", .{index_str}); + log.warn("invalid color palette index in OSC 104: {s} {}", .{ index_str, err }); continue; }, }; @@ -1854,10 +1854,15 @@ test "OSC: OSC4: get palette color 1" { try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); try testing.expect(cmd.color_operation.operations.items.len == 1); - const op = cmd.color_operation.operations.items[0]; - try testing.expect(op == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); - try testing.expectEqual(cmd.color_operation.terminator, .st); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + try testing.expectEqual(cmd.color_operation.terminator, .st); + } } test "OSC: OSC4: get palette color 2" { @@ -1878,12 +1883,18 @@ test "OSC: OSC4: get palette color 2" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); } { const op = cmd.color_operation.operations.items[1]; try testing.expect(op == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 2 }, op.report); + try testing.expectEqual( + Command.ColorKind{ .palette = 2 }, + op.report, + ); } try testing.expectEqual(cmd.color_operation.terminator, .st); } @@ -1905,7 +1916,10 @@ test "OSC: OSC4: set palette color 1" { try testing.expect(cmd.color_operation.operations.items.len == 1); const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, op.set.color, @@ -1930,7 +1944,10 @@ test "OSC: OSC4: set palette color 2" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, op.set.color, @@ -1939,7 +1956,10 @@ test "OSC: OSC4: set palette color 2" { { const op = cmd.color_operation.operations.items[1]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 1 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0x00, .g = 0x11, .b = 0x22 }, op.set.color, @@ -1947,6 +1967,60 @@ test "OSC: OSC4: set palette color 2" { } } +test "OSC: OSC4: get with invalid index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;1111;?;1;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + } +} + +test "OSC: OSC4: set with invalid index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;256;#ffffff;1;#aabbcc"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } +} + test "OSC: OSC4: mix get/set palette color" { const testing = std.testing; @@ -1965,7 +2039,10 @@ test "OSC: OSC4: mix get/set palette color" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .set); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.set.kind); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); try testing.expectEqual( RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, op.set.color, @@ -1996,7 +2073,10 @@ test "OSC: OSC104: reset palette color 1" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .reset); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.reset, + ); } } @@ -2018,12 +2098,68 @@ test "OSC: OSC104: reset palette color 2" { { const op = cmd.color_operation.operations.items[0]; try testing.expect(op == .reset); - try testing.expectEqual(Command.ColorKind{ .palette = 17 }, op.reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.reset, + ); } { const op = cmd.color_operation.operations.items[1]; try testing.expect(op == .reset); - try testing.expectEqual(Command.ColorKind{ .palette = 111 }, op.reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 111 }, + op.reset, + ); + } +} + +test "OSC: OSC104: invalid palette index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;ffff;111"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 111 }, + op.reset, + ); + } +} + +test "OSC: OSC104: empty palette index" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "104;;111"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind{ .palette = 111 }, + op.reset, + ); } } From 5bb74929554e298651298d32f73ef29de14e4294 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 20:44:33 -0500 Subject: [PATCH 305/642] OSC: convert OSC 110, 111, and 112 and add more tests --- src/terminal/Parser.zig | 21 +- src/terminal/osc.zig | 554 +++++++++++++++++++++++++--------- src/terminal/stream.zig | 21 -- src/termio/stream_handler.zig | 197 ------------ 4 files changed, 437 insertions(+), 356 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 14ed6d6df..80772d71f 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -877,7 +877,12 @@ test "osc: change window title (end in esc)" { // https://github.com/darrenstarr/VtNetCore/pull/14 // Saw this on HN, decided to add a test case because why not. test "osc: 112 incomplete sequence" { - var p = init(); + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = init(); + p.osc_parser.alloc = arena.allocator(); + _ = p.next(0x1B); _ = p.next(']'); _ = p.next('1'); @@ -892,8 +897,18 @@ test "osc: 112 incomplete sequence" { try testing.expect(a[2] == null); const cmd = a[0].?.osc_dispatch; - try testing.expect(cmd == .reset_color); - try testing.expectEqual(cmd.reset_color.kind, .cursor); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + osc.Command.ColorKind.cursor, + op.reset, + ); + } } } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 778196160..7b9239e43 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -116,37 +116,6 @@ pub const Command = union(enum) { terminator: Terminator = .st, }, - /// OSC 4, OSC 10, and OSC 11 color report. - report_color: struct { - /// OSC 4 requests a palette color, OSC 10 requests the foreground - /// color, OSC 11 the background color. - kind: ColorKind, - - /// We must reply with the same string terminator (ST) as used in the - /// request. - terminator: Terminator = .st, - }, - - /// Modify the foreground (OSC 10) or background color (OSC 11), or a palette color (OSC 4) - set_color: struct { - /// OSC 4 sets a palette color, OSC 10 sets the foreground color, OSC 11 - /// the background color. - kind: ColorKind, - - /// The color spec as a string - value: []const u8, - }, - - /// Reset a palette color (OSC 104) or the foreground (OSC 110), background - /// (OSC 111), or cursor (OSC 112) color. - reset_color: struct { - kind: ColorKind, - - /// OSC 104 can have parameters indicating which palette colors to - /// reset. - value: []const u8, - }, - /// Kitty color protocol, OSC 21 /// https://sw.kovidgoyal.net/kitty/color-stack/#id1 kitty_color_protocol: kitty.color.OSC, @@ -195,6 +164,9 @@ pub const Command = union(enum) { osc_11 = 11, osc_12 = 12, osc_104 = 104, + osc_110 = 110, + osc_111 = 111, + osc_112 = 112, pub fn format( self: ColorOperationSource, @@ -347,15 +319,6 @@ pub const Parser = struct { @"8", @"9", - // OSC 10 is used to query or set the current foreground color. - query_fg_color, - - // OSC 11 is used to query or set the current background color. - query_bg_color, - - // OSC 12 is used to query or set the current cursor color. - query_cursor_color, - // We're in a semantic prompt OSC command but we aren't sure // what the command is yet, i.e. `133;` semantic_prompt, @@ -372,9 +335,27 @@ pub const Parser = struct { // Get/set color palette index osc_4, + // Get/set foreground color + osc_10, + + // Get/set background color + osc_11, + + // Get/set cursor color + osc_12, + // Reset color palette index osc_104, + // Reset foreground color + osc_110, + + // Reset background color + osc_111, + + // Reset cursor color + osc_112, + // Hyperlinks hyperlink_param_key, hyperlink_param_value, @@ -548,15 +529,26 @@ pub const Parser = struct { }, .@"10" => switch (c) { - ';' => self.state = .query_fg_color, + ';' => osc_10: { + if (self.alloc == null) { + log.warn("OSC 10 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_10; + } + self.state = .osc_10; + self.buf_start = self.buf_idx; + self.complete = true; + }, '4' => self.state = .@"104", else => self.state = .invalid, }, + .osc_10 => {}, + .@"104" => switch (c) { ';' => osc_104: { if (self.alloc == null) { - log.info("OSC 104 requires an allocator, but none was provided", .{}); + log.warn("OSC 104 requires an allocator, but none was provided", .{}); self.state = .invalid; break :osc_104; } @@ -570,30 +562,73 @@ pub const Parser = struct { .osc_104 => {}, .@"11" => switch (c) { - ';' => self.state = .query_bg_color, - '0' => { - self.command = .{ .reset_color = .{ .kind = .foreground, .value = undefined } }; + ';' => osc_11: { + if (self.alloc == null) { + log.warn("OSC 11 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_11; + } + self.state = .osc_11; + self.buf_start = self.buf_idx; self.complete = true; - self.state = .invalid; }, - '1' => { - self.command = .{ .reset_color = .{ .kind = .background, .value = undefined } }; + '0' => osc_110: { + if (self.alloc == null) { + log.warn("OSC 110 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_110; + } + self.state = .osc_110; + self.buf_start = self.buf_idx; self.complete = true; - self.state = .invalid; }, - '2' => { - self.command = .{ .reset_color = .{ .kind = .cursor, .value = undefined } }; + '1' => osc_111: { + if (self.alloc == null) { + log.warn("OSC 111 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_111; + } + self.state = .osc_111; + self.buf_start = self.buf_idx; + self.complete = true; + }, + '2' => osc_112: { + if (self.alloc == null) { + log.warn("OSC 112 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_112; + } + self.state = .osc_112; + self.buf_start = self.buf_idx; self.complete = true; - self.state = .invalid; }, else => self.state = .invalid, }, + .osc_11 => {}, + + .osc_110 => {}, + + .osc_111 => {}, + + .osc_112 => {}, + .@"12" => switch (c) { - ';' => self.state = .query_cursor_color, + ';' => osc_12: { + if (self.alloc == null) { + log.warn("OSC 12 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_12; + } + self.state = .osc_12; + self.buf_start = self.buf_idx; + self.complete = true; + }, else => self.state = .invalid, }, + .osc_12 => {}, + .@"13" => switch (c) { '3' => self.state = .@"133", else => self.state = .invalid, @@ -978,60 +1013,6 @@ pub const Parser = struct { }, }, - .query_fg_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .foreground } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .foreground, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .query_bg_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .background } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .background, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - - .query_cursor_color => switch (c) { - '?' => { - self.command = .{ .report_color = .{ .kind = .cursor } }; - self.complete = true; - self.state = .invalid; - }, - else => { - self.command = .{ .set_color = .{ - .kind = .cursor, - .value = "", - } }; - - self.state = .string; - self.temp_state = .{ .str = &self.command.set_color.value }; - self.buf_start = self.buf_idx - 1; - }, - }, - .semantic_prompt => switch (c) { 'A' => { self.state = .semantic_option_start; @@ -1397,6 +1378,115 @@ pub const Parser = struct { } } + fn parseOSC101112(self: *Parser) void { + assert(switch (self.state) { + .osc_10, .osc_11, .osc_12 => true, + else => false, + }); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = switch (self.state) { + .osc_10 => .osc_10, + .osc_11 => .osc_11, + .osc_12 => .osc_12, + else => unreachable, + }, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { + log.warn("unable to allocate memory for OSC 10/11/12 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + const str = self.buf[self.buf_start..self.buf_idx]; + var it = std.mem.splitScalar(u8, str, ';'); + const color_str = it.next() orelse { + log.warn("OSC 10/11/12 requires an argument", .{}); + self.state = .invalid; + return; + }; + if (std.mem.eql(u8, color_str, "?")) { + self.command.color_operation.operations.append( + alloc, + .{ + .report = switch (self.state) { + .osc_10 => .foreground, + .osc_11 => .background, + .osc_12 => .cursor, + else => unreachable, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } else { + const color = RGB.parse(color_str) catch |err| { + log.warn("invalid color specification in OSC 10/11/12: {s} {}", .{ color_str, err }); + return; + }; + self.command.color_operation.operations.append( + alloc, + .{ + .set = .{ + .kind = switch (self.state) { + .osc_10 => .foreground, + .osc_11 => .background, + .osc_12 => .cursor, + else => unreachable, + }, + .color = color, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + } + + fn parseOSC110111112(self: *Parser) void { + assert(switch (self.state) { + .osc_110, .osc_111, .osc_112 => true, + else => false, + }); + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = switch (self.state) { + .osc_110 => .osc_110, + .osc_111 => .osc_111, + .osc_112 => .osc_112, + else => unreachable, + }, + .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { + log.warn("unable to allocate memory for OSC 110/111/112 parsing: {}", .{err}); + self.state = .invalid; + return; + }, + }, + }; + self.command.color_operation.operations.append( + alloc, + .{ + .reset = switch (self.state) { + .osc_110 => .foreground, + .osc_111 => .background, + .osc_112 => .cursor, + else => unreachable, + }, + }, + ) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + } + fn parseOSC104(self: *Parser) void { assert(self.state == .osc_104); @@ -1457,13 +1547,22 @@ pub const Parser = struct { .allocable_string => self.endAllocableString(), .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), - .osc_4 => self.parseOSC4(), - .osc_104 => self.parseOSC104(), + .osc_4, + => self.parseOSC4(), + .osc_10, + .osc_11, + .osc_12, + => self.parseOSC101112(), + .osc_104, + => self.parseOSC104(), + .osc_110, + .osc_111, + .osc_112, + => self.parseOSC110111112(), else => {}, } switch (self.command) { - .report_color => |*c| c.terminator = .init(terminator_ch), .kitty_color_protocol => |*c| c.terminator = .init(terminator_ch), .color_operation => |*c| c.terminator = .init(terminator_ch), else => {}, @@ -1674,17 +1773,86 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: reset cursor color" { +test "OSC: OSC110: reset cursor color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); - const input = "112"; + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "110"; for (input) |ch| p.next(ch); const cmd = p.end(null).?; - try testing.expect(cmd == .reset_color); - try testing.expectEqual(cmd.reset_color.kind, .cursor); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_110); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.foreground, + op.reset, + ); + } +} + +test "OSC: OSC111: reset cursor color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "111"; + for (input) |ch| p.next(ch); + + const cmd = p.end(null).?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_111); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.background, + op.reset, + ); + } +} + +test "OSC: OSC112: reset cursor color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "112"; + for (input) |ch| { + log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); + p.next(ch); + } + log.warn("finish: {s}", .{@tagName(p.state)}); + + const cmd = p.end(null).?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.cursor, + op.reset, + ); + } } test "OSC: get/set clipboard" { @@ -1781,62 +1949,178 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } -test "OSC: report default foreground color" { +test "OSC: OSC10: report default foreground color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "10;?"; for (input) |ch| p.next(ch); // This corresponds to ST = ESC followed by \ const cmd = p.end('\x1b').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(cmd.report_color.kind, .foreground); - try testing.expectEqual(cmd.report_color.terminator, .st); + + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind.foreground, + op.report, + ); + } } -test "OSC: set foreground color" { +test "OSC: OSC10: set foreground color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "10;rgbi:0.0/0.5/1.0"; for (input) |ch| p.next(ch); const cmd = p.end('\x07').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(cmd.set_color.kind, .foreground); - try testing.expectEqualStrings(cmd.set_color.value, "rgbi:0.0/0.5/1.0"); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind.foreground, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0x00, .g = 0x7f, .b = 0xff }, + op.set.color, + ); + } } -test "OSC: report default background color" { +test "OSC: OSC11: report default background color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "11;?"; for (input) |ch| p.next(ch); // This corresponds to ST = BEL character const cmd = p.end('\x07').?; - try testing.expect(cmd == .report_color); - try testing.expectEqual(cmd.report_color.kind, .background); - try testing.expectEqual(cmd.report_color.terminator, .bel); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind.background, + op.report, + ); + } + try testing.expectEqual(cmd.color_operation.terminator, .bel); } -test "OSC: set background color" { +test "OSC: OSC11: set background color" { const testing = std.testing; - var p: Parser = .{}; + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; const input = "11;rgb:f/ff/ffff"; for (input) |ch| p.next(ch); const cmd = p.end('\x1b').?; - try testing.expect(cmd == .set_color); - try testing.expectEqual(cmd.set_color.kind, .background); - try testing.expectEqualStrings(cmd.set_color.value, "rgb:f/ff/ffff"); + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind.background, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xff, .g = 0xff, .b = 0xff }, + op.set.color, + ); + } +} + +test "OSC: OSC12: report background color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "12;?"; + for (input) |ch| p.next(ch); + + // This corresponds to ST = BEL character + const cmd = p.end('\x07').?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind.cursor, + op.report, + ); + } + try testing.expectEqual(cmd.color_operation.terminator, .bel); +} + +test "OSC: OSC12: set background color" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "12;rgb:f/ff/ffff"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.operations.items.len == 1); + { + const op = cmd.color_operation.operations.items[0]; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind.cursor, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xff, .g = 0xff, .b = 0xff }, + op.set.color, + ); + } } test "OSC: OSC4: get palette color 1" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 2a1ae80c9..08ce23098 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1562,27 +1562,6 @@ pub fn Stream(comptime Handler: type) type { } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, - .report_color => |v| { - if (@hasDecl(T, "reportColor")) { - try self.handler.reportColor(v.kind, v.terminator); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .set_color => |v| { - if (@hasDecl(T, "setColor")) { - try self.handler.setColor(v.kind, v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - - .reset_color => |v| { - if (@hasDecl(T, "resetColor")) { - try self.handler.resetColor(v.kind, v.value); - return; - } else log.warn("unimplemented OSC callback: {}", .{cmd}); - }, - .kitty_color_protocol => |v| { if (@hasDecl(T, "sendKittyColorReport")) { try self.handler.sendKittyColorReport(v); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 57a1eeacf..396aae01f 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1370,203 +1370,6 @@ pub const StreamHandler = struct { } } - /// Implements OSC 4, OSC 10, and OSC 11, which reports palette color, - /// default foreground color, and background color respectively. - pub fn reportColor( - self: *StreamHandler, - kind: terminal.osc.Command.ColorKind, - terminator: terminal.osc.Terminator, - ) !void { - if (self.osc_color_report_format == .none) return; - - const color = switch (kind) { - .palette => |i| self.terminal.color_palette.colors[i], - .foreground => self.foreground_color orelse self.default_foreground_color, - .background => self.background_color orelse self.default_background_color, - .cursor => self.cursor_color orelse - self.default_cursor_color orelse - self.foreground_color orelse - self.default_foreground_color, - }; - - var msg: termio.Message = .{ .write_small = .{} }; - const resp = switch (self.osc_color_report_format) { - .@"16-bit" => switch (kind) { - .palette => |i| try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", - .{ - kind.code(), - i, - @as(u16, color.r) * 257, - @as(u16, color.g) * 257, - @as(u16, color.b) * 257, - terminator.string(), - }, - ), - else => try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};rgb:{x:0>4}/{x:0>4}/{x:0>4}{s}", - .{ - kind.code(), - @as(u16, color.r) * 257, - @as(u16, color.g) * 257, - @as(u16, color.b) * 257, - terminator.string(), - }, - ), - }, - - .@"8-bit" => switch (kind) { - .palette => |i| try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", - .{ - kind.code(), - i, - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - terminator.string(), - }, - ), - else => try std.fmt.bufPrint( - &msg.write_small.data, - "\x1B]{s};rgb:{x:0>2}/{x:0>2}/{x:0>2}{s}", - .{ - kind.code(), - @as(u16, color.r), - @as(u16, color.g), - @as(u16, color.b), - terminator.string(), - }, - ), - }, - .none => unreachable, // early return above - }; - msg.write_small.len = @intCast(resp.len); - self.messageWriter(msg); - } - - pub fn setColor( - self: *StreamHandler, - kind: terminal.osc.Command.ColorKind, - value: []const u8, - ) !void { - const color = try terminal.color.RGB.parse(value); - - switch (kind) { - .palette => |i| { - self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = color; - self.terminal.color_palette.mask.set(i); - }, - .foreground => { - self.foreground_color = color; - _ = self.renderer_mailbox.push(.{ - .foreground_color = color, - }, .{ .forever = {} }); - }, - .background => { - self.background_color = color; - _ = self.renderer_mailbox.push(.{ - .background_color = color, - }, .{ .forever = {} }); - }, - .cursor => { - self.cursor_color = color; - _ = self.renderer_mailbox.push(.{ - .cursor_color = color, - }, .{ .forever = {} }); - }, - } - - // Notify the surface of the color change - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = kind, - .color = color, - } }); - } - - pub fn resetColor( - self: *StreamHandler, - kind: terminal.osc.Command.ColorKind, - value: []const u8, - ) !void { - switch (kind) { - .palette => { - const mask = &self.terminal.color_palette.mask; - if (value.len == 0) { - // Find all bit positions in the mask which are set and - // reset those indices to the default palette - var it = mask.iterator(.{}); - while (it.next()) |i| { - self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask.unset(i); - - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .{ .palette = @intCast(i) }, - .color = self.terminal.color_palette.colors[i], - } }); - } - } else { - var it = std.mem.tokenizeScalar(u8, value, ';'); - while (it.next()) |param| { - // Skip invalid parameters - const i = std.fmt.parseUnsigned(u8, param, 10) catch continue; - if (mask.isSet(i)) { - self.terminal.flags.dirty.palette = true; - self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; - mask.unset(i); - - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .{ .palette = @intCast(i) }, - .color = self.terminal.color_palette.colors[i], - } }); - } - } - } - }, - .foreground => { - self.foreground_color = null; - _ = self.renderer_mailbox.push(.{ - .foreground_color = self.foreground_color, - }, .{ .forever = {} }); - - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .foreground, - .color = self.default_foreground_color, - } }); - }, - .background => { - self.background_color = null; - _ = self.renderer_mailbox.push(.{ - .background_color = self.background_color, - }, .{ .forever = {} }); - - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .background, - .color = self.default_background_color, - } }); - }, - .cursor => { - self.cursor_color = null; - - _ = self.renderer_mailbox.push(.{ - .cursor_color = self.cursor_color, - }, .{ .forever = {} }); - - if (self.default_cursor_color) |color| { - self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .cursor, - .color = color, - } }); - } - }, - } - } - pub fn showDesktopNotification( self: *StreamHandler, title: []const u8, From 1288296fdc718308049b124f031a3899973e59ab Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:04:26 -0500 Subject: [PATCH 306/642] OSC: add a datastructure to prevent some (most?) allocations --- src/datastruct/list.zig | 100 ++++++++++++++++ src/terminal/Parser.zig | 6 +- src/terminal/osc.zig | 216 +++++++++++++++++++++------------- src/terminal/stream.zig | 2 +- src/termio/stream_handler.zig | 6 +- 5 files changed, 246 insertions(+), 84 deletions(-) create mode 100644 src/datastruct/list.zig diff --git a/src/datastruct/list.zig b/src/datastruct/list.zig new file mode 100644 index 000000000..e5f8bb483 --- /dev/null +++ b/src/datastruct/list.zig @@ -0,0 +1,100 @@ +const std = @import("std"); + +const assert = std.debug.assert; + +/// Datastructure to manage a (usually) small list of items. To prevent allocations +/// on the heap, statically allocate a small array that gets used to store items. Once +/// that small array is full then memory will be dynamically allocated on the heap +/// to store items. +pub fn ArrayListStaticUnmanaged(comptime static_size: usize, comptime T: type) type { + return struct { + count: usize, + static: [static_size]T, + dynamic: std.ArrayListUnmanaged(T), + + const Self = @This(); + + pub const empty: Self = .{ + .count = 0, + .static = undefined, + .dynamic = .empty, + }; + + pub fn deinit(self: *Self, alloc: std.mem.Allocator) void { + self.dynamic.deinit(alloc); + } + + pub fn append(self: *Self, alloc: std.mem.Allocator, item: T) !void { + if (self.count < static_size) { + self.static[self.count] = item; + self.count += 1; + assert(self.count <= static_size); + return; + } + try self.dynamic.append(alloc, item); + self.count += 1; + assert(self.count == static_size + self.dynamic.items.len); + } + + pub const Iterator = struct { + context: *const Self, + index: usize, + + pub fn next(self: *Iterator) ?T { + if (self.index >= self.context.count) return null; + + if (self.index < static_size) { + defer self.index += 1; + return self.context.static[self.index]; + } + + assert(self.index - static_size < self.context.dynamic.items.len); + + defer self.index += 1; + return self.context.dynamic.items[self.index - static_size]; + } + }; + + pub fn iterator(self: *const Self) Iterator { + return .{ + .context = self, + .index = 0, + }; + } + }; +} + +test "ArrayListStaticUnmanged: 1" { + const alloc = std.testing.allocator; + + var l: ArrayListStaticUnmanaged(1, usize) = .empty; + defer l.deinit(alloc); + + try l.append(alloc, 1); + + try std.testing.expectEqual(1, l.count); + try std.testing.expectEqual(1, l.static[0]); + try std.testing.expectEqual(0, l.dynamic.items.len); + + var it = l.iterator(); + try std.testing.expectEqual(1, it.next().?); + try std.testing.expectEqual(null, it.next()); +} + +test "ArrayListStaticUnmanged: 2" { + const alloc = std.testing.allocator; + + var l: ArrayListStaticUnmanaged(1, usize) = .empty; + defer l.deinit(alloc); + + try l.append(alloc, 1); + try l.append(alloc, 2); + + try std.testing.expectEqual(2, l.count); + try std.testing.expectEqual(1, l.static[0]); + try std.testing.expectEqual(1, l.dynamic.items.len); + var it = l.iterator(); + try std.testing.expectEqual(1, it.next().?); + try std.testing.expectEqual(2, it.next().?); + try std.testing.expectEqual(null, it.next()); +} diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 80772d71f..0f035f7fb 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -900,15 +900,17 @@ test "osc: 112 incomplete sequence" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( osc.Command.ColorKind.cursor, op.reset, ); } + try std.testing.expect(it.next() == null); } } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7b9239e43..0f5ecf724 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -13,6 +13,8 @@ const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); +const ArrayListStaticUnmanaged = @import("../datastruct/list.zig").ArrayListStaticUnmanaged; + const log = std.log.scoped(.osc); pub const Command = union(enum) { @@ -112,7 +114,7 @@ pub const Command = union(enum) { /// OSC color operations color_operation: struct { source: ColorOperationSource, - operations: std.ArrayListUnmanaged(ColorOperation) = .empty, + operations: ColorOperationList = .empty, terminator: Terminator = .st, }, @@ -187,6 +189,8 @@ pub const Command = union(enum) { report: ColorKind, }; + pub const ColorOperationList = ArrayListStaticUnmanaged(4, ColorOperation); + pub const ColorKind = union(enum) { palette: u8, foreground, @@ -1325,11 +1329,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_4, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { - log.warn("unable to allocate memory for OSC 4 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; @@ -1394,11 +1394,7 @@ pub const Parser = struct { .osc_12 => .osc_12, else => unreachable, }, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { - log.warn("unable to allocate memory for OSC 10/11/12 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; const str = self.buf[self.buf_start..self.buf_idx]; @@ -1464,11 +1460,7 @@ pub const Parser = struct { .osc_112 => .osc_112, else => unreachable, }, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 1) catch |err| { - log.warn("unable to allocate memory for OSC 110/111/112 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; self.command.color_operation.operations.append( @@ -1495,11 +1487,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_104, - .operations = std.ArrayListUnmanaged(Command.ColorOperation).initCapacity(alloc, 8) catch |err| { - log.warn("unable to allocate memory for OSC 104 parsing: {}", .{err}); - self.state = .invalid; - return; - }, + .operations = .empty, }, }; @@ -1788,15 +1776,17 @@ test "OSC: OSC110: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_110); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind.foreground, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC111: reset cursor color" { @@ -1814,15 +1804,17 @@ test "OSC: OSC111: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_111); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind.background, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC112: reset cursor color" { @@ -1834,25 +1826,55 @@ test "OSC: OSC112: reset cursor color" { var p: Parser = .{ .alloc = arena.allocator() }; const input = "112"; - for (input) |ch| { - log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); - p.next(ch); - } - log.warn("finish: {s}", .{@tagName(p.state)}); + for (input) |ch| p.next(ch); const cmd = p.end(null).?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind.cursor, op.reset, ); } + try testing.expect(it.next() == null); +} + +test "OSC: OSC112: reset cursor color with semicolon" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "112;"; + for (input) |ch| { + log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); + p.next(ch); + } + log.warn("finish: {s}", .{@tagName(p.state)}); + + const cmd = p.end(0x07).?; + try testing.expect(cmd == .color_operation); + try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .reset); + try testing.expectEqual( + Command.ColorKind.cursor, + op.reset, + ); + } + try testing.expect(it.next() == null); } test "OSC: get/set clipboard" { @@ -1966,15 +1988,17 @@ test "OSC: OSC10: report default foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind.foreground, op.report, ); } + try testing.expect(it.next() == null); } test "OSC: OSC10: set foreground color" { @@ -1992,9 +2016,10 @@ test "OSC: OSC10: set foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind.foreground, @@ -2005,6 +2030,7 @@ test "OSC: OSC10: set foreground color" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC11: report default background color" { @@ -2023,9 +2049,10 @@ test "OSC: OSC11: report default background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind.background, @@ -2033,6 +2060,7 @@ test "OSC: OSC11: report default background color" { ); } try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(it.next() == null); } test "OSC: OSC11: set background color" { @@ -2050,9 +2078,10 @@ test "OSC: OSC11: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind.background, @@ -2063,6 +2092,7 @@ test "OSC: OSC11: set background color" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC12: report background color" { @@ -2081,9 +2111,10 @@ test "OSC: OSC12: report background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind.cursor, @@ -2091,6 +2122,7 @@ test "OSC: OSC12: report background color" { ); } try testing.expectEqual(cmd.color_operation.terminator, .bel); + try testing.expect(it.next() == null); } test "OSC: OSC12: set background color" { @@ -2108,9 +2140,10 @@ test "OSC: OSC12: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind.cursor, @@ -2121,6 +2154,7 @@ test "OSC: OSC12: set background color" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: get palette color 1" { @@ -2137,9 +2171,10 @@ test "OSC: OSC4: get palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2147,6 +2182,7 @@ test "OSC: OSC4: get palette color 1" { ); try testing.expectEqual(cmd.color_operation.terminator, .st); } + try testing.expect(it.next() == null); } test "OSC: OSC4: get palette color 2" { @@ -2163,9 +2199,10 @@ test "OSC: OSC4: get palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2173,7 +2210,7 @@ test "OSC: OSC4: get palette color 2" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 2 }, @@ -2181,6 +2218,7 @@ test "OSC: OSC4: get palette color 2" { ); } try testing.expectEqual(cmd.color_operation.terminator, .st); + try testing.expect(it.next() == null); } test "OSC: OSC4: set palette color 1" { @@ -2197,17 +2235,21 @@ test "OSC: OSC4: set palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); - const op = cmd.color_operation.operations.items[0]; - try testing.expect(op == .set); - try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, - op.set.color, - ); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .set); + try testing.expectEqual( + Command.ColorKind{ .palette = 17 }, + op.set.kind, + ); + try testing.expectEqual( + RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, + op.set.color, + ); + } + try testing.expect(it.next() == null); } test "OSC: OSC4: set palette color 2" { @@ -2224,9 +2266,10 @@ test "OSC: OSC4: set palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, @@ -2238,7 +2281,7 @@ test "OSC: OSC4: set palette color 2" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2249,6 +2292,7 @@ test "OSC: OSC4: set palette color 2" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: get with invalid index" { @@ -2265,15 +2309,17 @@ test "OSC: OSC4: get with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: set with invalid index" { @@ -2290,9 +2336,10 @@ test "OSC: OSC4: set with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, @@ -2303,6 +2350,7 @@ test "OSC: OSC4: set with invalid index" { op.set.color, ); } + try testing.expect(it.next() == null); } test "OSC: OSC4: mix get/set palette color" { @@ -2319,9 +2367,10 @@ test "OSC: OSC4: mix get/set palette color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, @@ -2333,10 +2382,11 @@ test "OSC: OSC4: mix get/set palette color" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .report); try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); } + try testing.expect(it.next() == null); } test "OSC: OSC104: reset palette color 1" { @@ -2353,15 +2403,17 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC104: reset palette color 2" { @@ -2378,9 +2430,10 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 2); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, @@ -2388,13 +2441,14 @@ test "OSC: OSC104: reset palette color 2" { ); } { - const op = cmd.color_operation.operations.items[1]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC104: invalid palette index" { @@ -2411,15 +2465,17 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, ); } + try testing.expect(it.next() == null); } test "OSC: OSC104: empty palette index" { @@ -2436,15 +2492,17 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.items.len == 1); + try testing.expect(cmd.color_operation.operations.count == 1); + var it = cmd.color_operation.operations.iterator(); { - const op = cmd.color_operation.operations.items[0]; + const op = it.next().?; try testing.expect(op == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, ); } + try std.testing.expect(it.next() == null); } test "OSC: conemu sleep" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 08ce23098..fd30720b3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1557,7 +1557,7 @@ pub fn Stream(comptime Handler: type) type { .color_operation => |v| { if (@hasDecl(T, "handleColorOperation")) { - try self.handler.handleColorOperation(v.source, v.operations.items, v.terminator); + try self.handler.handleColorOperation(v.source, &v.operations, v.terminator); return; } else log.warn("unimplemented OSC callback: {}", .{cmd}); }, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 396aae01f..fd450f229 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1198,7 +1198,7 @@ pub const StreamHandler = struct { pub fn handleColorOperation( self: *StreamHandler, source: terminal.osc.Command.ColorOperationSource, - operations: []terminal.osc.Command.ColorOperation, + operations: *const terminal.osc.Command.ColorOperationList, terminator: terminal.osc.Terminator, ) !void { var buffer: [1024]u8 = undefined; @@ -1212,7 +1212,9 @@ pub const StreamHandler = struct { try writer.print("\x1b]{}", .{source}); - for (operations) |op| { + var it = operations.iterator(); + + while (it.next()) |op| { switch (op) { .set => |set| { switch (set.kind) { From 04e8e521719e040e36e0814fd6944220a7e1cbd5 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:26:01 -0500 Subject: [PATCH 307/642] OSC: reflow comment --- src/datastruct/list.zig | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/datastruct/list.zig b/src/datastruct/list.zig index e5f8bb483..7e9b78761 100644 --- a/src/datastruct/list.zig +++ b/src/datastruct/list.zig @@ -2,10 +2,10 @@ const std = @import("std"); const assert = std.debug.assert; -/// Datastructure to manage a (usually) small list of items. To prevent allocations -/// on the heap, statically allocate a small array that gets used to store items. Once -/// that small array is full then memory will be dynamically allocated on the heap -/// to store items. +/// Datastructure to manage a (usually) small list of items. To prevent +/// allocations on the heap, statically allocate a small array that gets used to +/// store items. Once that small array is full then memory will be dynamically +/// allocated on the heap to store items. pub fn ArrayListStaticUnmanaged(comptime static_size: usize, comptime T: type) type { return struct { count: usize, From 1d9d253e4d2651f5c075082e94bf0eecbcd706e8 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:26:21 -0500 Subject: [PATCH 308/642] OSC: fix bug with buffer disappearing --- src/termio/stream_handler.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index fd450f229..51dec5347 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1367,7 +1367,7 @@ pub const StreamHandler = struct { } if (report) { try writer.writeAll(terminator.string()); - const msg: termio.Message = .{ .write_stable = response.items }; + const msg = try termio.Message.writeReq(self.alloc, response.items); self.messageWriter(msg); } } From 397a8b13e06b74ba5a9a73f4bcbecf769ba3f485 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Fri, 23 May 2025 22:57:18 -0500 Subject: [PATCH 309/642] OSC: more tests --- src/terminal/osc.zig | 205 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 0f5ecf724..a8906b74f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -2295,7 +2295,7 @@ test "OSC: OSC4: set palette color 2" { try testing.expect(it.next() == null); } -test "OSC: OSC4: get with invalid index" { +test "OSC: OSC4: get with invalid index 1" { const testing = std.testing; var arena = std.heap.ArenaAllocator.init(std.testing.allocator); @@ -2322,6 +2322,209 @@ test "OSC: OSC4: get with invalid index" { try testing.expect(it.next() == null); } +test "OSC: OSC4: get with invalid index 2" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;5;?;1111;?;1;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.count == 2); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 5 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + +// Inspired by Microsoft Edit +test "OSC: OSC4: multiple get 8a" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.count == 8); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 0 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 1 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 2 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 3 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 4 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 5 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 6 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 7 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + +// Inspired by Microsoft Edit +test "OSC: OSC4: multiple get 8b" { + const testing = std.testing; + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + + var p: Parser = .{ .alloc = arena.allocator() }; + + const input = "4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.operations.count == 8); + var it = cmd.color_operation.operations.iterator(); + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 8 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 9 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 10 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 11 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 12 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 13 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 14 }, + op.report, + ); + } + { + const op = it.next().?; + try testing.expect(op == .report); + try testing.expectEqual( + Command.ColorKind{ .palette = 15 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + test "OSC: OSC4: set with invalid index" { const testing = std.testing; From 479fa9f809b079e638941afe8394564e2bffbd8f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:04:33 -0500 Subject: [PATCH 310/642] OSC: use std.SegmentedList instead of custom data structure --- src/datastruct/list.zig | 100 ----------- src/terminal/Parser.zig | 6 +- src/terminal/osc.zig | 308 ++++++++++++++++------------------ src/termio/stream_handler.zig | 4 +- 4 files changed, 154 insertions(+), 264 deletions(-) delete mode 100644 src/datastruct/list.zig diff --git a/src/datastruct/list.zig b/src/datastruct/list.zig deleted file mode 100644 index 7e9b78761..000000000 --- a/src/datastruct/list.zig +++ /dev/null @@ -1,100 +0,0 @@ -const std = @import("std"); - -const assert = std.debug.assert; - -/// Datastructure to manage a (usually) small list of items. To prevent -/// allocations on the heap, statically allocate a small array that gets used to -/// store items. Once that small array is full then memory will be dynamically -/// allocated on the heap to store items. -pub fn ArrayListStaticUnmanaged(comptime static_size: usize, comptime T: type) type { - return struct { - count: usize, - static: [static_size]T, - dynamic: std.ArrayListUnmanaged(T), - - const Self = @This(); - - pub const empty: Self = .{ - .count = 0, - .static = undefined, - .dynamic = .empty, - }; - - pub fn deinit(self: *Self, alloc: std.mem.Allocator) void { - self.dynamic.deinit(alloc); - } - - pub fn append(self: *Self, alloc: std.mem.Allocator, item: T) !void { - if (self.count < static_size) { - self.static[self.count] = item; - self.count += 1; - assert(self.count <= static_size); - return; - } - try self.dynamic.append(alloc, item); - self.count += 1; - assert(self.count == static_size + self.dynamic.items.len); - } - - pub const Iterator = struct { - context: *const Self, - index: usize, - - pub fn next(self: *Iterator) ?T { - if (self.index >= self.context.count) return null; - - if (self.index < static_size) { - defer self.index += 1; - return self.context.static[self.index]; - } - - assert(self.index - static_size < self.context.dynamic.items.len); - - defer self.index += 1; - return self.context.dynamic.items[self.index - static_size]; - } - }; - - pub fn iterator(self: *const Self) Iterator { - return .{ - .context = self, - .index = 0, - }; - } - }; -} - -test "ArrayListStaticUnmanged: 1" { - const alloc = std.testing.allocator; - - var l: ArrayListStaticUnmanaged(1, usize) = .empty; - defer l.deinit(alloc); - - try l.append(alloc, 1); - - try std.testing.expectEqual(1, l.count); - try std.testing.expectEqual(1, l.static[0]); - try std.testing.expectEqual(0, l.dynamic.items.len); - - var it = l.iterator(); - try std.testing.expectEqual(1, it.next().?); - try std.testing.expectEqual(null, it.next()); -} - -test "ArrayListStaticUnmanged: 2" { - const alloc = std.testing.allocator; - - var l: ArrayListStaticUnmanaged(1, usize) = .empty; - defer l.deinit(alloc); - - try l.append(alloc, 1); - try l.append(alloc, 2); - - try std.testing.expectEqual(2, l.count); - try std.testing.expectEqual(1, l.static[0]); - try std.testing.expectEqual(1, l.dynamic.items.len); - var it = l.iterator(); - try std.testing.expectEqual(1, it.next().?); - try std.testing.expectEqual(2, it.next().?); - try std.testing.expectEqual(null, it.next()); -} diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 0f035f7fb..df18fbc7a 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -900,11 +900,11 @@ test "osc: 112 incomplete sequence" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( osc.Command.ColorKind.cursor, op.reset, diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index a8906b74f..449713ff2 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -13,8 +13,6 @@ const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); -const ArrayListStaticUnmanaged = @import("../datastruct/list.zig").ArrayListStaticUnmanaged; - const log = std.log.scoped(.osc); pub const Command = union(enum) { @@ -111,10 +109,18 @@ pub const Command = union(enum) { value: []const u8, }, - /// OSC color operations + /// OSC color operations to set, reset, or report color settings. Some OSCs + /// allow multiple operations to be specified in a single OSC so we need a + /// list-like datastructure to manage them. We use std.SegmentedList because + /// it minimizes the number of allocations and copies because a large + /// majority of the time there will be only one operation per OSC. + /// + /// Currently, these OSCs are handled by `color_operation`: + /// + /// 4, 10, 11, 12, 104, 110, 111, 112 color_operation: struct { source: ColorOperationSource, - operations: ColorOperationList = .empty, + operations: ColorOperationList = .{}, terminator: Terminator = .st, }, @@ -189,7 +195,7 @@ pub const Command = union(enum) { report: ColorKind, }; - pub const ColorOperationList = ArrayListStaticUnmanaged(4, ColorOperation); + pub const ColorOperationList = std.SegmentedList(ColorOperation, 4); pub const ColorKind = union(enum) { palette: u8, @@ -1329,7 +1335,6 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_4, - .operations = .empty, }, }; @@ -1346,34 +1351,30 @@ pub const Parser = struct { }; const spec_str = it.next() orelse continue; if (std.mem.eql(u8, spec_str, "?")) { - self.command.color_operation.operations.append( - alloc, - .{ - .report = .{ .palette = index }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .report = .{ .palette = index }, + }; } else { const color = RGB.parse(spec_str) catch |err| { log.warn("invalid color specification in OSC 4: {s} {}", .{ spec_str, err }); continue; }; - self.command.color_operation.operations.append( - alloc, - .{ - .set = .{ - .kind = .{ - .palette = index, - }, - .color = color, - }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .set = .{ + .kind = .{ + .palette = index, + }, + .color = color, + }, + }; } } } @@ -1394,7 +1395,6 @@ pub const Parser = struct { .osc_12 => .osc_12, else => unreachable, }, - .operations = .empty, }, }; const str = self.buf[self.buf_start..self.buf_idx]; @@ -1405,42 +1405,38 @@ pub const Parser = struct { return; }; if (std.mem.eql(u8, color_str, "?")) { - self.command.color_operation.operations.append( - alloc, - .{ - .report = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .report = switch (self.state) { + .osc_10 => .foreground, + .osc_11 => .background, + .osc_12 => .cursor, + else => unreachable, + }, + }; } else { const color = RGB.parse(color_str) catch |err| { log.warn("invalid color specification in OSC 10/11/12: {s} {}", .{ color_str, err }); return; }; - self.command.color_operation.operations.append( - alloc, - .{ - .set = .{ - .kind = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - .color = color, - }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .set = .{ + .kind = switch (self.state) { + .osc_10 => .foreground, + .osc_11 => .background, + .osc_12 => .cursor, + else => unreachable, + }, + .color = color, + }, + }; } } @@ -1460,23 +1456,20 @@ pub const Parser = struct { .osc_112 => .osc_112, else => unreachable, }, - .operations = .empty, }, }; - self.command.color_operation.operations.append( - alloc, - .{ - .reset = switch (self.state) { - .osc_110 => .foreground, - .osc_111 => .background, - .osc_112 => .cursor, - else => unreachable, - }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .reset = switch (self.state) { + .osc_110 => .foreground, + .osc_111 => .background, + .osc_112 => .cursor, + else => unreachable, + }, + }; } fn parseOSC104(self: *Parser) void { @@ -1487,7 +1480,6 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = .osc_104, - .operations = .empty, }, }; @@ -1500,15 +1492,13 @@ pub const Parser = struct { continue; }, }; - self.command.color_operation.operations.append( - alloc, - .{ - .reset = .{ .palette = index }, - }, - ) catch |err| { + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; + op.* = .{ + .reset = .{ .palette = index }, + }; } } @@ -1776,11 +1766,11 @@ test "OSC: OSC110: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_110); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.foreground, op.reset, @@ -1804,11 +1794,11 @@ test "OSC: OSC111: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_111); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.background, op.reset, @@ -1832,11 +1822,11 @@ test "OSC: OSC112: reset cursor color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.cursor, op.reset, @@ -1864,11 +1854,11 @@ test "OSC: OSC112: reset cursor color with semicolon" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_112); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind.cursor, op.reset, @@ -1988,11 +1978,11 @@ test "OSC: OSC10: report default foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind.foreground, op.report, @@ -2016,11 +2006,11 @@ test "OSC: OSC10: set foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_10); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind.foreground, op.set.kind, @@ -2049,11 +2039,11 @@ test "OSC: OSC11: report default background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind.background, op.report, @@ -2078,11 +2068,11 @@ test "OSC: OSC11: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_11); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind.background, op.set.kind, @@ -2111,11 +2101,11 @@ test "OSC: OSC12: report background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind.cursor, op.report, @@ -2140,11 +2130,11 @@ test "OSC: OSC12: set background color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); try testing.expect(cmd.color_operation.source == .osc_12); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind.cursor, op.set.kind, @@ -2171,11 +2161,11 @@ test "OSC: OSC4: get palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2199,11 +2189,11 @@ test "OSC: OSC4: get palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2211,7 +2201,7 @@ test "OSC: OSC4: get palette color 2" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 2 }, op.report, @@ -2235,11 +2225,11 @@ test "OSC: OSC4: set palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.set.kind, @@ -2266,11 +2256,11 @@ test "OSC: OSC4: set palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.set.kind, @@ -2282,7 +2272,7 @@ test "OSC: OSC4: set palette color 2" { } { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.set.kind, @@ -2309,11 +2299,11 @@ test "OSC: OSC4: get with invalid index 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2336,11 +2326,11 @@ test "OSC: OSC4: get with invalid index 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 5 }, op.report, @@ -2348,7 +2338,7 @@ test "OSC: OSC4: get with invalid index 2" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2372,11 +2362,11 @@ test "OSC: OSC4: multiple get 8a" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 8); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 8); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 0 }, op.report, @@ -2384,7 +2374,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.report, @@ -2392,7 +2382,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 2 }, op.report, @@ -2400,7 +2390,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 3 }, op.report, @@ -2408,7 +2398,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 4 }, op.report, @@ -2416,7 +2406,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 5 }, op.report, @@ -2424,7 +2414,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 6 }, op.report, @@ -2432,7 +2422,7 @@ test "OSC: OSC4: multiple get 8a" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 7 }, op.report, @@ -2456,11 +2446,11 @@ test "OSC: OSC4: multiple get 8b" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 8); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 8); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 8 }, op.report, @@ -2468,7 +2458,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 9 }, op.report, @@ -2476,7 +2466,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 10 }, op.report, @@ -2484,7 +2474,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 11 }, op.report, @@ -2492,7 +2482,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 12 }, op.report, @@ -2500,7 +2490,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 13 }, op.report, @@ -2508,7 +2498,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 14 }, op.report, @@ -2516,7 +2506,7 @@ test "OSC: OSC4: multiple get 8b" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual( Command.ColorKind{ .palette = 15 }, op.report, @@ -2539,11 +2529,11 @@ test "OSC: OSC4: set with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 1 }, op.set.kind, @@ -2570,11 +2560,11 @@ test "OSC: OSC4: mix get/set palette color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_4); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .set); + try testing.expect(op.* == .set); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.set.kind, @@ -2586,7 +2576,7 @@ test "OSC: OSC4: mix get/set palette color" { } { const op = it.next().?; - try testing.expect(op == .report); + try testing.expect(op.* == .report); try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); } try testing.expect(it.next() == null); @@ -2606,11 +2596,11 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.reset, @@ -2633,11 +2623,11 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 2); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 2); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 17 }, op.reset, @@ -2645,7 +2635,7 @@ test "OSC: OSC104: reset palette color 2" { } { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, @@ -2668,11 +2658,11 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, @@ -2695,11 +2685,11 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expect(cmd.color_operation.source == .osc_104); - try testing.expect(cmd.color_operation.operations.count == 1); - var it = cmd.color_operation.operations.iterator(); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; - try testing.expect(op == .reset); + try testing.expect(op.* == .reset); try testing.expectEqual( Command.ColorKind{ .palette = 111 }, op.reset, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 51dec5347..5977f6564 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1212,10 +1212,10 @@ pub const StreamHandler = struct { try writer.print("\x1b]{}", .{source}); - var it = operations.iterator(); + var it = operations.constIterator(0); while (it.next()) |op| { - switch (op) { + switch (op.*) { .set => |set| { switch (set.kind) { .palette => |i| { From bd4d1950ce12855479424a7d7713bc65383e6d32 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:17:31 -0500 Subject: [PATCH 311/642] OSC: remove unused code --- src/terminal/osc.zig | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 449713ff2..0c429c70e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -202,15 +202,6 @@ pub const Command = union(enum) { foreground, background, cursor, - - pub fn code(self: ColorKind) []const u8 { - return switch (self) { - .palette => "4", - .foreground => "10", - .background => "11", - .cursor => "12", - }; - } }; pub const ProgressState = enum { From f2dfd9f6779f0cabb61f2c4ef3a70216ab9d42de Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:18:09 -0500 Subject: [PATCH 312/642] OSC: improve formatting of ColorOperationSource --- src/terminal/osc.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 0c429c70e..78a3560af 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -179,10 +179,10 @@ pub const Command = union(enum) { pub fn format( self: ColorOperationSource, comptime _: []const u8, - _: std.fmt.FormatOptions, + options: std.fmt.FormatOptions, writer: anytype, ) !void { - try writer.print("{d}", .{@intFromEnum(self)}); + try std.fmt.formatInt(@intFromEnum(self), 10, .lower, options, writer); } }; From e0ddc7a2fa66b5e990fc1bfa6ddc0e3ef930bb40 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:32:10 -0500 Subject: [PATCH 313/642] OSC: clean up color_operation handling --- src/termio/stream_handler.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 5977f6564..4bb0f9c9d 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1201,6 +1201,9 @@ pub const StreamHandler = struct { operations: *const terminal.osc.Command.ColorOperationList, terminator: terminal.osc.Terminator, ) !void { + // return early if there is nothing to do + if (operations.count() == 0) return; + var buffer: [1024]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&buffer); const alloc = fba.allocator(); @@ -1366,6 +1369,8 @@ pub const StreamHandler = struct { } } if (report) { + // If any of the operations were reports, finialize the report + // string and send it to the terminal. try writer.writeAll(terminator.string()); const msg = try termio.Message.writeReq(self.alloc, response.items); self.messageWriter(msg); From 35384670c4c80f532966955c2e4ad78dc41e2a48 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 08:59:37 -0500 Subject: [PATCH 314/642] OSC: fix typo --- src/termio/stream_handler.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 4bb0f9c9d..ca16b0bd2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1369,7 +1369,7 @@ pub const StreamHandler = struct { } } if (report) { - // If any of the operations were reports, finialize the report + // If any of the operations were reports, finalize the report // string and send it to the terminal. try writer.writeAll(terminator.string()); const msg = try termio.Message.writeReq(self.alloc, response.items); From fa03115f01abf63e45e37316479e291150fc6787 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 10:52:36 -0500 Subject: [PATCH 315/642] OSC: don't use arena during testing --- src/terminal/Parser.zig | 6 +- src/terminal/osc.zig | 147 ++++++++++++++-------------------------- 2 files changed, 51 insertions(+), 102 deletions(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index df18fbc7a..8cf2996d6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -877,11 +877,9 @@ test "osc: change window title (end in esc)" { // https://github.com/darrenstarr/VtNetCore/pull/14 // Saw this on HN, decided to add a test case because why not. test "osc: 112 incomplete sequence" { - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - var p: Parser = init(); - p.osc_parser.alloc = arena.allocator(); + defer p.deinit(); + p.osc_parser.alloc = std.testing.allocator; _ = p.next(0x1B); _ = p.next(']'); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 78a3560af..7e5a71536 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -1745,10 +1745,8 @@ test "OSC: end_of_input" { test "OSC: OSC110: reset cursor color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "110"; for (input) |ch| p.next(ch); @@ -1773,10 +1771,8 @@ test "OSC: OSC110: reset cursor color" { test "OSC: OSC111: reset cursor color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "111"; for (input) |ch| p.next(ch); @@ -1801,10 +1797,8 @@ test "OSC: OSC111: reset cursor color" { test "OSC: OSC112: reset cursor color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "112"; for (input) |ch| p.next(ch); @@ -1829,10 +1823,8 @@ test "OSC: OSC112: reset cursor color" { test "OSC: OSC112: reset cursor color with semicolon" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "112;"; for (input) |ch| { @@ -1888,9 +1880,8 @@ test "OSC: get/set clipboard (optional parameter)" { test "OSC: get/set clipboard with allocator" { const testing = std.testing; - const alloc = testing.allocator; - var p: Parser = .{ .alloc = alloc }; + var p: Parser = .{ .alloc = testing.allocator }; defer p.deinit(); const input = "52;s;?"; @@ -1955,10 +1946,8 @@ test "OSC: longer than buffer" { test "OSC: OSC10: report default foreground color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "10;?"; for (input) |ch| p.next(ch); @@ -1985,10 +1974,8 @@ test "OSC: OSC10: report default foreground color" { test "OSC: OSC10: set foreground color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "10;rgbi:0.0/0.5/1.0"; for (input) |ch| p.next(ch); @@ -2017,10 +2004,8 @@ test "OSC: OSC10: set foreground color" { test "OSC: OSC11: report default background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "11;?"; for (input) |ch| p.next(ch); @@ -2047,10 +2032,8 @@ test "OSC: OSC11: report default background color" { test "OSC: OSC11: set background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "11;rgb:f/ff/ffff"; for (input) |ch| p.next(ch); @@ -2079,10 +2062,8 @@ test "OSC: OSC11: set background color" { test "OSC: OSC12: report background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "12;?"; for (input) |ch| p.next(ch); @@ -2109,10 +2090,8 @@ test "OSC: OSC12: report background color" { test "OSC: OSC12: set background color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "12;rgb:f/ff/ffff"; for (input) |ch| p.next(ch); @@ -2141,10 +2120,8 @@ test "OSC: OSC12: set background color" { test "OSC: OSC4: get palette color 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;1;?"; for (input) |ch| p.next(ch); @@ -2169,10 +2146,8 @@ test "OSC: OSC4: get palette color 1" { test "OSC: OSC4: get palette color 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;1;?;2;?"; for (input) |ch| p.next(ch); @@ -2205,10 +2180,8 @@ test "OSC: OSC4: get palette color 2" { test "OSC: OSC4: set palette color 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;17;rgb:aa/bb/cc"; for (input) |ch| p.next(ch); @@ -2236,10 +2209,8 @@ test "OSC: OSC4: set palette color 1" { test "OSC: OSC4: set palette color 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;17;rgb:aa/bb/cc;1;rgb:00/11/22"; for (input) |ch| p.next(ch); @@ -2279,10 +2250,8 @@ test "OSC: OSC4: set palette color 2" { test "OSC: OSC4: get with invalid index 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;1111;?;1;?"; for (input) |ch| p.next(ch); @@ -2306,10 +2275,8 @@ test "OSC: OSC4: get with invalid index 1" { test "OSC: OSC4: get with invalid index 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;5;?;1111;?;1;?"; for (input) |ch| p.next(ch); @@ -2342,10 +2309,8 @@ test "OSC: OSC4: get with invalid index 2" { test "OSC: OSC4: multiple get 8a" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?"; for (input) |ch| p.next(ch); @@ -2426,10 +2391,8 @@ test "OSC: OSC4: multiple get 8a" { test "OSC: OSC4: multiple get 8b" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?"; for (input) |ch| p.next(ch); @@ -2509,10 +2472,8 @@ test "OSC: OSC4: multiple get 8b" { test "OSC: OSC4: set with invalid index" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;256;#ffffff;1;#aabbcc"; for (input) |ch| p.next(ch); @@ -2540,10 +2501,8 @@ test "OSC: OSC4: set with invalid index" { test "OSC: OSC4: mix get/set palette color" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "4;17;rgb:aa/bb/cc;254;?"; for (input) |ch| p.next(ch); @@ -2576,10 +2535,8 @@ test "OSC: OSC4: mix get/set palette color" { test "OSC: OSC104: reset palette color 1" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;17"; for (input) |ch| p.next(ch); @@ -2603,10 +2560,8 @@ test "OSC: OSC104: reset palette color 1" { test "OSC: OSC104: reset palette color 2" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;17;111"; for (input) |ch| p.next(ch); @@ -2638,10 +2593,8 @@ test "OSC: OSC104: reset palette color 2" { test "OSC: OSC104: invalid palette index" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;ffff;111"; for (input) |ch| p.next(ch); @@ -2665,10 +2618,8 @@ test "OSC: OSC104: invalid palette index" { test "OSC: OSC104: empty palette index" { const testing = std.testing; - var arena = std.heap.ArenaAllocator.init(std.testing.allocator); - defer arena.deinit(); - - var p: Parser = .{ .alloc = arena.allocator() }; + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); const input = "104;;111"; for (input) |ch| p.next(ch); From bcf4d55dad47472e317130f4372fb3ddfa35b512 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 11:30:17 -0500 Subject: [PATCH 316/642] OSC: nest ColorOperation-related structs --- src/apprt/surface.zig | 2 +- src/terminal/Parser.zig | 4 +- src/terminal/osc.zig | 232 +++++++++++++++++----------------- src/termio/stream_handler.zig | 4 +- 4 files changed, 122 insertions(+), 120 deletions(-) diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 6de41c544..dce6a3a56 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -74,7 +74,7 @@ pub const Message = union(enum) { /// A terminal color was changed using OSC sequences. color_change: struct { - kind: terminal.osc.Command.ColorKind, + kind: terminal.osc.Command.ColorOperation.Kind, color: terminal.color.RGB, }, diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 8cf2996d6..ec3f322f6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -897,14 +897,14 @@ test "osc: 112 incomplete sequence" { const cmd = a[0].?.osc_dispatch; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.source == .reset_cursor); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - osc.Command.ColorKind.cursor, + osc.Command.ColorOperation.Kind.cursor, op.reset, ); } diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7e5a71536..67f665f1a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -119,8 +119,8 @@ pub const Command = union(enum) { /// /// 4, 10, 11, 12, 104, 110, 111, 112 color_operation: struct { - source: ColorOperationSource, - operations: ColorOperationList = .{}, + source: ColorOperation.Source, + operations: ColorOperation.List = .{}, terminator: Terminator = .st, }, @@ -166,42 +166,44 @@ pub const Command = union(enum) { /// Wait input (OSC 9;5) wait_input: void, - pub const ColorOperationSource = enum(u16) { - osc_4 = 4, - osc_10 = 10, - osc_11 = 11, - osc_12 = 12, - osc_104 = 104, - osc_110 = 110, - osc_111 = 111, - osc_112 = 112, - - pub fn format( - self: ColorOperationSource, - comptime _: []const u8, - options: std.fmt.FormatOptions, - writer: anytype, - ) !void { - try std.fmt.formatInt(@intFromEnum(self), 10, .lower, options, writer); - } - }; - pub const ColorOperation = union(enum) { + pub const Source = enum(u16) { + // these numbers are based on the OSC operation code + // see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands + get_set_palette = 4, + get_set_foreground = 10, + get_set_background = 11, + get_set_cursor = 12, + reset_palette = 104, + reset_foreground = 110, + reset_background = 111, + reset_cursor = 112, + + pub fn format( + self: Source, + comptime _: []const u8, + options: std.fmt.FormatOptions, + writer: anytype, + ) !void { + try std.fmt.formatInt(@intFromEnum(self), 10, .lower, options, writer); + } + }; + + pub const List = std.SegmentedList(ColorOperation, 4); + + pub const Kind = union(enum) { + palette: u8, + foreground, + background, + cursor, + }; + set: struct { - kind: ColorKind, + kind: Kind, color: RGB, }, - reset: ColorKind, - report: ColorKind, - }; - - pub const ColorOperationList = std.SegmentedList(ColorOperation, 4); - - pub const ColorKind = union(enum) { - palette: u8, - foreground, - background, - cursor, + reset: Kind, + report: Kind, }; pub const ProgressState = enum { @@ -1325,7 +1327,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ - .source = .osc_4, + .source = .get_set_palette, }, }; @@ -1381,9 +1383,9 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = switch (self.state) { - .osc_10 => .osc_10, - .osc_11 => .osc_11, - .osc_12 => .osc_12, + .osc_10 => .get_set_foreground, + .osc_11 => .get_set_background, + .osc_12 => .get_set_cursor, else => unreachable, }, }, @@ -1442,9 +1444,9 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ .source = switch (self.state) { - .osc_110 => .osc_110, - .osc_111 => .osc_111, - .osc_112 => .osc_112, + .osc_110 => .reset_foreground, + .osc_111 => .reset_background, + .osc_112 => .reset_cursor, else => unreachable, }, }, @@ -1470,7 +1472,7 @@ pub const Parser = struct { self.command = .{ .color_operation = .{ - .source = .osc_104, + .source = .get_set_palette, }, }; @@ -1742,7 +1744,7 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: OSC110: reset cursor color" { +test "OSC: OSC110: reset foreground color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -1754,21 +1756,21 @@ test "OSC: OSC110: reset cursor color" { const cmd = p.end(null).?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_110); + try testing.expect(cmd.color_operation.source == .reset_foreground); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind.foreground, + Command.ColorOperation.Kind.foreground, op.reset, ); } try testing.expect(it.next() == null); } -test "OSC: OSC111: reset cursor color" { +test "OSC: OSC111: reset background color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -1780,14 +1782,14 @@ test "OSC: OSC111: reset cursor color" { const cmd = p.end(null).?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_111); + try testing.expect(cmd.color_operation.source == .reset_background); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind.background, + Command.ColorOperation.Kind.background, op.reset, ); } @@ -1806,14 +1808,14 @@ test "OSC: OSC112: reset cursor color" { const cmd = p.end(null).?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.source == .reset_cursor); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.reset, ); } @@ -1836,14 +1838,14 @@ test "OSC: OSC112: reset cursor color with semicolon" { const cmd = p.end(0x07).?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_112); + try testing.expect(cmd.color_operation.source == .reset_cursor); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.reset, ); } @@ -1943,7 +1945,7 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } -test "OSC: OSC10: report default foreground color" { +test "OSC: OSC10: report foreground color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -1957,14 +1959,14 @@ test "OSC: OSC10: report default foreground color" { try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.source == .get_set_foreground); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind.foreground, + Command.ColorOperation.Kind.foreground, op.report, ); } @@ -1983,14 +1985,14 @@ test "OSC: OSC10: set foreground color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_10); + try testing.expect(cmd.color_operation.source == .get_set_foreground); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind.foreground, + Command.ColorOperation.Kind.foreground, op.set.kind, ); try testing.expectEqual( @@ -2001,7 +2003,7 @@ test "OSC: OSC10: set foreground color" { try testing.expect(it.next() == null); } -test "OSC: OSC11: report default background color" { +test "OSC: OSC11: report background color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -2014,14 +2016,14 @@ test "OSC: OSC11: report default background color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.source == .get_set_background); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind.background, + Command.ColorOperation.Kind.background, op.report, ); } @@ -2041,14 +2043,14 @@ test "OSC: OSC11: set background color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_11); + try testing.expect(cmd.color_operation.source == .get_set_background); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind.background, + Command.ColorOperation.Kind.background, op.set.kind, ); try testing.expectEqual( @@ -2059,7 +2061,7 @@ test "OSC: OSC11: set background color" { try testing.expect(it.next() == null); } -test "OSC: OSC12: report background color" { +test "OSC: OSC12: report cursor color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -2072,14 +2074,14 @@ test "OSC: OSC12: report background color" { const cmd = p.end('\x07').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.source == .get_set_cursor); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.report, ); } @@ -2087,7 +2089,7 @@ test "OSC: OSC12: report background color" { try testing.expect(it.next() == null); } -test "OSC: OSC12: set background color" { +test "OSC: OSC12: set cursor color" { const testing = std.testing; var p: Parser = .{ .alloc = testing.allocator }; @@ -2099,14 +2101,14 @@ test "OSC: OSC12: set background color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(cmd.color_operation.source == .osc_12); + try testing.expect(cmd.color_operation.source == .get_set_cursor); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind.cursor, + Command.ColorOperation.Kind.cursor, op.set.kind, ); try testing.expectEqual( @@ -2128,14 +2130,14 @@ test "OSC: OSC4: get palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); try testing.expectEqual(cmd.color_operation.terminator, .st); @@ -2154,14 +2156,14 @@ test "OSC: OSC4: get palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2169,7 +2171,7 @@ test "OSC: OSC4: get palette color 2" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 2 }, + Command.ColorOperation.Kind{ .palette = 2 }, op.report, ); } @@ -2188,14 +2190,14 @@ test "OSC: OSC4: set palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.set.kind, ); try testing.expectEqual( @@ -2217,14 +2219,14 @@ test "OSC: OSC4: set palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.set.kind, ); try testing.expectEqual( @@ -2236,7 +2238,7 @@ test "OSC: OSC4: set palette color 2" { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.set.kind, ); try testing.expectEqual( @@ -2258,14 +2260,14 @@ test "OSC: OSC4: get with invalid index 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2283,14 +2285,14 @@ test "OSC: OSC4: get with invalid index 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 5 }, + Command.ColorOperation.Kind{ .palette = 5 }, op.report, ); } @@ -2298,7 +2300,7 @@ test "OSC: OSC4: get with invalid index 2" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2317,14 +2319,14 @@ test "OSC: OSC4: multiple get 8a" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 8); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 0 }, + Command.ColorOperation.Kind{ .palette = 0 }, op.report, ); } @@ -2332,7 +2334,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.report, ); } @@ -2340,7 +2342,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 2 }, + Command.ColorOperation.Kind{ .palette = 2 }, op.report, ); } @@ -2348,7 +2350,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 3 }, + Command.ColorOperation.Kind{ .palette = 3 }, op.report, ); } @@ -2356,7 +2358,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 4 }, + Command.ColorOperation.Kind{ .palette = 4 }, op.report, ); } @@ -2364,7 +2366,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 5 }, + Command.ColorOperation.Kind{ .palette = 5 }, op.report, ); } @@ -2372,7 +2374,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 6 }, + Command.ColorOperation.Kind{ .palette = 6 }, op.report, ); } @@ -2380,7 +2382,7 @@ test "OSC: OSC4: multiple get 8a" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 7 }, + Command.ColorOperation.Kind{ .palette = 7 }, op.report, ); } @@ -2399,14 +2401,14 @@ test "OSC: OSC4: multiple get 8b" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 8); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 8 }, + Command.ColorOperation.Kind{ .palette = 8 }, op.report, ); } @@ -2414,7 +2416,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 9 }, + Command.ColorOperation.Kind{ .palette = 9 }, op.report, ); } @@ -2422,7 +2424,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 10 }, + Command.ColorOperation.Kind{ .palette = 10 }, op.report, ); } @@ -2430,7 +2432,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 11 }, + Command.ColorOperation.Kind{ .palette = 11 }, op.report, ); } @@ -2438,7 +2440,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 12 }, + Command.ColorOperation.Kind{ .palette = 12 }, op.report, ); } @@ -2446,7 +2448,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 13 }, + Command.ColorOperation.Kind{ .palette = 13 }, op.report, ); } @@ -2454,7 +2456,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 14 }, + Command.ColorOperation.Kind{ .palette = 14 }, op.report, ); } @@ -2462,7 +2464,7 @@ test "OSC: OSC4: multiple get 8b" { const op = it.next().?; try testing.expect(op.* == .report); try testing.expectEqual( - Command.ColorKind{ .palette = 15 }, + Command.ColorOperation.Kind{ .palette = 15 }, op.report, ); } @@ -2480,14 +2482,14 @@ test "OSC: OSC4: set with invalid index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 1 }, + Command.ColorOperation.Kind{ .palette = 1 }, op.set.kind, ); try testing.expectEqual( @@ -2509,14 +2511,14 @@ test "OSC: OSC4: mix get/set palette color" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_4); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .set); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.set.kind, ); try testing.expectEqual( @@ -2527,7 +2529,7 @@ test "OSC: OSC4: mix get/set palette color" { { const op = it.next().?; try testing.expect(op.* == .report); - try testing.expectEqual(Command.ColorKind{ .palette = 254 }, op.report); + try testing.expectEqual(Command.ColorOperation.Kind{ .palette = 254 }, op.report); } try testing.expect(it.next() == null); } @@ -2543,14 +2545,14 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.reset, ); } @@ -2568,14 +2570,14 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 2); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 17 }, + Command.ColorOperation.Kind{ .palette = 17 }, op.reset, ); } @@ -2583,7 +2585,7 @@ test "OSC: OSC104: reset palette color 2" { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 111 }, + Command.ColorOperation.Kind{ .palette = 111 }, op.reset, ); } @@ -2601,14 +2603,14 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 111 }, + Command.ColorOperation.Kind{ .palette = 111 }, op.reset, ); } @@ -2626,14 +2628,14 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .osc_104); + try testing.expect(cmd.color_operation.source == .get_set_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - Command.ColorKind{ .palette = 111 }, + Command.ColorOperation.Kind{ .palette = 111 }, op.reset, ); } diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ca16b0bd2..554a87805 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1197,8 +1197,8 @@ pub const StreamHandler = struct { pub fn handleColorOperation( self: *StreamHandler, - source: terminal.osc.Command.ColorOperationSource, - operations: *const terminal.osc.Command.ColorOperationList, + source: terminal.osc.Command.ColorOperation.Source, + operations: *const terminal.osc.Command.ColorOperation.List, terminator: terminal.osc.Terminator, ) !void { // return early if there is nothing to do From 5fb32fd8a0d43412cf9375ad5f1fe850f23810ca Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 11:37:34 -0500 Subject: [PATCH 317/642] OSC: add comptime check for size of OSC Command --- src/terminal/osc.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 67f665f1a..63d3e4c6b 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -189,7 +189,7 @@ pub const Command = union(enum) { } }; - pub const List = std.SegmentedList(ColorOperation, 4); + pub const List = std.SegmentedList(ColorOperation, 2); pub const Kind = union(enum) { palette: u8, @@ -213,6 +213,11 @@ pub const Command = union(enum) { indeterminate, pause, }; + + comptime { + assert(@sizeOf(Command) == 64); + // @compileLog(@sizeOf(Command)); + } }; /// The terminator used to end an OSC command. For OSC commands that demand From f0fc82c80f070937234198f6404e3626c514ad9f Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 12:12:28 -0500 Subject: [PATCH 318/642] OSC: account for 32-bit systems in comptime Command size check --- src/terminal/osc.zig | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 63d3e4c6b..8ca4326c5 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -215,7 +215,11 @@ pub const Command = union(enum) { }; comptime { - assert(@sizeOf(Command) == 64); + assert(@sizeOf(Command) == switch (@sizeOf(usize)) { + 4 => 44, + 8 => 64, + else => unreachable, + }); // @compileLog(@sizeOf(Command)); } }; From 1104993c940a26dcf3baad6917e988ea0c913cfb Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 16:42:55 -0500 Subject: [PATCH 319/642] OSC: move some processing back inside the OSC state machine --- src/terminal/osc.zig | 423 ++++++++++++++++++++++++------------------- 1 file changed, 238 insertions(+), 185 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 8ca4326c5..d0b59e834 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -345,7 +345,8 @@ pub const Parser = struct { clipboard_kind_end, // Get/set color palette index - osc_4, + osc_4_index, + osc_4_color, // Get/set foreground color osc_10, @@ -359,15 +360,6 @@ pub const Parser = struct { // Reset color palette index osc_104, - // Reset foreground color - osc_110, - - // Reset background color - osc_111, - - // Reset cursor color - osc_112, - // Hyperlinks hyperlink_param_key, hyperlink_param_value, @@ -547,6 +539,11 @@ pub const Parser = struct { self.state = .invalid; break :osc_10; } + self.command = .{ + .color_operation = .{ + .source = .get_set_foreground, + }, + }; self.state = .osc_10; self.buf_start = self.buf_idx; self.complete = true; @@ -555,7 +552,10 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_10 => {}, + .osc_10, .osc_11, .osc_12 => switch (c) { + ';' => self.parseOSC101112(false), + else => {}, + }, .@"104" => switch (c) { ';' => osc_104: { @@ -564,6 +564,11 @@ pub const Parser = struct { self.state = .invalid; break :osc_104; } + self.command = .{ + .color_operation = .{ + .source = .reset_palette, + }, + }; self.state = .osc_104; self.buf_start = self.buf_idx; self.complete = true; @@ -571,7 +576,10 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_104 => {}, + .osc_104 => switch (c) { + ';' => self.parseOSC104(false), + else => {}, + }, .@"11" => switch (c) { ';' => osc_11: { @@ -580,51 +588,52 @@ pub const Parser = struct { self.state = .invalid; break :osc_11; } + self.command = .{ + .color_operation = .{ + .source = .get_set_background, + }, + }; self.state = .osc_11; self.buf_start = self.buf_idx; self.complete = true; }, - '0' => osc_110: { + '0'...'2' => blk: { if (self.alloc == null) { - log.warn("OSC 110 requires an allocator, but none was provided", .{}); + log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); self.state = .invalid; - break :osc_110; + break :blk; } - self.state = .osc_110; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '1' => osc_111: { - if (self.alloc == null) { - log.warn("OSC 111 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_111; - } - self.state = .osc_111; - self.buf_start = self.buf_idx; - self.complete = true; - }, - '2' => osc_112: { - if (self.alloc == null) { - log.warn("OSC 112 requires an allocator, but none was provided", .{}); - self.state = .invalid; - break :osc_112; - } - self.state = .osc_112; - self.buf_start = self.buf_idx; + + const alloc = self.alloc orelse return; + + self.command = .{ + .color_operation = .{ + .source = switch (c) { + '0' => .reset_foreground, + '1' => .reset_background, + '2' => .reset_cursor, + else => unreachable, + }, + }, + }; + const op = self.command.color_operation.operations.addOne(alloc) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + op.* = .{ + .reset = switch (c) { + '0' => .foreground, + '1' => .background, + '2' => .cursor, + else => unreachable, + }, + }; + self.state = .swallow; self.complete = true; }, else => self.state = .invalid, }, - .osc_11 => {}, - - .osc_110 => {}, - - .osc_111 => {}, - - .osc_112 => {}, - .@"12" => switch (c) { ';' => osc_12: { if (self.alloc == null) { @@ -632,6 +641,11 @@ pub const Parser = struct { self.state = .invalid; break :osc_12; } + self.command = .{ + .color_operation = .{ + .source = .get_set_cursor, + }, + }; self.state = .osc_12; self.buf_start = self.buf_idx; self.complete = true; @@ -639,8 +653,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_12 => {}, - .@"13" => switch (c) { '3' => self.state = .@"133", else => self.state = .invalid, @@ -728,14 +740,30 @@ pub const Parser = struct { self.state = .invalid; break :osc_4; } - self.state = .osc_4; + self.command = .{ + .color_operation = .{ + .source = .get_set_palette, + }, + }; + self.state = .osc_4_index; self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, - .osc_4 => {}, + .osc_4_index => switch (c) { + ';' => self.state = .osc_4_color, + else => {}, + }, + + .osc_4_color => switch (c) { + ';' => { + self.parseOSC4(false); + self.state = .osc_4_index; + }, + else => {}, + }, .@"5" => switch (c) { '2' => self.state = .@"52", @@ -1329,85 +1357,104 @@ pub const Parser = struct { self.temp_state.str.* = list.items; } - fn parseOSC4(self: *Parser) void { - assert(self.state == .osc_4); + fn parseOSC4(self: *Parser, final: bool) void { + assert(self.state == .osc_4_color); + assert(self.command == .color_operation); + assert(self.command.color_operation.source == .get_set_palette); const alloc = self.alloc orelse return; + const operations = &self.command.color_operation.operations; - self.command = .{ - .color_operation = .{ - .source = .get_set_palette, + const str = self.buf[self.buf_start .. self.buf_idx - (1 - @intFromBool(final))]; + self.buf_start = 0; + self.buf_idx = 0; + + var it = std.mem.splitScalar(u8, str, ';'); + const index_str = it.next() orelse { + log.warn("OSC 4 is missing palette index", .{}); + return; + }; + const spec_str = it.next() orelse { + log.warn("OSC 4 is missing color spec", .{}); + return; + }; + const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => { + log.warn("invalid color palette index in OSC 4: {s} {}", .{ index_str, err }); + return; }, }; - - const str = self.buf[self.buf_start..self.buf_idx]; - var it = std.mem.splitScalar(u8, str, ';'); - while (it.next()) |index_str| { - const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => { - log.warn("invalid color palette index in OSC 4: {s} {}", .{ index_str, err }); - // skip any color spec - _ = it.next(); - continue; + if (std.mem.eql(u8, spec_str, "?")) { + const op = operations.addOne(alloc) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + op.* = .{ + .report = .{ .palette = index }, + }; + } else { + const color = RGB.parse(spec_str) catch |err| { + log.warn("invalid color specification in OSC 4: '{s}' {}", .{ spec_str, err }); + return; + }; + const op = operations.addOne(alloc) catch |err| { + log.warn("unable to append color operation: {}", .{err}); + return; + }; + op.* = .{ + .set = .{ + .kind = .{ + .palette = index, + }, + .color = color, }, }; - const spec_str = it.next() orelse continue; - if (std.mem.eql(u8, spec_str, "?")) { - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .report = .{ .palette = index }, - }; - } else { - const color = RGB.parse(spec_str) catch |err| { - log.warn("invalid color specification in OSC 4: {s} {}", .{ spec_str, err }); - continue; - }; - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .set = .{ - .kind = .{ - .palette = index, - }, - .color = color, - }, - }; - } } } - fn parseOSC101112(self: *Parser) void { + fn parseOSC101112(self: *Parser, final: bool) void { assert(switch (self.state) { .osc_10, .osc_11, .osc_12 => true, else => false, }); + assert(self.command == .color_operation); + assert(self.command.color_operation.source == switch (self.state) { + .osc_10 => Command.ColorOperation.Source.get_set_foreground, + .osc_11 => Command.ColorOperation.Source.get_set_background, + .osc_12 => Command.ColorOperation.Source.get_set_cursor, + else => unreachable, + }); + + const spec_str = self.buf[self.buf_start .. self.buf_idx - (1 - @intFromBool(final))]; + + if (self.command.color_operation.operations.count() > 0) { + // don't emit the warning if the string is empty + if (spec_str.len == 0) return; + + log.warn("OSC 1{s} can only accept 1 color", .{switch (self.state) { + .osc_10 => "0", + .osc_11 => "1", + .osc_12 => "2", + else => unreachable, + }}); + return; + } + + if (spec_str.len == 0) { + log.warn("OSC 1{s} requires an argument", .{switch (self.state) { + .osc_10 => "0", + .osc_11 => "1", + .osc_12 => "2", + else => unreachable, + }}); + return; + } const alloc = self.alloc orelse return; + const operations = &self.command.color_operation.operations; - self.command = .{ - .color_operation = .{ - .source = switch (self.state) { - .osc_10 => .get_set_foreground, - .osc_11 => .get_set_background, - .osc_12 => .get_set_cursor, - else => unreachable, - }, - }, - }; - const str = self.buf[self.buf_start..self.buf_idx]; - var it = std.mem.splitScalar(u8, str, ';'); - const color_str = it.next() orelse { - log.warn("OSC 10/11/12 requires an argument", .{}); - self.state = .invalid; - return; - }; - if (std.mem.eql(u8, color_str, "?")) { - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { + if (std.mem.eql(u8, spec_str, "?")) { + const op = operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; @@ -1420,11 +1467,20 @@ pub const Parser = struct { }, }; } else { - const color = RGB.parse(color_str) catch |err| { - log.warn("invalid color specification in OSC 10/11/12: {s} {}", .{ color_str, err }); + const color = RGB.parse(spec_str) catch |err| { + log.warn("invalid color specification in OSC 1{s}: {s} {}", .{ + switch (self.state) { + .osc_10 => "0", + .osc_11 => "1", + .osc_12 => "2", + else => unreachable, + }, + spec_str, + err, + }); return; }; - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { + const op = operations.addOne(alloc) catch |err| { log.warn("unable to append color operation: {}", .{err}); return; }; @@ -1442,22 +1498,21 @@ pub const Parser = struct { } } - fn parseOSC110111112(self: *Parser) void { - assert(switch (self.state) { - .osc_110, .osc_111, .osc_112 => true, - else => false, - }); + fn parseOSC104(self: *Parser, final: bool) void { + assert(self.state == .osc_104); + assert(self.command == .color_operation); + assert(self.command.color_operation.source == .reset_palette); const alloc = self.alloc orelse return; - self.command = .{ - .color_operation = .{ - .source = switch (self.state) { - .osc_110 => .reset_foreground, - .osc_111 => .reset_background, - .osc_112 => .reset_cursor, - else => unreachable, - }, + const index_str = self.buf[self.buf_start .. self.buf_idx - (1 - @intFromBool(final))]; + self.buf_start = 0; + self.buf_idx = 0; + + const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { + error.Overflow, error.InvalidCharacter => { + log.warn("invalid color palette index in OSC 104: {s} {}", .{ index_str, err }); + return; }, }; const op = self.command.color_operation.operations.addOne(alloc) catch |err| { @@ -1465,45 +1520,10 @@ pub const Parser = struct { return; }; op.* = .{ - .reset = switch (self.state) { - .osc_110 => .foreground, - .osc_111 => .background, - .osc_112 => .cursor, - else => unreachable, - }, + .reset = .{ .palette = index }, }; } - fn parseOSC104(self: *Parser) void { - assert(self.state == .osc_104); - - const alloc = self.alloc orelse return; - - self.command = .{ - .color_operation = .{ - .source = .get_set_palette, - }, - }; - - const str = self.buf[self.buf_start..self.buf_idx]; - var it = std.mem.splitScalar(u8, str, ';'); - while (it.next()) |index_str| { - const index = std.fmt.parseUnsigned(u8, index_str, 10) catch |err| switch (err) { - error.Overflow, error.InvalidCharacter => { - log.warn("invalid color palette index in OSC 104: {s} {}", .{ index_str, err }); - continue; - }, - }; - const op = self.command.color_operation.operations.addOne(alloc) catch |err| { - log.warn("unable to append color operation: {}", .{err}); - return; - }; - op.* = .{ - .reset = .{ .palette = index }, - }; - } - } - /// End the sequence and return the command, if any. If the return value /// is null, then no valid command was found. The optional terminator_ch /// is the final character in the OSC sequence. This is used to determine @@ -1527,18 +1547,9 @@ pub const Parser = struct { .allocable_string => self.endAllocableString(), .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), - .osc_4, - => self.parseOSC4(), - .osc_10, - .osc_11, - .osc_12, - => self.parseOSC101112(), - .osc_104, - => self.parseOSC104(), - .osc_110, - .osc_111, - .osc_112, - => self.parseOSC110111112(), + .osc_4_color => self.parseOSC4(true), + .osc_10, .osc_11, .osc_12 => self.parseOSC101112(true), + .osc_104 => self.parseOSC104(true), else => {}, } @@ -1838,10 +1849,7 @@ test "OSC: OSC112: reset cursor color with semicolon" { defer p.deinit(); const input = "112;"; - for (input) |ch| { - log.warn("feeding {c} {s}", .{ ch, @tagName(p.state) }); - p.next(ch); - } + for (input) |ch| p.next(ch); log.warn("finish: {s}", .{@tagName(p.state)}); const cmd = p.end(0x07).?; @@ -2538,7 +2546,52 @@ test "OSC: OSC4: mix get/set palette color" { { const op = it.next().?; try testing.expect(op.* == .report); - try testing.expectEqual(Command.ColorOperation.Kind{ .palette = 254 }, op.report); + try testing.expectEqual( + Command.ColorOperation.Kind{ .palette = 254 }, + op.report, + ); + } + try testing.expect(it.next() == null); +} + +test "OSC: OSC4: incomplete color/spec 1" { + const testing = std.testing; + + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); + + const input = "4;17"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.operations.count() == 0); + var it = cmd.color_operation.operations.constIterator(0); + try testing.expect(it.next() == null); +} + +test "OSC: OSC4: incomplete color/spec 2" { + const testing = std.testing; + + var p: Parser = .{ .alloc = testing.allocator }; + defer p.deinit(); + + const input = "4;17;?;42"; + for (input) |ch| p.next(ch); + + const cmd = p.end('\x1b').?; + try testing.expect(cmd == .color_operation); + try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.operations.count() == 1); + var it = cmd.color_operation.operations.constIterator(0); + { + const op = it.next().?; + try testing.expect(op.* == .report); + try testing.expectEqual( + Command.ColorOperation.Kind{ .palette = 17 }, + op.report, + ); } try testing.expect(it.next() == null); } @@ -2554,7 +2607,7 @@ test "OSC: OSC104: reset palette color 1" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.source == .reset_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { @@ -2579,8 +2632,8 @@ test "OSC: OSC104: reset palette color 2" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); - try testing.expect(cmd.color_operation.operations.count() == 2); + try testing.expect(cmd.color_operation.source == .reset_palette); + try testing.expectEqual(2, cmd.color_operation.operations.count()); var it = cmd.color_operation.operations.constIterator(0); { const op = it.next().?; @@ -2612,7 +2665,7 @@ test "OSC: OSC104: invalid palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.source == .reset_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { @@ -2637,7 +2690,7 @@ test "OSC: OSC104: empty palette index" { const cmd = p.end('\x1b').?; try testing.expect(cmd == .color_operation); - try testing.expect(cmd.color_operation.source == .get_set_palette); + try testing.expect(cmd.color_operation.source == .reset_palette); try testing.expect(cmd.color_operation.operations.count() == 1); var it = cmd.color_operation.operations.constIterator(0); { From d3cb6d0d41835f9e57d4dca6b927440e0f505bb4 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 29 May 2025 15:45:51 -0500 Subject: [PATCH 320/642] GTK: add action to show the GTK inspector The default keybinds for showing the GTK inspector (`ctrl+shift+i` and `ctrl+shift+d`) don't work reliably in Ghostty due to the way Ghostty handles input. You can show the GTK inspector by setting the environment variable `GTK_DEBUG` to `interactive` before starting Ghostty but that's not always convenient. This adds a keybind action that will show the GTK inspector. Due to API limitations toggling the GTK inspector using the keybind action is impractical because GTK does not provide a convenient API to determine if the GTK inspector is already showing. Thus we limit ourselves to strictly showing the GTK inspector. To close the GTK inspector the user must click the close button on the GTK inspector window. If the GTK inspector window is already visible but is hidden, calling the keybind action will not bring the GTK inspector window to the front. --- include/ghostty.h | 1 + .../TerminalCommandPalette.swift | 3 ++- src/App.zig | 1 + src/apprt/action.zig | 4 ++++ src/apprt/glfw.zig | 1 + src/apprt/gtk/App.zig | 19 +++++++++++++++++++ src/input/Binding.zig | 4 ++++ src/input/command.zig | 6 ++++++ 8 files changed, 38 insertions(+), 1 deletion(-) diff --git a/include/ghostty.h b/include/ghostty.h index 950f5ef80..6b1625a30 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -653,6 +653,7 @@ typedef enum { GHOSTTY_ACTION_INITIAL_SIZE, GHOSTTY_ACTION_CELL_SIZE, GHOSTTY_ACTION_INSPECTOR, + GHOSTTY_ACTION_SHOW_GTK_INSPECTOR, GHOSTTY_ACTION_RENDER_INSPECTOR, GHOSTTY_ACTION_DESKTOP_NOTIFICATION, GHOSTTY_ACTION_SET_TITLE, diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 57a76dd43..47f2baf23 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -29,7 +29,8 @@ struct TerminalCommandPaletteView: View { let key = String(cString: c.action_key) switch (key) { case "toggle_tab_overview", - "toggle_window_decorations": + "toggle_window_decorations", + "show_gtk_inspector": return false default: return true diff --git a/src/App.zig b/src/App.zig index 005b745a6..39db2e2f9 100644 --- a/src/App.zig +++ b/src/App.zig @@ -445,6 +445,7 @@ pub fn performAction( .toggle_quick_terminal => _ = try rt_app.performAction(.app, .toggle_quick_terminal, {}), .toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}), .check_for_updates => _ = try rt_app.performAction(.app, .check_for_updates, {}), + .show_gtk_inspector => _ = try rt_app.performAction(.app, .show_gtk_inspector, {}), } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 8a23bc1a4..7866db182 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -165,6 +165,9 @@ pub const Action = union(Key) { /// Control whether the inspector is shown or hidden. inspector: Inspector, + /// Show the GTK inspector. + show_gtk_inspector, + /// The inspector for the given target has changes and should be /// rendered at the next opportunity. render_inspector, @@ -284,6 +287,7 @@ pub const Action = union(Key) { initial_size, cell_size, inspector, + show_gtk_inspector, render_inspector, desktop_notification, set_title, diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index 221d5344a..d67567aee 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -250,6 +250,7 @@ pub const App = struct { .reset_window_size, .ring_bell, .check_for_updates, + .show_gtk_inspector, => { log.info("unimplemented action={}", .{action}); return false; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 55c0be5e0..d1c8f2c59 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -481,6 +481,7 @@ pub fn performAction( .config_change => self.configChange(target, value.config), .reload_config => try self.reloadConfig(target, value), .inspector => self.controlInspector(target, value), + .show_gtk_inspector => self.showGTKInspector(), .desktop_notification => self.showDesktopNotification(target, value), .set_title => try self.setTitle(target, value), .pwd => try self.setPwd(target, value), @@ -687,6 +688,12 @@ fn controlInspector( surface.controlInspector(mode); } +fn showGTKInspector( + _: *const App, +) void { + gtk.Window.setInteractiveDebugging(@intFromBool(true)); +} + fn toggleMaximize(_: *App, target: apprt.Target) void { switch (target) { .app => {}, @@ -1060,6 +1067,7 @@ fn syncActionAccelerators(self: *App) !void { try self.syncActionAccelerator("app.open-config", .{ .open_config = {} }); try self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} }); try self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle }); + try self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector); try self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette); try self.syncActionAccelerator("win.close", .{ .close_window = {} }); try self.syncActionAccelerator("win.new-window", .{ .new_window = {} }); @@ -1655,6 +1663,16 @@ fn gtkActionPresentSurface( ); } +fn gtkActionShowGTKInspector( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *App, +) callconv(.c) void { + self.core_app.performAction(self, .show_gtk_inspector) catch |err| { + log.err("error showing GTK inspector err={}", .{err}); + }; +} + /// This is called to setup the action map that this application supports. /// This should be called only once on startup. fn initActions(self: *App) void { @@ -1673,6 +1691,7 @@ fn initActions(self: *App) void { .{ "open-config", gtkActionOpenConfig, null }, .{ "reload-config", gtkActionReloadConfig, null }, .{ "present-surface", gtkActionPresentSurface, t }, + .{ "show-gtk-inspector", gtkActionShowGTKInspector, null }, }; inline for (actions) |entry| { const action = gio.SimpleAction.new(entry[0], entry[2]); diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 3818d99a6..4a5fb4522 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -397,6 +397,9 @@ pub const Action = union(enum) { /// keybind = cmd+i=inspector:toggle inspector: InspectorMode, + /// Show the GTK inspector. + show_gtk_inspector, + /// Open the configuration file in the default OS editor. If your default OS /// editor isn't configured then this will fail. Currently, any failures to /// open the configuration will show up only in the logs. @@ -795,6 +798,7 @@ pub const Action = union(enum) { .toggle_quick_terminal, .toggle_visibility, .check_for_updates, + .show_gtk_inspector, => .app, // These are app but can be special-cased in a surface context. diff --git a/src/input/command.zig b/src/input/command.zig index 8ef4a5f0e..53d1b6b3d 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -298,6 +298,12 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Toggle the inspector.", }}, + .show_gtk_inspector => comptime &.{.{ + .action = .show_gtk_inspector, + .title = "Show the GTK Inspector", + .description = "Show the GTK inspector.", + }}, + .open_config => comptime &.{.{ .action = .open_config, .title = "Open Config", From 0f1860f066cff0f4f93fa8d7bbe161cda3f3cb98 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 May 2025 14:47:29 -0700 Subject: [PATCH 321/642] build: use a libc txt file to point to correct Apple SDK This fixes an issue where Ghostty would not build against the macOS 15.5 SDK. What was happening was that Zig was adding its embedded libc paths to the clang command line, which included old headers that were incompatible with the latest (macOS 15.5) SDK. Ghostty was adding the newer paths but they were being overridden by the embedded libc paths. The reason this was happening is because Zig was using its own logic to find the libc paths and this was colliding with the paths we were setting manually. To fix this, we now use a `libc.txt` file that explicitly tells Zig where to find libc, and we base this on our own SDK search logic. --- pkg/apple-sdk/build.zig | 91 +++++++++++++++++++++++++++++++-------- pkg/breakpad/build.zig | 2 +- pkg/cimgui/build.zig | 3 +- pkg/freetype/build.zig | 2 +- pkg/glfw/build.zig | 5 +-- pkg/glslang/build.zig | 6 +-- pkg/harfbuzz/build.zig | 3 +- pkg/highway/build.zig | 3 +- pkg/libintl/build.zig | 2 +- pkg/libpng/build.zig | 2 +- pkg/macos/build.zig | 5 +-- pkg/oniguruma/build.zig | 2 +- pkg/sentry/build.zig | 3 +- pkg/simdutf/build.zig | 2 +- pkg/spirv-cross/build.zig | 2 +- pkg/utfcpp/build.zig | 2 +- pkg/wuffs/build.zig | 5 --- pkg/zlib/build.zig | 2 +- src/build/SharedDeps.zig | 2 +- 19 files changed, 92 insertions(+), 52 deletions(-) diff --git a/pkg/apple-sdk/build.zig b/pkg/apple-sdk/build.zig index 1be733dd6..18a6c0968 100644 --- a/pkg/apple-sdk/build.zig +++ b/pkg/apple-sdk/build.zig @@ -7,12 +7,17 @@ pub fn build(b: *std.Build) !void { _ = optimize; } -/// Add the SDK framework, include, and library paths to the given module. -/// The module target is used to determine the SDK to use so it must have -/// a resolved target. -pub fn addPaths(b: *std.Build, m: *std.Build.Module) !void { +/// Setup the step to point to the proper Apple SDK for libc and +/// frameworks. This expects and relies on the native SDK being +/// installed on the system. Ghostty doesn't support cross-compilation +/// for Apple platforms. +pub fn addPaths( + b: *std.Build, + step: *std.Build.Step.Compile, +) !void { // The cache. This always uses b.allocator and never frees memory - // (which is idiomatic for a Zig build exe). + // (which is idiomatic for a Zig build exe). We cache the libc txt + // file we create because it is expensive to generate (subprocesses). const Cache = struct { const Key = struct { arch: std.Target.Cpu.Arch, @@ -20,27 +25,72 @@ pub fn addPaths(b: *std.Build, m: *std.Build.Module) !void { abi: std.Target.Abi, }; - var map: std.AutoHashMapUnmanaged(Key, ?[]const u8) = .{}; + var map: std.AutoHashMapUnmanaged(Key, ?struct { + libc: std.Build.LazyPath, + framework: []const u8, + system_include: []const u8, + library: []const u8, + }) = .{}; }; - const target = m.resolved_target.?.result; + const target = step.rootModuleTarget(); const gop = try Cache.map.getOrPut(b.allocator, .{ .arch = target.cpu.arch, .os = target.os.tag, .abi = target.abi, }); - // This executes `xcrun` to get the SDK path. We don't want to execute - // this multiple times so we cache the value. if (!gop.found_existing) { - gop.value_ptr.* = std.zig.system.darwin.getSdk( - b.allocator, - m.resolved_target.?.result, - ); + // Detect our SDK using the "findNative" Zig stdlib function. + // This is really important because it forces using `xcrun` to + // find the SDK path. + const libc = try std.zig.LibCInstallation.findNative(.{ + .allocator = b.allocator, + .target = step.rootModuleTarget(), + .verbose = false, + }); + + // Render the file compatible with the `--libc` Zig flag. + var list: std.ArrayList(u8) = .init(b.allocator); + defer list.deinit(); + try libc.render(list.writer()); + + // Create a temporary file to store the libc path because + // `--libc` expects a file path. + const wf = b.addWriteFiles(); + const path = wf.add("libc.txt", list.items); + + // Determine our framework path. Zig has a bug where it doesn't + // parse this from the libc txt file for `-framework` flags: + // https://github.com/ziglang/zig/issues/24024 + const framework_path = framework: { + const down1 = std.fs.path.dirname(libc.sys_include_dir.?).?; + const down2 = std.fs.path.dirname(down1).?; + break :framework try std.fs.path.join(b.allocator, &.{ + down2, + "System", + "Library", + "Frameworks", + }); + }; + + const library_path = library: { + const down1 = std.fs.path.dirname(libc.sys_include_dir.?).?; + break :library try std.fs.path.join(b.allocator, &.{ + down1, + "lib", + }); + }; + + gop.value_ptr.* = .{ + .libc = path, + .framework = framework_path, + .system_include = libc.sys_include_dir.?, + .library = library_path, + }; } - // The active SDK we want to use - const path = gop.value_ptr.* orelse return switch (target.os.tag) { + const value = gop.value_ptr.* orelse return switch (target.os.tag) { // Return a more descriptive error. Before we just returned the // generic error but this was confusing a lot of community members. // It costs us nothing in the build script to return something better. @@ -50,7 +100,12 @@ pub fn addPaths(b: *std.Build, m: *std.Build.Module) !void { .watchos => error.XcodeWatchOSSDKNotFound, else => error.XcodeAppleSDKNotFound, }; - m.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ path, "/System/Library/Frameworks" }) }); - m.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ path, "/usr/include" }) }); - m.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ path, "/usr/lib" }) }); + + step.setLibCFile(value.libc); + + // This is only necessary until this bug is fixed: + // https://github.com/ziglang/zig/issues/24024 + step.root_module.addSystemFrameworkPath(.{ .cwd_relative = value.framework }); + step.root_module.addSystemIncludePath(.{ .cwd_relative = value.system_include }); + step.root_module.addLibraryPath(.{ .cwd_relative = value.library }); } diff --git a/pkg/breakpad/build.zig b/pkg/breakpad/build.zig index e2fdec7ad..42247b12c 100644 --- a/pkg/breakpad/build.zig +++ b/pkg/breakpad/build.zig @@ -13,7 +13,7 @@ pub fn build(b: *std.Build) !void { lib.addIncludePath(b.path("vendor")); if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/cimgui/build.zig b/pkg/cimgui/build.zig index c76b53966..3ca735383 100644 --- a/pkg/cimgui/build.zig +++ b/pkg/cimgui/build.zig @@ -84,8 +84,7 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag.isDarwin()) { if (!target.query.isNative()) { - try @import("apple_sdk").addPaths(b, lib.root_module); - try @import("apple_sdk").addPaths(b, module); + try @import("apple_sdk").addPaths(b, lib); } lib.addCSourceFile(.{ .file = imgui.path("backends/imgui_impl_metal.mm"), diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index bfe27e5aa..e9f72210a 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -69,7 +69,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu lib.linkLibC(); if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/glfw/build.zig b/pkg/glfw/build.zig index cc61f18b2..142a558da 100644 --- a/pkg/glfw/build.zig +++ b/pkg/glfw/build.zig @@ -24,7 +24,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); if (target.result.os.tag.isDarwin()) { - try apple_sdk.addPaths(b, exe.root_module); + try apple_sdk.addPaths(b, exe); } const tests_run = b.addRunArtifact(exe); @@ -122,8 +122,7 @@ fn buildLib( }, .macos => { - try apple_sdk.addPaths(b, lib.root_module); - try apple_sdk.addPaths(b, module); + try apple_sdk.addPaths(b, lib); // Transitive dependencies, explicit linkage of these works around // ziglang/zig#17130 diff --git a/pkg/glslang/build.zig b/pkg/glslang/build.zig index 629490aa4..747216a39 100644 --- a/pkg/glslang/build.zig +++ b/pkg/glslang/build.zig @@ -16,10 +16,6 @@ pub fn build(b: *std.Build) !void { module.addIncludePath(upstream.path("")); module.addIncludePath(b.path("override")); - if (target.result.os.tag.isDarwin()) { - const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, module); - } if (target.query.isNative()) { const test_exe = b.addTest(.{ @@ -55,7 +51,7 @@ fn buildGlslang( lib.addIncludePath(b.path("override")); if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/harfbuzz/build.zig b/pkg/harfbuzz/build.zig index d0dd6d01c..3bdc30a32 100644 --- a/pkg/harfbuzz/build.zig +++ b/pkg/harfbuzz/build.zig @@ -93,8 +93,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu lib.linkLibCpp(); if (target.result.os.tag.isDarwin()) { - try apple_sdk.addPaths(b, lib.root_module); - try apple_sdk.addPaths(b, module); + try apple_sdk.addPaths(b, lib); } const dynamic_link_opts = options.dynamic_link_opts; diff --git a/pkg/highway/build.zig b/pkg/highway/build.zig index c72ca355f..5036316da 100644 --- a/pkg/highway/build.zig +++ b/pkg/highway/build.zig @@ -23,8 +23,7 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); - try apple_sdk.addPaths(b, module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/libintl/build.zig b/pkg/libintl/build.zig index 53eb67f16..1baed195a 100644 --- a/pkg/libintl/build.zig +++ b/pkg/libintl/build.zig @@ -40,7 +40,7 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } if (b.lazyDependency("gettext", .{})) |upstream| { diff --git a/pkg/libpng/build.zig b/pkg/libpng/build.zig index d012f2712..8729398f8 100644 --- a/pkg/libpng/build.zig +++ b/pkg/libpng/build.zig @@ -15,7 +15,7 @@ pub fn build(b: *std.Build) !void { } if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } // For dynamic linking, we prefer dynamic linking and to search by diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index 911664a2f..df76da9b4 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -45,8 +45,7 @@ pub fn build(b: *std.Build) !void { module.linkFramework("CoreVideo", .{}); module.linkFramework("QuartzCore", .{}); - try apple_sdk.addPaths(b, lib.root_module); - try apple_sdk.addPaths(b, module); + try apple_sdk.addPaths(b, lib); } b.installArtifact(lib); @@ -58,7 +57,7 @@ pub fn build(b: *std.Build) !void { .optimize = optimize, }); if (target.result.os.tag.isDarwin()) { - try apple_sdk.addPaths(b, test_exe.root_module); + try apple_sdk.addPaths(b, test_exe); } test_exe.linkLibrary(lib); diff --git a/pkg/oniguruma/build.zig b/pkg/oniguruma/build.zig index 1c93bbf9a..c23d744df 100644 --- a/pkg/oniguruma/build.zig +++ b/pkg/oniguruma/build.zig @@ -67,7 +67,7 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } if (b.lazyDependency("oniguruma", .{})) |upstream| { diff --git a/pkg/sentry/build.zig b/pkg/sentry/build.zig index 3c0019710..0e6993ad4 100644 --- a/pkg/sentry/build.zig +++ b/pkg/sentry/build.zig @@ -20,8 +20,7 @@ pub fn build(b: *std.Build) !void { lib.linkLibC(); if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); - try apple_sdk.addPaths(b, module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/simdutf/build.zig b/pkg/simdutf/build.zig index 859653443..30de40fea 100644 --- a/pkg/simdutf/build.zig +++ b/pkg/simdutf/build.zig @@ -14,7 +14,7 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/spirv-cross/build.zig b/pkg/spirv-cross/build.zig index c7d0d2039..ff67e3e72 100644 --- a/pkg/spirv-cross/build.zig +++ b/pkg/spirv-cross/build.zig @@ -44,7 +44,7 @@ fn buildSpirvCross( lib.linkLibCpp(); if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/utfcpp/build.zig b/pkg/utfcpp/build.zig index 6b80fec7b..8e1a3cb20 100644 --- a/pkg/utfcpp/build.zig +++ b/pkg/utfcpp/build.zig @@ -13,7 +13,7 @@ pub fn build(b: *std.Build) !void { if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } var flags = std.ArrayList([]const u8).init(b.allocator); diff --git a/pkg/wuffs/build.zig b/pkg/wuffs/build.zig index d47771c22..4d144e76a 100644 --- a/pkg/wuffs/build.zig +++ b/pkg/wuffs/build.zig @@ -11,11 +11,6 @@ pub fn build(b: *std.Build) !void { .link_libc = true, }); - if (target.result.os.tag.isDarwin()) { - const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, module); - } - const unit_tests = b.addTest(.{ .root_source_file = b.path("src/main.zig"), .target = target, diff --git a/pkg/zlib/build.zig b/pkg/zlib/build.zig index 28ae62424..28344c989 100644 --- a/pkg/zlib/build.zig +++ b/pkg/zlib/build.zig @@ -12,7 +12,7 @@ pub fn build(b: *std.Build) !void { lib.linkLibC(); if (target.result.os.tag.isDarwin()) { const apple_sdk = @import("apple_sdk"); - try apple_sdk.addPaths(b, lib.root_module); + try apple_sdk.addPaths(b, lib); } if (b.lazyDependency("zlib", .{})) |upstream| { diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index 512975ac0..d3741a358 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -377,7 +377,7 @@ pub fn add( // We always require the system SDK so that our system headers are available. // This makes things like `os/log.h` available for cross-compiling. if (step.rootModuleTarget().os.tag.isDarwin()) { - try @import("apple_sdk").addPaths(b, step.root_module); + try @import("apple_sdk").addPaths(b, step); const metallib = self.metallib.?; metallib.output.addStepDependencies(&step.step); From c5e5d61438343e888a83fe5fa190442ec5eb4534 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 May 2025 09:52:49 -0700 Subject: [PATCH 322/642] terminal: bring alt screen behaviors much closer in line with xterm This brings the behavior of mode 47, 1047, and 1049 much closer to xterm's behavior. I found that our prior implementation had many deficiencies. For example, we weren't properly copying the cursor state back to the primary screen from the alternate screen for modes 47 and 1047. And we weren't saving/restoring cursor state unconditionally for mode 1049 even if we were already in the alternate screen. These are weird, edgy behaviors that I don't think anyone expected (evidence by there being no bug reports about them), but they are bugs nontheless. Many tests added. --- src/terminal/Terminal.zig | 470 ++++++++++++++++++++++++++-------- src/termio/stream_handler.zig | 31 +-- 2 files changed, 368 insertions(+), 133 deletions(-) diff --git a/src/terminal/Terminal.zig b/src/terminal/Terminal.zig index bb6702201..be7a58f9b 100644 --- a/src/terminal/Terminal.zig +++ b/src/terminal/Terminal.zig @@ -2515,39 +2515,37 @@ pub fn getScreen(self: *Terminal, t: ScreenType) *Screen { &self.secondary_screen; } -/// Options for switching to the alternate screen. -pub const AlternateScreenOptions = struct { - cursor_save: bool = false, - clear_on_enter: bool = false, - clear_on_exit: bool = false, -}; - -/// Switch to the alternate screen buffer. +/// Switch to the given screen type (alternate or primary). /// -/// The alternate screen buffer: -/// * has its own grid -/// * has its own cursor state (included saved cursor) -/// * does not support scrollback +/// This does NOT handle behaviors such as clearing the screen, +/// copying the cursor, etc. This should be handled by downstream +/// callers. /// -pub fn alternateScreen( - self: *Terminal, - options: AlternateScreenOptions, -) void { - //log.info("alt screen active={} options={} cursor={}", .{ self.active_screen, options, self.screen.cursor }); +/// After calling this function, the `self.screen` field will point +/// to the current screen, and the returned value will be the previous +/// screen. If the return value is null, then the screen was not +/// switched because it was already the active screen. +/// +/// Note: This is written in a generic way so that we can support +/// more than two screens in the future if needed. There isn't +/// currently a spec for this, but it is something I think might +/// be useful in the future. +pub fn switchScreen(self: *Terminal, t: ScreenType) ?*Screen { + // If we're already on the requested screen we do nothing. + if (self.active_screen == t) return null; - // TODO: test - // TODO(mitchellh): what happens if we enter alternate screen multiple times? - // for now, we ignore... - if (self.active_screen == .alternate) return; - - // If we requested cursor save, we save the cursor in the primary screen - if (options.cursor_save) self.saveCursor(); + // We always end hyperlink state when switching screens. + // We need to do this on the original screen. + self.screen.endHyperlink(); // Switch the screens const old = self.screen; self.screen = self.secondary_screen; self.secondary_screen = old; - self.active_screen = .alternate; + self.active_screen = t; + + // The new screen should not have any hyperlinks set + assert(self.screen.cursor.hyperlink_id == 0); // Bring our charset state with us self.screen.charset = old.charset; @@ -2555,62 +2553,122 @@ pub fn alternateScreen( // Clear our selection self.screen.clearSelection(); - // Mark kitty images as dirty so they redraw + // Mark kitty images as dirty so they redraw. Without this set + // the images will remain where they were (the dirty bit on + // the screen only tracks the terminal grid, not the images). self.screen.kitty_images.dirty = true; - // Mark our terminal as dirty + // Mark our terminal as dirty to redraw the grid. self.flags.dirty.clear = true; - // Bring our pen with us - self.screen.cursorCopy(old.cursor, .{ - .hyperlink = false, - }) catch |err| { - log.warn("cursor copy failed entering alt screen err={}", .{err}); - }; + return &self.secondary_screen; +} - if (options.clear_on_enter) { - self.eraseDisplay(.complete, false); +/// Switch screen via a mode switch (e.g. mode 47, 1047, 1049). +/// This is a much more opinionated operation than `switchScreen` +/// since it also handles the behaviors of the specific mode, +/// such as clearing the screen, saving/restoring the cursor, +/// etc. +/// +/// This should be used for legacy compatibility with VT protocols, +/// but more modern usage should use `switchScreen` instead and handle +/// details like clearing the screen, cursor saving, etc. manually. +pub fn switchScreenMode( + self: *Terminal, + mode: SwitchScreenMode, + enabled: bool, +) void { + // The behavior in this function is completely based on reading + // the xterm source, specifically "charproc.c" for + // `srm_ALTBUF`, `srm_OPT_ALTBUF`, and `srm_OPT_ALTBUF_CURSOR`. + // We shouldn't touch anything in here without adding a unit + // test AND verifying the behavior with xterm. + + switch (mode) { + .@"47" => {}, + + // If we're disabling 1047 and we're on alt screen then + // we clear the screen. + .@"1047" => if (!enabled and self.active_screen == .alternate) { + self.eraseDisplay(.complete, false); + }, + + // 1049 unconditionally saves the cursor on enabling, even + // if we're already on the alternate screen. + .@"1049" => if (enabled) self.saveCursor(), + } + + // Switch screens first to whatever we're going to. + const to: ScreenType = if (enabled) .alternate else .primary; + const old_ = self.switchScreen(to); + + switch (mode) { + // For these modes, we need to copy the cursor. We only copy + // the cursor if the screen actually changed, otherwise the + // cursor is already copied. The cursor is copied regardless + // of destination screen. + .@"47", .@"1047" => if (old_) |old| { + self.screen.cursorCopy(old.cursor, .{ + .hyperlink = false, + }) catch |err| { + log.warn( + "cursor copy failed entering alt screen err={}", + .{err}, + ); + }; + }, + + // Mode 1049 restores cursor on the primary screen when + // we disable it. + .@"1049" => if (enabled) { + assert(self.active_screen == .alternate); + self.eraseDisplay(.complete, false); + + // When we enter alt screen with 1049, we always copy the + // cursor from the primary screen (if we weren't already + // on it). + if (old_) |old| { + self.screen.cursorCopy(old.cursor, .{ + .hyperlink = false, + }) catch |err| { + log.warn( + "cursor copy failed entering alt screen err={}", + .{err}, + ); + }; + } + } else { + assert(self.active_screen == .primary); + self.restoreCursor() catch |err| { + log.warn( + "restore cursor on switch screen failed to={} err={}", + .{ to, err }, + ); + }; + }, } } -/// Switch back to the primary screen (reset alternate screen mode). -pub fn primaryScreen( - self: *Terminal, - options: AlternateScreenOptions, -) void { - //log.info("primary screen active={} options={}", .{ self.active_screen, options }); +/// Modal screen changes. These map to the literal terminal +/// modes to enable or disable alternate screen modes. They each +/// have subtle behaviors so we define them as an enum here. +pub const SwitchScreenMode = enum { + /// Legacy alternate screen mode. This goes to the alternate + /// screen or primary screen and only copies the cursor. The + /// screen is not erased. + @"47", - // TODO: test - // TODO(mitchellh): what happens if we enter alternate screen multiple times? - if (self.active_screen == .primary) return; + /// Alternate screen mode where the alternate screen is cleared + /// on exit. The primary screen is never cleared. The cursor is + /// copied. + @"1047", - if (options.clear_on_exit) self.eraseDisplay(.complete, false); - - // Switch the screens - const old = self.screen; - self.screen = self.secondary_screen; - self.secondary_screen = old; - self.active_screen = .primary; - - // Clear our selection - self.screen.clearSelection(); - - // Mark kitty images as dirty so they redraw - self.screen.kitty_images.dirty = true; - - // Mark our terminal as dirty - self.flags.dirty.clear = true; - - // We always end hyperlink state - self.screen.endHyperlink(); - - // Restore the cursor from the primary screen. This should not - // fail because we should not have to allocate memory since swapping - // screens does not create new cursors. - if (options.cursor_save) self.restoreCursor() catch |err| { - log.warn("restore cursor on primary screen failed err={}", .{err}); - }; -} + /// Save primary screen cursor, switch to alternate screen, + /// and clear the alternate screen on entry. On exit, + /// do not clear the screen, and restore the cursor on the + /// primary screen. + @"1049", +}; /// Return the current string value of the terminal. Newlines are /// encoded as "\n". This omits any formatting such as fg/bg. @@ -9203,37 +9261,6 @@ test "Terminal: saveCursor" { try testing.expect(t.modes.get(.origin)); } -test "Terminal: saveCursor with screen change" { - const alloc = testing.allocator; - var t = try init(alloc, .{ .cols = 3, .rows = 3 }); - defer t.deinit(alloc); - - try t.setAttribute(.{ .bold = {} }); - t.setCursorPos(t.screen.cursor.y + 1, 3); - try testing.expect(t.screen.cursor.x == 2); - t.screen.charset.gr = .G3; - t.modes.set(.origin, true); - t.alternateScreen(.{ - .cursor_save = true, - .clear_on_enter = true, - }); - // make sure our cursor and charset have come with us - try testing.expect(t.screen.cursor.style.flags.bold); - try testing.expect(t.screen.cursor.x == 2); - try testing.expect(t.screen.charset.gr == .G3); - try testing.expect(t.modes.get(.origin)); - t.screen.charset.gr = .G0; - try t.setAttribute(.{ .reset_bold = {} }); - t.modes.set(.origin, false); - t.primaryScreen(.{ - .cursor_save = true, - .clear_on_enter = true, - }); - try testing.expect(t.screen.cursor.style.flags.bold); - try testing.expect(t.screen.charset.gr == .G3); - try testing.expect(t.modes.get(.origin)); -} - test "Terminal: saveCursor position" { const alloc = testing.allocator; var t = try init(alloc, .{ .cols = 10, .rows = 5 }); @@ -10472,7 +10499,7 @@ test "Terminal: cursorIsAtPrompt alternate screen" { try testing.expect(t.cursorIsAtPrompt()); // Secondary screen is never a prompt - t.alternateScreen(.{}); + t.switchScreenMode(.@"1049", true); try testing.expect(!t.cursorIsAtPrompt()); t.markSemanticPrompt(.prompt); try testing.expect(!t.cursorIsAtPrompt()); @@ -10556,7 +10583,7 @@ test "Terminal: fullReset clears alt screen kitty keyboard state" { var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 }); defer t.deinit(testing.allocator); - t.alternateScreen(.{}); + t.switchScreenMode(.@"1049", true); t.screen.kitty_keyboard.push(.{ .disambiguate = true, .report_events = false, @@ -10564,7 +10591,7 @@ test "Terminal: fullReset clears alt screen kitty keyboard state" { .report_all = true, .report_associated = true, }); - t.primaryScreen(.{}); + t.switchScreenMode(.@"1049", false); t.fullReset(); try testing.expectEqual(0, t.secondary_screen.kitty_keyboard.current().int()); @@ -10869,3 +10896,236 @@ test "Terminal: DECCOLM resets scroll region" { try testing.expectEqual(@as(usize, 0), t.scrolling_region.left); try testing.expectEqual(@as(usize, 79), t.scrolling_region.right); } + +test "Terminal: mode 47 alt screen plain" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + // Print on primary screen + try t.printString("1A"); + + // Go to alt screen with mode 47 + t.switchScreenMode(.@"47", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Screen should be empty + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + + // Print on alt screen. This should be off center because + // we copy the cursor over from the primary screen + try t.printString("2B"); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" 2B", str); + } + + // Go back to primary + t.switchScreenMode(.@"47", false); + try testing.expectEqual(ScreenType.primary, t.active_screen); + + // Primary screen should still have the original content + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("1A", str); + } + + // Go back to alt screen with mode 47 + t.switchScreenMode(.@"47", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Screen should retain content + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" 2B", str); + } +} + +test "Terminal: mode 47 copies cursor both directions" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + // Color our cursor red + try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } }); + + // Go to alt screen with mode 47 + t.switchScreenMode(.@"47", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Verify that our style is set + { + try testing.expect(t.screen.cursor.style_id != style.default_id); + const page = &t.screen.cursor.page_pin.node.data; + try testing.expectEqual(@as(usize, 1), page.styles.count()); + try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 0); + } + + // Set a new style + try t.setAttribute(.{ .direct_color_fg = .{ .r = 0, .g = 0xFF, .b = 0 } }); + + // Go back to primary + t.switchScreenMode(.@"47", false); + try testing.expectEqual(ScreenType.primary, t.active_screen); + + // Verify that our style is still set + { + try testing.expect(t.screen.cursor.style_id != style.default_id); + const page = &t.screen.cursor.page_pin.node.data; + try testing.expectEqual(@as(usize, 1), page.styles.count()); + try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 0); + } +} + +test "Terminal: mode 1047 alt screen plain" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + // Print on primary screen + try t.printString("1A"); + + // Go to alt screen with mode 47 + t.switchScreenMode(.@"1047", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Screen should be empty + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + + // Print on alt screen. This should be off center because + // we copy the cursor over from the primary screen + try t.printString("2B"); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" 2B", str); + } + + // Go back to primary + t.switchScreenMode(.@"1047", false); + try testing.expectEqual(ScreenType.primary, t.active_screen); + + // Primary screen should still have the original content + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("1A", str); + } + + // Go back to alt screen with mode 1047 + t.switchScreenMode(.@"1047", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Screen should be empty + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } +} + +test "Terminal: mode 1047 copies cursor both directions" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + // Color our cursor red + try t.setAttribute(.{ .direct_color_fg = .{ .r = 0xFF, .g = 0, .b = 0x7F } }); + + // Go to alt screen with mode 47 + t.switchScreenMode(.@"1047", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Verify that our style is set + { + try testing.expect(t.screen.cursor.style_id != style.default_id); + const page = &t.screen.cursor.page_pin.node.data; + try testing.expectEqual(@as(usize, 1), page.styles.count()); + try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 0); + } + + // Set a new style + try t.setAttribute(.{ .direct_color_fg = .{ .r = 0, .g = 0xFF, .b = 0 } }); + + // Go back to primary + t.switchScreenMode(.@"1047", false); + try testing.expectEqual(ScreenType.primary, t.active_screen); + + // Verify that our style is still set + { + try testing.expect(t.screen.cursor.style_id != style.default_id); + const page = &t.screen.cursor.page_pin.node.data; + try testing.expectEqual(@as(usize, 1), page.styles.count()); + try testing.expect(page.styles.refCount(page.memory, t.screen.cursor.style_id) > 0); + } +} + +test "Terminal: mode 1049 alt screen plain" { + const alloc = testing.allocator; + var t = try init(alloc, .{ .rows = 5, .cols = 5 }); + defer t.deinit(alloc); + + // Print on primary screen + try t.printString("1A"); + + // Go to alt screen with mode 47 + t.switchScreenMode(.@"1049", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Screen should be empty + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } + + // Print on alt screen. This should be off center because + // we copy the cursor over from the primary screen + try t.printString("2B"); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings(" 2B", str); + } + + // Go back to primary + t.switchScreenMode(.@"1049", false); + try testing.expectEqual(ScreenType.primary, t.active_screen); + + // Primary screen should still have the original content + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("1A", str); + } + + // Write, our cursor should be restored back. + try t.printString("C"); + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("1AC", str); + } + + // Go back to alt screen with mode 1049 + t.switchScreenMode(.@"1049", true); + try testing.expectEqual(ScreenType.alternate, t.active_screen); + + // Screen should be empty + { + const str = try t.plainString(testing.allocator); + defer testing.allocator.free(str); + try testing.expectEqualStrings("", str); + } +} diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ffd00e14d..96565b30d 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -583,42 +583,17 @@ pub const StreamHandler = struct { }, .alt_screen_legacy => { - if (enabled) - self.terminal.alternateScreen(.{}) - else - self.terminal.primaryScreen(.{}); - - // Schedule a render since we changed screens + self.terminal.switchScreenMode(.@"47", enabled); try self.queueRender(); }, .alt_screen => { - const opts: terminal.Terminal.AlternateScreenOptions = .{ - .cursor_save = false, - .clear_on_enter = false, - }; - - if (enabled) - self.terminal.alternateScreen(opts) - else - self.terminal.primaryScreen(opts); - - // Schedule a render since we changed screens + self.terminal.switchScreenMode(.@"1047", enabled); try self.queueRender(); }, .alt_screen_save_cursor_clear_enter => { - const opts: terminal.Terminal.AlternateScreenOptions = .{ - .cursor_save = true, - .clear_on_enter = true, - }; - - if (enabled) - self.terminal.alternateScreen(opts) - else - self.terminal.primaryScreen(opts); - - // Schedule a render since we changed screens + self.terminal.switchScreenMode(.@"1049", enabled); try self.queueRender(); }, From 891b23917b3bbd0c07bffa96ae53da5dad062fd7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 May 2025 16:03:01 -0700 Subject: [PATCH 323/642] input: "ignore" binding action are still be processed by the OS/GUI Related to #7468 This changes the behavior of "ignore". Previously, Ghostty would consider "ignore" actions consumed but do nothing. They were like a black hole. Now, Ghostty returns `ignored` which lets the apprt forward the event to the OS/GUI. This enables keys that would otherwise be pty-encoded to be processed later, such as for GTK to show the GTK inspector. --- src/Surface.zig | 18 ++++++++++++------ src/input/Binding.zig | 11 +++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 01639964b..62a0ce549 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -2069,12 +2069,18 @@ fn maybeHandleBinding( break :performed try self.performBindingAction(action); }; - // If we performed an action and it was a closing action, - // our "self" pointer is not safe to use anymore so we need to - // just exit immediately. - if (performed and closingAction(action)) { - log.debug("key binding is a closing binding, halting key event processing", .{}); - return .closed; + if (performed) { + // If we performed an action and it was a closing action, + // our "self" pointer is not safe to use anymore so we need to + // just exit immediately. + if (closingAction(action)) { + log.debug("key binding is a closing binding, halting key event processing", .{}); + return .closed; + } + + // If our action was "ignore" then we return the special input + // effect of "ignored". + if (action == .ignore) return .ignored; } // If we have the performable flag and the action was not performed, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 3818d99a6..bda0cfd47 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -222,13 +222,20 @@ pub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool { /// The set of actions that a keybinding can take. pub const Action = union(enum) { - /// Ignore this key combination, don't send it to the child process, just - /// black hole it. + /// Ignore this key combination, don't send it to the child process, + /// pretend that it never happened at the Ghostty level. The key + /// combination may still be processed by the OS or other + /// applications. ignore, /// This action is used to flag that the binding should be removed from /// the set. This should never exist in an active set and `set.put` has an /// assertion to verify this. + /// + /// This is only able to unbind bindings that were previously + /// bound to Ghostty. This cannot unbind bindings that were not + /// bound by Ghostty (e.g. bindings set by the OS or some other + /// application). unbind, /// Send a CSI sequence. The value should be the CSI sequence without the From 4d18c06804f1567c5b3efad60331c636f3eb4f28 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 14:04:14 +0200 Subject: [PATCH 324/642] gtk(wayland): customize keyboard interactivity for quick terminal Fixes #7476 --- src/apprt/gtk/Window.zig | 2 ++ src/apprt/gtk/winproto/wayland.zig | 28 ++++++++++++++++-------- src/config/Config.zig | 35 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index aa1f0a4b1..d9d2da057 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -90,6 +90,7 @@ pub const DerivedConfig = struct { quick_terminal_position: configpkg.Config.QuickTerminalPosition, quick_terminal_size: configpkg.Config.QuickTerminalSize, quick_terminal_autohide: bool, + quick_terminal_keyboard_interactivity: configpkg.Config.QuickTerminalKeyboardInteractivity, maximize: bool, fullscreen: bool, @@ -109,6 +110,7 @@ pub const DerivedConfig = struct { .quick_terminal_position = config.@"quick-terminal-position", .quick_terminal_size = config.@"quick-terminal-size", .quick_terminal_autohide = config.@"quick-terminal-autohide", + .quick_terminal_keyboard_interactivity = config.@"quick-terminal-keyboard-interactivity", .maximize = config.maximize, .fullscreen = config.fullscreen, diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 5f5feca6e..5a4f24ff7 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -110,7 +110,6 @@ pub const App = struct { gtk4_layer_shell.initForWindow(window); gtk4_layer_shell.setLayer(window, .top); - gtk4_layer_shell.setKeyboardMode(window, .on_demand); } fn registryListener( @@ -356,9 +355,9 @@ pub const Window = struct { fn syncQuickTerminal(self: *Window) !void { const window = self.apprt_window.window.as(gtk.Window); - const position = self.apprt_window.config.quick_terminal_position; + const config = &self.apprt_window.config; - const anchored_edge: ?gtk4_layer_shell.ShellEdge = switch (position) { + const anchored_edge: ?gtk4_layer_shell.ShellEdge = switch (config.quick_terminal_position) { .left => .left, .right => .right, .top => .top, @@ -366,6 +365,15 @@ pub const Window = struct { .center => null, }; + gtk4_layer_shell.setKeyboardMode( + window, + switch (config.quick_terminal_keyboard_interactivity) { + .none => .none, + .@"on-demand" => .on_demand, + .exclusive => .exclusive, + }, + ); + for (std.meta.tags(gtk4_layer_shell.ShellEdge)) |edge| { if (anchored_edge) |anchored| { if (edge == anchored) { @@ -412,16 +420,18 @@ pub const Window = struct { apprt_window: *ApprtWindow, ) callconv(.c) void { const window = apprt_window.window.as(gtk.Window); - const size = apprt_window.config.quick_terminal_size; - const position = apprt_window.config.quick_terminal_position; + const config = &apprt_window.config; var monitor_size: gdk.Rectangle = undefined; monitor.getGeometry(&monitor_size); - const dims = size.calculate(position, .{ - .width = @intCast(monitor_size.f_width), - .height = @intCast(monitor_size.f_height), - }); + const dims = config.quick_terminal_size.calculate( + config.quick_terminal_position, + .{ + .width = @intCast(monitor_size.f_width), + .height = @intCast(monitor_size.f_height), + }, + ); window.setDefaultSize(@intCast(dims.width), @intCast(dims.height)); } diff --git a/src/config/Config.zig b/src/config/Config.zig index a20719a8f..b59334160 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1808,6 +1808,34 @@ keybind: Keybinds = .{}, /// On Linux the behavior is always equivalent to `move`. @"quick-terminal-space-behavior": QuickTerminalSpaceBehavior = .move, +/// Determines under which circumstances that the quick terminal should receive +/// keyboard input. See the corresponding [Wayland documentation](https://wayland.app/protocols/wlr-layer-shell-unstable-v1#zwlr_layer_surface_v1:enum:keyboard_interactivity) +/// for a more detailed explanation of the behavior of each option. +/// +/// > [!NOTE] +/// > The exact behavior of each option may differ significantly across +/// > compositors -- experiment with them on your system to find one that +/// > suits your liking! +/// +/// Valid values are: +/// +/// * `none` +/// +/// The quick terminal will not receive any keyboard input. +/// +/// * `on-demand` (default) +/// +/// The quick terminal would only receive keyboard input when it is focused. +/// +/// * `exclusive` +/// +/// The quick terminal will always receive keyboard input, even when another +/// window is currently focused. +/// +/// Only has an effect on Linux Wayland. +/// On macOS the behavior is always equivalent to `on-demand`. +@"quick-terminal-keyboard-interactivity": QuickTerminalKeyboardInteractivity = .@"on-demand", + /// Whether to enable shell integration auto-injection or not. Shell integration /// greatly enhances the terminal experience by enabling a number of features: /// @@ -6138,6 +6166,13 @@ pub const QuickTerminalSpaceBehavior = enum { move, }; +/// See quick-terminal-keyboard-interactivity +pub const QuickTerminalKeyboardInteractivity = enum { + none, + @"on-demand", + exclusive, +}; + /// See grapheme-width-method pub const GraphemeWidthMethod = enum { legacy, From 6fac355363abb63c148adbb99f7b4a430cb494a5 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 14:44:29 +0200 Subject: [PATCH 325/642] gtk(wayland): fallback when on-demand mode isn't supported This shouldn't be a real problem anymore since as of now (May 2025) all major compositors support at least version 4, but let's do this just in case. --- pkg/gtk4-layer-shell/src/main.zig | 4 ++++ src/apprt/gtk/winproto/wayland.zig | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/gtk4-layer-shell/src/main.zig b/pkg/gtk4-layer-shell/src/main.zig index 88d99772b..d7eafa135 100644 --- a/pkg/gtk4-layer-shell/src/main.zig +++ b/pkg/gtk4-layer-shell/src/main.zig @@ -27,6 +27,10 @@ pub fn isSupported() bool { return c.gtk_layer_is_supported() != 0; } +pub fn getProtocolVersion() c_uint { + return c.gtk_layer_get_protocol_version(); +} + pub fn initForWindow(window: *gtk.Window) void { c.gtk_layer_init_for_window(@ptrCast(window)); } diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 5a4f24ff7..e6861b1ed 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -369,7 +369,13 @@ pub const Window = struct { window, switch (config.quick_terminal_keyboard_interactivity) { .none => .none, - .@"on-demand" => .on_demand, + .@"on-demand" => on_demand: { + if (gtk4_layer_shell.getProtocolVersion() < 4) { + log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{}); + break :on_demand .exclusive; + } + break :on_demand .on_demand; + }, .exclusive => .exclusive, }, ); From 71a1ece7e91112a9f3dc7c57ef31a5bea7b0897c Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 14:50:26 +0200 Subject: [PATCH 326/642] gtk(wayland): gtk4-layer-shell -> layer-shell It was getting a bit too unwieldy. --- src/apprt/gtk/winproto/wayland.zig | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index e6861b1ed..a8eaa5be7 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -6,8 +6,8 @@ const build_options = @import("build_options"); const gdk = @import("gdk"); const gdk_wayland = @import("gdk_wayland"); const gobject = @import("gobject"); -const gtk4_layer_shell = @import("gtk4-layer-shell"); const gtk = @import("gtk"); +const layer_shell = @import("gtk4-layer-shell"); const wayland = @import("wayland"); const Config = @import("../../../config.zig").Config; @@ -98,7 +98,7 @@ pub const App = struct { } pub fn supportsQuickTerminal(_: App) bool { - if (!gtk4_layer_shell.isSupported()) { + if (!layer_shell.isSupported()) { log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{}); return false; } @@ -108,8 +108,8 @@ pub const App = struct { pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void { const window = apprt_window.window.as(gtk.Window); - gtk4_layer_shell.initForWindow(window); - gtk4_layer_shell.setLayer(window, .top); + layer_shell.initForWindow(window); + layer_shell.setLayer(window, .top); } fn registryListener( @@ -357,7 +357,7 @@ pub const Window = struct { const window = self.apprt_window.window.as(gtk.Window); const config = &self.apprt_window.config; - const anchored_edge: ?gtk4_layer_shell.ShellEdge = switch (config.quick_terminal_position) { + const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) { .left => .left, .right => .right, .top => .top, @@ -365,12 +365,12 @@ pub const Window = struct { .center => null, }; - gtk4_layer_shell.setKeyboardMode( + layer_shell.setKeyboardMode( window, switch (config.quick_terminal_keyboard_interactivity) { .none => .none, .@"on-demand" => on_demand: { - if (gtk4_layer_shell.getProtocolVersion() < 4) { + if (layer_shell.getProtocolVersion() < 4) { log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{}); break :on_demand .exclusive; } @@ -380,18 +380,18 @@ pub const Window = struct { }, ); - for (std.meta.tags(gtk4_layer_shell.ShellEdge)) |edge| { + for (std.meta.tags(layer_shell.ShellEdge)) |edge| { if (anchored_edge) |anchored| { if (edge == anchored) { - gtk4_layer_shell.setMargin(window, edge, 0); - gtk4_layer_shell.setAnchor(window, edge, true); + layer_shell.setMargin(window, edge, 0); + layer_shell.setAnchor(window, edge, true); continue; } } // Arbitrary margin - could be made customizable? - gtk4_layer_shell.setMargin(window, edge, 20); - gtk4_layer_shell.setAnchor(window, edge, false); + layer_shell.setMargin(window, edge, 20); + layer_shell.setAnchor(window, edge, false); } if (self.apprt_window.isQuickTerminal()) { From dee7c835deaaeb9cbc76077a06efe393e5f6e8db Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 14:53:35 +0200 Subject: [PATCH 327/642] gtk(wayland): remove redundant check --- src/apprt/gtk/winproto/wayland.zig | 52 ++++++++++++++---------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index a8eaa5be7..483a09d3c 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -357,14 +357,6 @@ pub const Window = struct { const window = self.apprt_window.window.as(gtk.Window); const config = &self.apprt_window.config; - const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) { - .left => .left, - .right => .right, - .top => .top, - .bottom => .bottom, - .center => null, - }; - layer_shell.setKeyboardMode( window, switch (config.quick_terminal_keyboard_interactivity) { @@ -380,6 +372,14 @@ pub const Window = struct { }, ); + const anchored_edge: ?layer_shell.ShellEdge = switch (config.quick_terminal_position) { + .left => .left, + .right => .right, + .top => .top, + .bottom => .bottom, + .center => null, + }; + for (std.meta.tags(layer_shell.ShellEdge)) |edge| { if (anchored_edge) |anchored| { if (edge == anchored) { @@ -394,29 +394,27 @@ pub const Window = struct { layer_shell.setAnchor(window, edge, false); } - if (self.apprt_window.isQuickTerminal()) { - if (self.slide) |slide| slide.release(); + if (self.slide) |slide| slide.release(); - self.slide = if (anchored_edge) |anchored| slide: { - const mgr = self.app_context.kde_slide_manager orelse break :slide null; + self.slide = if (anchored_edge) |anchored| slide: { + const mgr = self.app_context.kde_slide_manager orelse break :slide null; - const slide = mgr.create(self.surface) catch |err| { - log.warn("could not create slide object={}", .{err}); - break :slide null; - }; + const slide = mgr.create(self.surface) catch |err| { + log.warn("could not create slide object={}", .{err}); + break :slide null; + }; - const slide_location: org.KdeKwinSlide.Location = switch (anchored) { - .top => .top, - .bottom => .bottom, - .left => .left, - .right => .right, - }; + const slide_location: org.KdeKwinSlide.Location = switch (anchored) { + .top => .top, + .bottom => .bottom, + .left => .left, + .right => .right, + }; - slide.setLocation(@intCast(@intFromEnum(slide_location))); - slide.commit(); - break :slide slide; - } else null; - } + slide.setLocation(@intCast(@intFromEnum(slide_location))); + slide.commit(); + break :slide slide; + } else null; } /// Update the size of the quick terminal based on monitor dimensions. From 6959fa84387397b6cb490acbc0fb1212f51c376a Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 15:02:01 +0200 Subject: [PATCH 328/642] gtk(wayland): explicitly set layer name Even though gtk4-layer-shell's documentation claims that "nobody quite knows what it's for", some compositors (like Niri) can define custom rules based on the layer name and it's beneficial in those cases to define a distinct name just for our quick terminals. --- pkg/gtk4-layer-shell/src/main.zig | 4 ++++ src/apprt/gtk/winproto/wayland.zig | 1 + 2 files changed, 5 insertions(+) diff --git a/pkg/gtk4-layer-shell/src/main.zig b/pkg/gtk4-layer-shell/src/main.zig index d7eafa135..06936bba2 100644 --- a/pkg/gtk4-layer-shell/src/main.zig +++ b/pkg/gtk4-layer-shell/src/main.zig @@ -50,3 +50,7 @@ pub fn setMargin(window: *gtk.Window, edge: ShellEdge, margin_size: c_int) void pub fn setKeyboardMode(window: *gtk.Window, mode: KeyboardMode) void { c.gtk_layer_set_keyboard_mode(@ptrCast(window), @intFromEnum(mode)); } + +pub fn setNamespace(window: *gtk.Window, name: [:0]const u8) void { + c.gtk_layer_set_namespace(@ptrCast(window), name.ptr); +} diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 483a09d3c..b718609e3 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -110,6 +110,7 @@ pub const App = struct { layer_shell.initForWindow(window); layer_shell.setLayer(window, .top); + layer_shell.setNamespace(window, "ghostty-quick-terminal"); } fn registryListener( From 90f431005b877704231ffc1ca437658881e333b6 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 21:23:10 +0200 Subject: [PATCH 329/642] gtk: request user attention on bell I'm not sure if this should be enabled by default like the tab animation, but on KDE at least this is unintrusive enough for me to always enable by default. Alacritty appears to agree with me as well. --- src/apprt/gtk/Surface.zig | 5 +++ src/apprt/gtk/Window.zig | 12 ++++-- src/apprt/gtk/winproto.zig | 6 +++ src/apprt/gtk/winproto/noop.zig | 2 + src/apprt/gtk/winproto/wayland.zig | 61 +++++++++++++++++++++++++++--- src/apprt/gtk/winproto/x11.zig | 29 +++++++------- src/build/SharedDeps.zig | 4 +- 7 files changed, 95 insertions(+), 24 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1ee00ff1b..3d16e9fbb 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2447,6 +2447,11 @@ pub fn ringBell(self: *Surface) !void { // Need attention if we're not the currently selected tab if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true)); } + + // Request user attention + window.winproto.setUrgent(true) catch |err| { + log.err("failed to request user attention={}", .{err}); + }; } /// Handle a stream that is in an error state. diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index aa1f0a4b1..41eae3d85 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -814,11 +814,15 @@ fn gtkWindowNotifyIsActive( _: *gobject.ParamSpec, self: *Window, ) callconv(.c) void { - if (!self.isQuickTerminal()) return; + self.winproto.setUrgent(false) catch |err| { + log.err("failed to unrequest user attention={}", .{err}); + }; - // Hide when we're unfocused - if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) { - self.toggleVisibility(); + if (self.isQuickTerminal()) { + // Hide when we're unfocused + if (self.config.quick_terminal_autohide and self.window.as(gtk.Window).isActive() == 0) { + self.toggleVisibility(); + } } } diff --git a/src/apprt/gtk/winproto.zig b/src/apprt/gtk/winproto.zig index ff83e6851..2dbe5a7a0 100644 --- a/src/apprt/gtk/winproto.zig +++ b/src/apprt/gtk/winproto.zig @@ -146,4 +146,10 @@ pub const Window = union(Protocol) { inline else => |*v| try v.addSubprocessEnv(env), } } + + pub fn setUrgent(self: *Window, urgent: bool) !void { + switch (self.*) { + inline else => |*v| try v.setUrgent(urgent), + } + } }; diff --git a/src/apprt/gtk/winproto/noop.zig b/src/apprt/gtk/winproto/noop.zig index 5cb5887c9..fb732b756 100644 --- a/src/apprt/gtk/winproto/noop.zig +++ b/src/apprt/gtk/winproto/noop.zig @@ -70,4 +70,6 @@ pub const Window = struct { } pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {} + + pub fn setUrgent(_: *Window, _: bool) !void {} }; diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 5f5feca6e..98b0ee238 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -16,6 +16,7 @@ const ApprtWindow = @import("../Window.zig"); const wl = wayland.client.wl; const org = wayland.client.org; +const xdg = wayland.client.xdg; const log = std.log.scoped(.winproto_wayland); @@ -34,6 +35,8 @@ pub const App = struct { kde_slide_manager: ?*org.KdeKwinSlideManager = null, default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null, + + xdg_activation: ?*xdg.ActivationV1 = null, }; pub fn init( @@ -150,6 +153,15 @@ pub const App = struct { context.kde_slide_manager = slide_manager; return; } + + if (registryBind( + xdg.ActivationV1, + registry, + global, + )) |activation| { + context.xdg_activation = activation; + return; + } }, // We don't handle removal events @@ -207,15 +219,19 @@ pub const Window = struct { app_context: *App.Context, /// A token that, when present, indicates that the window is blurred. - blur_token: ?*org.KdeKwinBlur, + blur_token: ?*org.KdeKwinBlur = null, /// Object that controls the decoration mode (client/server/auto) /// of the window. - decoration: ?*org.KdeKwinServerDecoration, + decoration: ?*org.KdeKwinServerDecoration = null, /// Object that controls the slide-in/slide-out animations of the /// quick terminal. Always null for windows other than the quick terminal. - slide: ?*org.KdeKwinSlide, + slide: ?*org.KdeKwinSlide = null, + + /// Object that, when present, denotes that the window is currently + /// requesting attention from the user. + activation_token: ?*xdg.ActivationTokenV1 = null, pub fn init( alloc: Allocator, @@ -268,9 +284,7 @@ pub const Window = struct { .apprt_window = apprt_window, .surface = wl_surface, .app_context = app.context, - .blur_token = null, .decoration = deco, - .slide = null, }; } @@ -315,6 +329,21 @@ pub const Window = struct { _ = env; } + pub fn setUrgent(self: *Window, urgent: bool) !void { + const activation = self.app_context.xdg_activation orelse return; + + // If there already is a token, destroy and unset it + if (self.activation_token) |token| token.destroy(); + + self.activation_token = if (urgent) token: { + const token = try activation.getActivationToken(); + token.setSurface(self.surface); + token.setListener(*Window, onActivationTokenEvent, self); + token.commit(); + break :token token; + } else null; + } + /// Update the blur state of the window. fn syncBlur(self: *Window) !void { const manager = self.app_context.kde_blur_manager orelse return; @@ -425,4 +454,26 @@ pub const Window = struct { window.setDefaultSize(@intCast(dims.width), @intCast(dims.height)); } + + fn onActivationTokenEvent( + token: *xdg.ActivationTokenV1, + event: xdg.ActivationTokenV1.Event, + self: *Window, + ) void { + const activation = self.app_context.xdg_activation orelse return; + const current_token = self.activation_token orelse return; + + if (token.getId() != current_token.getId()) { + log.warn("received event for unknown activation token; ignoring", .{}); + return; + } + + switch (event) { + .done => |done| { + activation.activate(done.token, self.surface); + token.destroy(); + self.activation_token = null; + }, + } + } }; diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 387905b18..2c4925167 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -176,8 +176,8 @@ pub const App = struct { pub const Window = struct { app: *App, config: *const ApprtWindow.DerivedConfig, - window: xlib.Window, gtk_window: *adw.ApplicationWindow, + x11_surface: *gdk_x11.X11Surface, blur_region: Region = .{}, @@ -192,13 +192,6 @@ pub const Window = struct { gtk.Native, ).getSurface() orelse return error.NotX11Surface; - // Check if we're actually on X11 - if (gobject.typeCheckInstanceIsA( - surface.as(gobject.TypeInstance), - gdk_x11.X11Surface.getGObjectType(), - ) == 0) - return error.NotX11Surface; - const x11_surface = gobject.ext.cast( gdk_x11.X11Surface, surface, @@ -207,8 +200,8 @@ pub const Window = struct { return .{ .app = app, .config = &apprt_window.config, - .window = x11_surface.getXid(), .gtk_window = apprt_window.window, + .x11_surface = x11_surface, }; } @@ -279,7 +272,7 @@ pub const Window = struct { const blur = self.config.background_blur; log.debug("set blur={}, window xid={}, region={}", .{ blur, - self.window, + self.x11_surface.getXid(), self.blur_region, }); @@ -335,11 +328,19 @@ pub const Window = struct { pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void { var buf: [64]u8 = undefined; - const window_id = try std.fmt.bufPrint(&buf, "{}", .{self.window}); + const window_id = try std.fmt.bufPrint( + &buf, + "{}", + .{self.x11_surface.getXid()}, + ); try env.put("WINDOWID", window_id); } + pub fn setUrgent(self: *Window, urgent: bool) !void { + self.x11_surface.setUrgencyHint(@intFromBool(urgent)); + } + fn getWindowProperty( self: *Window, comptime T: type, @@ -363,7 +364,7 @@ pub const Window = struct { const code = c.XGetWindowProperty( @ptrCast(@alignCast(self.app.display)), - self.window, + self.x11_surface.getXid(), name, options.offset, options.length, @@ -401,7 +402,7 @@ pub const Window = struct { const status = c.XChangeProperty( @ptrCast(@alignCast(self.app.display)), - self.window, + self.x11_surface.getXid(), name, typ, @intFromEnum(format), @@ -419,7 +420,7 @@ pub const Window = struct { fn deleteProperty(self: *Window, name: c.Atom) X11Error!void { const status = c.XDeleteProperty( @ptrCast(@alignCast(self.app.display)), - self.window, + self.x11_surface.getXid(), name, ); if (status == 0) return error.RequestFailed; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index d3741a358..5d737cb6f 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -609,21 +609,23 @@ fn addGTK( .wayland_protocols = wayland_protocols_dep.path(""), }); - // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/blur.xml"), ); + // FIXME: replace with `zxdg_decoration_v1` once GTK merges https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398 scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/server-decoration.xml"), ); scanner.addCustomProtocol( plasma_wayland_protocols_dep.path("src/protocols/slide.xml"), ); + scanner.addSystemProtocol("staging/xdg-activation/xdg-activation-v1.xml"); scanner.generate("wl_compositor", 1); scanner.generate("org_kde_kwin_blur_manager", 1); scanner.generate("org_kde_kwin_server_decoration_manager", 1); scanner.generate("org_kde_kwin_slide_manager", 1); + scanner.generate("xdg_activation_v1", 1); step.root_module.addImport("wayland", b.createModule(.{ .root_source_file = scanner.result, From 8be5a78585a1185e355ed52ee64fa552655f07ec Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 May 2025 14:15:20 -0700 Subject: [PATCH 330/642] config: more robust handling of font-family overwrite for CLI args Fixes #7481 We unfortunately don't have a great way to unit test this since our logic relies on argv and I'm too lazy to extract it out right now. The core issue is that we previously detected if font-families changed by comparing lengths. This doesn't work because the CLI args can reset and add families back to a lesser length. This caused an integer overflow. We can fix this by not being clever and introducing the overwrite logic directly into the config type. I unit tested that. --- src/config/Config.zig | 64 +++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index a20719a8f..ce4e46df1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2722,19 +2722,18 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { // can replay if we are discarding the default files. const replay_len_start = self._replay_steps.items.len; - // Keep track of font families because if they are set from the CLI - // then we clear the previously set values. This avoids a UX oddity - // where on the CLI you have to specify `font-family=""` to clear the - // font families before setting a new one. + // font-family settings set via the CLI overwrite any prior values + // rather than append. This avoids a UX oddity where you have to + // specify `font-family=""` to clear the font families. const fields = &[_][]const u8{ "font-family", "font-family-bold", "font-family-italic", "font-family-bold-italic", }; - var counter: [fields.len]usize = undefined; - inline for (fields, 0..) |field, i| { - counter[i] = @field(self, field).list.items.len; + inline for (fields) |field| @field(self, field).overwrite_next = true; + defer { + inline for (fields) |field| @field(self, field).overwrite_next = false; } // Initialize our CLI iterator. @@ -2759,28 +2758,6 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void { try new_config.loadIter(alloc_gpa, &it); self.deinit(); self.* = new_config; - } else { - // If any of our font family settings were changed, then we - // replace the entire list with the new list. - inline for (fields, 0..) |field, i| { - const v = &@field(self, field); - - // The list can be empty if it was reset, i.e. --font-family="" - if (v.list.items.len > 0) { - const len = v.list.items.len - counter[i]; - if (len > 0) { - // Note: we don't have to worry about freeing the memory - // that we overwrite or cut off here because its all in - // an arena. - v.list.replaceRangeAssumeCapacity( - 0, - len, - v.list.items[counter[i]..], - ); - v.list.items.len = len; - } - } - } } // Any paths referenced from the CLI are relative to the current working @@ -4172,6 +4149,11 @@ pub const RepeatableString = struct { // Allocator for the list is the arena for the parent config. list: std.ArrayListUnmanaged([:0]const u8) = .{}, + // If true, then the next value will clear the list and start over + // rather than append. This is a bit of a hack but is here to make + // the font-family set of configurations work with CLI parsing. + overwrite_next: bool = false, + pub fn parseCLI(self: *Self, alloc: Allocator, input: ?[]const u8) !void { const value = input orelse return error.ValueRequired; @@ -4181,6 +4163,12 @@ pub const RepeatableString = struct { return; } + // If we're overwriting then we clear before appending + if (self.overwrite_next) { + self.list.clearRetainingCapacity(); + self.overwrite_next = false; + } + const copy = try alloc.dupeZ(u8, value); try self.list.append(alloc, copy); } @@ -4247,6 +4235,24 @@ pub const RepeatableString = struct { try testing.expectEqual(@as(usize, 0), list.list.items.len); } + test "parseCLI overwrite" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var list: Self = .{}; + try list.parseCLI(alloc, "A"); + + // Set our overwrite flag + list.overwrite_next = true; + + try list.parseCLI(alloc, "B"); + try testing.expectEqual(@as(usize, 1), list.list.items.len); + try list.parseCLI(alloc, "C"); + try testing.expectEqual(@as(usize, 2), list.list.items.len); + } + test "formatConfig empty" { const testing = std.testing; var buf = std.ArrayList(u8).init(testing.allocator); From 34f08a450e8abd8f5c0331e03befa87eb1276f77 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 30 May 2025 14:59:53 -0600 Subject: [PATCH 331/642] font: rework coretext discovery sorting This should make the sorting more robust to fonts with questionable metadata or atypical style names. I was originally just going to change the scoring slightly to account for fonts whose regular italic style is named "Regular Italic" - which previously resulted in the Bold Italic or Thin Italic style being chosen instead because they're shorter names, but I decided to do some better inspection of the metadata and looser style name matching while I was changing code here anyway. Also adds a unit test to verify the sorting works correctly, though a more comprehensive set of tests may be desirable in the future. --- src/font/discovery.zig | 432 ++++++++++++++++++++++++++++++----------- 1 file changed, 316 insertions(+), 116 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 384799da5..9284f9486 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -4,6 +4,7 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const fontconfig = @import("fontconfig"); const macos = @import("macos"); +const opentype = @import("opentype.zig"); const options = @import("main.zig").options; const Collection = @import("main.zig").Collection; const DeferredFace = @import("main.zig").DeferredFace; @@ -562,149 +563,266 @@ pub const CoreText = struct { desc: *const Descriptor, list: []*macos.text.FontDescriptor, ) void { - var desc_mut = desc.*; - if (desc_mut.style == null) { - // If there is no explicit style set, we set a preferred - // based on the style bool attributes. - // - // TODO: doesn't handle i18n font names well, we should have - // another mechanism that uses the weight attribute if it exists. - // Wait for this to be a real problem. - desc_mut.style = if (desc_mut.bold and desc_mut.italic) - "Bold Italic" - else if (desc_mut.bold) - "Bold" - else if (desc_mut.italic) - "Italic" - else - null; - } - - std.mem.sortUnstable(*macos.text.FontDescriptor, list, &desc_mut, struct { + std.mem.sortUnstable(*macos.text.FontDescriptor, list, desc, struct { fn lessThan( desc_inner: *const Descriptor, lhs: *macos.text.FontDescriptor, rhs: *macos.text.FontDescriptor, ) bool { - const lhs_score = score(desc_inner, lhs); - const rhs_score = score(desc_inner, rhs); + const lhs_score: Score = .score(desc_inner, lhs); + const rhs_score: Score = .score(desc_inner, rhs); // Higher score is "less" (earlier) return lhs_score.int() > rhs_score.int(); } }.lessThan); } - /// We represent our sorting score as a packed struct so that we can - /// compare scores numerically but build scores symbolically. + /// We represent our sorting score as a packed struct so that we + /// can compare scores numerically but build scores symbolically. + /// + /// Note that packed structs store their fields from least to most + /// significant, so the fields here are defined in increasing order + /// of precedence. const Score = packed struct { const Backing = @typeInfo(@This()).@"struct".backing_integer.?; - glyph_count: u16 = 0, // clamped if > intmax - traits: Traits = .unmatched, - style: Style = .unmatched, + /// Number of glyphs in the font, if two fonts have identical + /// scores otherwise then we prefer the one with more glyphs. + /// + /// (Number of glyphs clamped at u16 intmax) + glyph_count: u16 = 0, + /// A fuzzy match on the style string, less important than + /// an exact match, and less important than trait matches. + fuzzy_style: u8 = 0, + /// Whether the bold-ness of the font matches the descriptor. + /// This is less important than italic because a font that's italic + /// when it shouldn't be or not italic when it should be is a bigger + /// problem (subjectively) than being the wrong weight. + bold: bool = false, + /// Whether the italic-ness of the font matches the descriptor. + /// This is less important than an exact match on the style string + /// because we want users to be allowed to override trait matching + /// for the bold/italic/bold italic styles if they want. + italic: bool = false, + /// An exact (case-insensitive) match on the style string. + exact_style: bool = false, + /// Whether the font is monospace, this is more important than any of + /// the other fields unless we're looking for a specific codepoint, + /// in which case that is the most important thing. monospace: bool = false, + /// If we're looking for a codepoint, whether this font has it. codepoint: bool = false, - const Traits = enum(u8) { unmatched = 0, _ }; - const Style = enum(u8) { unmatched = 0, match = 0xFF, _ }; - pub fn int(self: Score) Backing { return @bitCast(self); } - }; - fn score(desc: *const Descriptor, ct_desc: *const macos.text.FontDescriptor) Score { - var score_acc: Score = .{}; + fn score(desc: *const Descriptor, ct_desc: *const macos.text.FontDescriptor) Score { + var self: Score = .{}; - // We always load the font if we can since some things can only be - // inspected on the font itself. - const font_: ?*macos.text.Font = macos.text.Font.createWithFontDescriptor( - ct_desc, - 12, - ) catch null; - defer if (font_) |font| font.release(); + // We always load the font if we can since some things can only be + // inspected on the font itself. Fonts that can't be loaded score + // 0 automatically because we don't want a font we can't load. + const font: *macos.text.Font = macos.text.Font.createWithFontDescriptor( + ct_desc, + 12, + ) catch return self; + defer font.release(); - // If we have a font, prefer the font with more glyphs. - if (font_) |font| { - const Type = @TypeOf(score_acc.glyph_count); - score_acc.glyph_count = std.math.cast( - Type, - font.getGlyphCount(), - ) orelse std.math.maxInt(Type); - } - - // If we're searching for a codepoint, prioritize fonts that - // have that codepoint. - if (desc.codepoint > 0) codepoint: { - const font = font_ orelse break :codepoint; - - // Turn UTF-32 into UTF-16 for CT API - var unichars: [2]u16 = undefined; - const pair = macos.foundation.stringGetSurrogatePairForLongCharacter( - desc.codepoint, - &unichars, - ); - const len: usize = if (pair) 2 else 1; - - // Get our glyphs - var glyphs = [2]macos.graphics.Glyph{ 0, 0 }; - score_acc.codepoint = font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len]); - } - - // Get our symbolic traits for the descriptor so we can compare - // boolean attributes like bold, monospace, etc. - const symbolic_traits: macos.text.FontSymbolicTraits = traits: { - const traits = ct_desc.copyAttribute(.traits) orelse break :traits .{}; - defer traits.release(); - - const key = macos.text.FontTraitKey.symbolic.key(); - const symbolic = traits.getValue(macos.foundation.Number, key) orelse - break :traits .{}; - - break :traits macos.text.FontSymbolicTraits.init(symbolic); - }; - - score_acc.monospace = symbolic_traits.monospace; - - score_acc.style = style: { - const style = ct_desc.copyAttribute(.style_name) orelse - break :style .unmatched; - defer style.release(); - - // Get our style string - var buf: [128]u8 = undefined; - const style_str = style.cstring(&buf, .utf8) orelse break :style .unmatched; - - // If we have a specific desired style, attempt to search for that. - if (desc.style) |desired_style| { - // Matching style string gets highest score - if (std.mem.eql(u8, desired_style, style_str)) break :style .match; - } else if (!desc.bold and !desc.italic) { - // If we do not, and we have no symbolic traits, then we try - // to find "regular" (or no style). If we have symbolic traits - // we do nothing but we can improve scoring by taking that into - // account, too. - if (std.mem.eql(u8, "Regular", style_str)) { - break :style .match; - } + // We prefer fonts with more glyphs, all else being equal. + { + const Type = @TypeOf(self.glyph_count); + self.glyph_count = std.math.cast( + Type, + font.getGlyphCount(), + ) orelse std.math.maxInt(Type); } - // Otherwise the score is based on the length of the style string. - // Shorter styles are scored higher. This is a heuristic that - // if we don't have a desired style then shorter tends to be - // more often the "regular" style. - break :style @enumFromInt(100 -| style_str.len); - }; + // If we're searching for a codepoint, then we + // prioritize fonts that have that codepoint. + if (desc.codepoint > 0) { + // Turn UTF-32 into UTF-16 for CT API + var unichars: [2]u16 = undefined; + const pair = macos.foundation.stringGetSurrogatePairForLongCharacter( + desc.codepoint, + &unichars, + ); + const len: usize = if (pair) 2 else 1; - score_acc.traits = traits: { - var count: u8 = 0; - if (desc.bold == symbolic_traits.bold) count += 1; - if (desc.italic == symbolic_traits.italic) count += 1; - break :traits @enumFromInt(count); - }; + // Get our glyphs + var glyphs = [2]macos.graphics.Glyph{ 0, 0 }; + self.codepoint = font.getGlyphsForCharacters( + unichars[0..len], + glyphs[0..len], + ); + } - return score_acc; - } + // Get our symbolic traits for the descriptor so we can + // compare boolean attributes like bold, monospace, etc. + const symbolic_traits: macos.text.FontSymbolicTraits = traits: { + const traits = ct_desc.copyAttribute(.traits) orelse break :traits .{}; + defer traits.release(); + + const key = macos.text.FontTraitKey.symbolic.key(); + const symbolic = traits.getValue(macos.foundation.Number, key) orelse + break :traits .{}; + + break :traits macos.text.FontSymbolicTraits.init(symbolic); + }; + + self.monospace = symbolic_traits.monospace; + + // We try to derived data from the font itself, which is generally + // more reliable than only using the symbolic traits for this. + const is_bold: bool, const is_italic: bool = derived: { + // We start with initial guesses based on the symbolic traits, + // but refine these with more information if we can get it. + var is_italic = symbolic_traits.italic; + var is_bold = symbolic_traits.bold; + + // Read the 'head' table out of the font data if it's available. + if (head: { + const tag = macos.text.FontTableTag.init("head"); + const data = font.copyTable(tag) orelse break :head null; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :head opentype.Head.init(ptr[0..len]) catch |err| { + log.warn("error parsing head table: {}", .{err}); + break :head null; + }; + }) |head_| { + const head: opentype.Head = head_; + is_bold = is_bold or (head.macStyle & 1 == 1); + is_italic = is_italic or (head.macStyle & 2 == 2); + } + + // Read the 'OS/2' table out of the font data if it's available. + if (os2: { + const tag = macos.text.FontTableTag.init("OS/2"); + const data = font.copyTable(tag) orelse break :os2 null; + defer data.release(); + const ptr = data.getPointer(); + const len = data.getLength(); + break :os2 opentype.OS2.init(ptr[0..len]) catch |err| { + log.warn("error parsing OS/2 table: {}", .{err}); + break :os2 null; + }; + }) |os2| { + is_bold = is_bold or os2.fsSelection.bold; + is_italic = is_italic or os2.fsSelection.italic; + } + + // Check if we have variation axes in our descriptor, if we + // do then we can derive weight italic-ness or both from them. + if (font.copyAttribute(.variation_axes)) |axes| variations: { + defer axes.release(); + + // Copy the variation values for this instance of the font. + // if there are none then we just break out immediately. + const values: *macos.foundation.Dictionary = + font.copyAttribute(.variation) orelse break :variations; + defer values.release(); + + var buf: [1024]u8 = undefined; + + // If we see the 'ital' value then we ignore 'slnt'. + var ital_seen = false; + + const len = axes.getCount(); + for (0..len) |i| { + const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i); + const Key = macos.text.FontVariationAxisKey; + const cf_id = dict.getValue(Key.identifier.Value(), Key.identifier.key()).?; + const cf_name = dict.getValue(Key.name.Value(), Key.name.key()).?; + const cf_def = dict.getValue(Key.default_value.Value(), Key.default_value.key()).?; + + const name_str = cf_name.cstring(&buf, .utf8) orelse ""; + + // Default value + var def: f64 = 0; + _ = cf_def.getValue(.double, &def); + // Value in this font + var val: f64 = def; + if (values.getValue( + macos.foundation.Number, + cf_id, + )) |cf_val| _ = cf_val.getValue(.double, &val); + + if (std.mem.eql(u8, "wght", name_str)) { + // Somewhat subjective threshold, we consider fonts + // bold if they have a 'wght' set greater than 600. + is_bold = val > 600; + continue; + } + if (std.mem.eql(u8, "ital", name_str)) { + is_italic = val > 0.5; + ital_seen = true; + continue; + } + if (!ital_seen and std.mem.eql(u8, "slnt", name_str)) { + // Arbitrary threshold of anything more than a 5 + // degree clockwise slant is considered italic. + is_italic = val <= -5.0; + continue; + } + } + } + + break :derived .{ is_bold, is_italic }; + }; + + self.bold = desc.bold == is_bold; + self.italic = desc.italic == is_italic; + + // Get the style string from the font. + var style_str_buf: [128]u8 = undefined; + const style_str: []const u8 = style_str: { + const style = ct_desc.copyAttribute(.style_name) orelse + break :style_str ""; + defer style.release(); + + break :style_str style.cstring(&style_str_buf, .utf8) orelse ""; + }; + + // The first string in this slice will be used for the exact match, + // and for the fuzzy match, all matching substrings will increase + // the rank. + const desired_styles: []const [:0]const u8 = desired: { + if (desc.style) |s| break :desired &.{s}; + + // If we don't have an explicitly desired style name, we base + // it on the bold and italic properties, this isn't ideal since + // fonts may use style names other than these, but it helps in + // some edge cases. + if (desc.bold) { + if (desc.italic) break :desired &.{ "bold italic", "bold", "italic", "oblique" }; + break :desired &.{ "bold", "upright" }; + } else if (desc.italic) { + break :desired &.{ "italic", "regular", "oblique" }; + } + break :desired &.{ "regular", "upright" }; + }; + + self.exact_style = std.ascii.eqlIgnoreCase( + style_str, + desired_styles[0], + ); + // Our "fuzzy match" score is 0 if the desired style isn't present + // in the string, otherwise we give higher priority for styles that + // have fewer characters not in the desired_styles list. + const fuzzy_type = @TypeOf(self.fuzzy_style); + self.fuzzy_style = @intCast(style_str.len); + for (desired_styles) |s| { + if (std.ascii.indexOfIgnoreCase(style_str, s) != null) { + self.fuzzy_style -|= @intCast(s.len); + } + } + self.fuzzy_style = std.math.maxInt(fuzzy_type) -| self.fuzzy_style; + + return self; + } + }; pub const DiscoverIterator = struct { alloc: Allocator, @@ -837,3 +955,85 @@ test "coretext codepoint" { // Should have other codepoints too try testing.expect(face.hasCodepoint('B', null)); } + +test "coretext sorting" { + if (options.backend != .coretext and options.backend != .coretext_freetype) + return error.SkipZigTest; + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!// + // FIXME: Disabled for now because SF Pro is not available in CI + // The solution likely involves directly testing that the + // `sortMatchingDescriptors` function sorts a bundled test + // font correctly, instead of relying on the system fonts. + if (true) return error.SkipZigTest; + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!// + + const testing = std.testing; + const alloc = testing.allocator; + + var ct = CoreText.init(); + defer ct.deinit(); + + // We try to get a Regular, Italic, Bold, & Bold Italic version of SF Pro, + // which should be installed on all Macs, and has many styles which makes + // it a good test, since there will be many results for each discovery. + + // Regular + { + var it = try ct.discover(alloc, .{ + .family = "SF Pro", + .size = 12, + }); + defer it.deinit(); + const res = (try it.next()).?; + var buf: [1024]u8 = undefined; + const name = try res.name(&buf); + try testing.expectEqualStrings("SF Pro Regular", name); + } + + // Regular Italic + // + // NOTE: This makes sure that we don't accidentally prefer "Thin Italic", + // which we previously did, because it has a shorter name. + { + var it = try ct.discover(alloc, .{ + .family = "SF Pro", + .size = 12, + .italic = true, + }); + defer it.deinit(); + const res = (try it.next()).?; + var buf: [1024]u8 = undefined; + const name = try res.name(&buf); + try testing.expectEqualStrings("SF Pro Regular Italic", name); + } + + // Bold + { + var it = try ct.discover(alloc, .{ + .family = "SF Pro", + .size = 12, + .bold = true, + }); + defer it.deinit(); + const res = (try it.next()).?; + var buf: [1024]u8 = undefined; + const name = try res.name(&buf); + try testing.expectEqualStrings("SF Pro Bold", name); + } + + // Bold Italic + { + var it = try ct.discover(alloc, .{ + .family = "SF Pro", + .size = 12, + .bold = true, + .italic = true, + }); + defer it.deinit(); + const res = (try it.next()).?; + var buf: [1024]u8 = undefined; + const name = try res.name(&buf); + try testing.expectEqualStrings("SF Pro Bold Italic", name); + } +} From 9ded668819910ae6f3245f78f17645131cac49fe Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 22:56:10 +0200 Subject: [PATCH 332/642] gtk(wayland,x11): remove even more redundant checks --- src/apprt/gtk/winproto/wayland.zig | 114 +++++++++++------------------ src/apprt/gtk/winproto/x11.zig | 7 +- 2 files changed, 45 insertions(+), 76 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 594a3382a..08f4858a5 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -76,9 +76,9 @@ pub const App = struct { registry.setListener(*Context, registryListener, context); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; - if (context.kde_decoration_manager != null) { - // FIXME: Roundtrip again because we have to wait for the decoration - // manager to respond with the preferred default mode. Ew. + // Do another round-trip to get the default decoration mode + if (context.kde_decoration_manager) |deco_manager| { + deco_manager.setListener(*Context, decoManagerListener, context); if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed; } @@ -121,80 +121,54 @@ pub const App = struct { event: wl.Registry.Event, context: *Context, ) void { - switch (event) { - // https://wayland.app/protocols/wayland#wl_registry:event:global - .global => |global| { - log.debug("wl_registry.global: interface={s}", .{global.interface}); + inline for (@typeInfo(Context).@"struct".fields) |field| { + // Globals should be optional pointers + const T = switch (@typeInfo(field.type)) { + .optional => |o| switch (@typeInfo(o.child)) { + .pointer => |v| v.child, + else => continue, + }, + else => continue, + }; - if (registryBind( - org.KdeKwinBlurManager, - registry, - global, - )) |blur_manager| { - context.kde_blur_manager = blur_manager; - return; - } + // Only process Wayland interfaces + if (!@hasDecl(T, "interface")) continue; - if (registryBind( - org.KdeKwinServerDecorationManager, - registry, - global, - )) |deco_manager| { - context.kde_decoration_manager = deco_manager; - deco_manager.setListener(*Context, decoManagerListener, context); - return; - } + switch (event) { + .global => |v| global: { + if (std.mem.orderZ( + u8, + v.interface, + T.interface.name, + ) != .eq) break :global; - if (registryBind( - org.KdeKwinSlideManager, - registry, - global, - )) |slide_manager| { - context.kde_slide_manager = slide_manager; - return; - } + @field(context, field.name) = registry.bind( + v.name, + T, + T.generated_version, + ) catch |err| { + log.warn( + "error binding interface {s} error={}", + .{ v.interface, err }, + ); + return; + }; + }, - if (registryBind( - xdg.ActivationV1, - registry, - global, - )) |activation| { - context.xdg_activation = activation; - return; - } - }, - - // We don't handle removal events - .global_remove => {}, + // This should be a rare occurrence, but in case a global + // is suddenly no longer available, we destroy and unset it + // as the protocol mandates. + .global_remove => |v| remove: { + const global = @field(context, field.name) orelse break :remove; + if (global.getId() == v.name) { + global.destroy(); + @field(context, field.name) = null; + } + }, + } } } - /// Bind a Wayland interface to a global object. Returns non-null - /// if the binding was successful, otherwise null. - /// - /// The type T is the Wayland interface type that we're requesting. - /// This function will verify that the global object is the correct - /// interface and version before binding. - fn registryBind( - comptime T: type, - registry: *wl.Registry, - global: anytype, - ) ?*T { - if (std.mem.orderZ( - u8, - global.interface, - T.interface.name, - ) != .eq) return null; - - return registry.bind(global.name, T, T.generated_version) catch |err| { - log.warn("error binding interface {s} error={}", .{ - global.interface, - err, - }); - return null; - }; - } - fn decoManagerListener( _: *org.KdeKwinServerDecorationManager, event: org.KdeKwinServerDecorationManager.Event, diff --git a/src/apprt/gtk/winproto/x11.zig b/src/apprt/gtk/winproto/x11.zig index 2c4925167..624de03f8 100644 --- a/src/apprt/gtk/winproto/x11.zig +++ b/src/apprt/gtk/winproto/x11.zig @@ -36,16 +36,11 @@ pub const App = struct { config: *const Config, ) !?App { // If the display isn't X11, then we don't need to do anything. - if (gobject.typeCheckInstanceIsA( - gdk_display.as(gobject.TypeInstance), - gdk_x11.X11Display.getGObjectType(), - ) == 0) return null; - - // Get our X11 display const gdk_x11_display = gobject.ext.cast( gdk_x11.X11Display, gdk_display, ) orelse return null; + const xlib_display = gdk_x11_display.getXdisplay(); const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn| From f99c988b27e0022dbd48309d1915eeddcee1a51d Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Fri, 30 May 2025 22:56:10 +0200 Subject: [PATCH 333/642] gtk(wayland): automatically bind globals --- src/apprt/gtk/winproto/wayland.zig | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/apprt/gtk/winproto/wayland.zig b/src/apprt/gtk/winproto/wayland.zig index 08f4858a5..cbe8c01a4 100644 --- a/src/apprt/gtk/winproto/wayland.zig +++ b/src/apprt/gtk/winproto/wayland.zig @@ -48,16 +48,11 @@ pub const App = struct { _ = config; _ = app_id; - // Check if we're actually on Wayland - if (gobject.typeCheckInstanceIsA( - gdk_display.as(gobject.TypeInstance), - gdk_wayland.WaylandDisplay.getGObjectType(), - ) == 0) return null; - const gdk_wayland_display = gobject.ext.cast( gdk_wayland.WaylandDisplay, gdk_display, - ) orelse return error.NoWaylandDisplay; + ) orelse return null; + const display: *wl.Display = @ptrCast(@alignCast( gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay, )); From fd7132db7142515a63251b6522dbb019f6d16f9d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 May 2025 15:05:53 -0700 Subject: [PATCH 334/642] macos: quick terminal can equalize splits Fixes #7480 --- .../Terminal/BaseTerminalController.swift | 16 ++++++++++++++++ .../Features/Terminal/TerminalController.swift | 16 ---------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 62384586a..9862e1288 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -124,6 +124,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyMaximizeDidToggle(_:)), name: .ghosttyMaximizeDidToggle, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidEqualizeSplits(_:)), + name: Ghostty.Notification.didEqualizeSplits, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -249,6 +254,17 @@ class BaseTerminalController: NSWindowController, guard surfaceTree?.contains(view: surfaceView) ?? false else { return } window.zoom(nil) } + + @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { + guard let target = notification.object as? Ghostty.SurfaceView else { return } + + // Check if target surface is in current controller's tree + guard surfaceTree?.contains(view: target) ?? false else { return } + + if case .split(let container) = surfaceTree { + _ = container.equalize() + } + } // MARK: Local Events diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cf2dd3348..f2868adb0 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -85,12 +85,6 @@ class TerminalController: BaseTerminalController { selector: #selector(onFrameDidChange), name: NSView.frameDidChangeNotification, object: nil) - center.addObserver( - self, - selector: #selector(onEqualizeSplits), - name: Ghostty.Notification.didEqualizeSplits, - object: nil - ) center.addObserver( self, selector: #selector(onCloseWindow), @@ -875,16 +869,6 @@ class TerminalController: BaseTerminalController { toggleFullscreen(mode: fullscreenMode) } - @objc private func onEqualizeSplits(_ notification: Notification) { - guard let target = notification.object as? Ghostty.SurfaceView else { return } - - // Check if target surface is in current controller's tree - guard surfaceTree?.contains(view: target) ?? false else { return } - - if case .split(let container) = surfaceTree { - _ = container.equalize() - } - } struct DerivedConfig { let backgroundColor: Color From dd670f5107ea47cb7d3a76cd0e93955523f092d7 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 30 May 2025 17:52:31 -0600 Subject: [PATCH 335/642] font/sprite: rework `yQuads` and friends for better alignment with `draw_block` This improves "outer edge" alignment of octants and other elements drawn using `yQuads` and friends with blocks drawn with `draw_block` -- this should guarantee alignment along a continuous edge, but may result in a 1px overlap of opposing edges (such as a top half block followed by a bottom half block with an odd cell height, they will both have the center row filled). This is very necessary since several block elements are needed to complete the set of octants, since dedicated octant characters aren't included when they would be redundant. --- src/font/sprite/Box.zig | 90 +++++++++++++++++++------------ src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 1048593 bytes 2 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index f3942b83d..dd02f701b 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -2488,10 +2488,10 @@ fn draw_sextant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { if (sex.tl) self.rect(canvas, 0, 0, x_halfs[0], y_thirds[0]); if (sex.tr) self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_thirds[0]); - if (sex.ml) self.rect(canvas, 0, y_thirds[0], x_halfs[0], y_thirds[1]); - if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[0], self.metrics.cell_width, y_thirds[1]); - if (sex.bl) self.rect(canvas, 0, y_thirds[1], x_halfs[0], self.metrics.cell_height); - if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, self.metrics.cell_height); + if (sex.ml) self.rect(canvas, 0, y_thirds[1], x_halfs[0], y_thirds[2]); + if (sex.mr) self.rect(canvas, x_halfs[1], y_thirds[1], self.metrics.cell_width, y_thirds[2]); + if (sex.bl) self.rect(canvas, 0, y_thirds[3], x_halfs[0], self.metrics.cell_height); + if (sex.br) self.rect(canvas, x_halfs[1], y_thirds[3], self.metrics.cell_width, self.metrics.cell_height); } fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { @@ -2545,42 +2545,58 @@ fn draw_octant(self: Box, canvas: *font.sprite.Canvas, cp: u32) void { const oct = octants[cp - octant_min]; if (oct.@"1") self.rect(canvas, 0, 0, x_halfs[0], y_quads[0]); if (oct.@"2") self.rect(canvas, x_halfs[1], 0, self.metrics.cell_width, y_quads[0]); - if (oct.@"3") self.rect(canvas, 0, y_quads[0], x_halfs[0], y_quads[1]); - if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[0], self.metrics.cell_width, y_quads[1]); - if (oct.@"5") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); - if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]); - if (oct.@"7") self.rect(canvas, 0, y_quads[2], x_halfs[0], self.metrics.cell_height); - if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[2], self.metrics.cell_width, self.metrics.cell_height); + if (oct.@"3") self.rect(canvas, 0, y_quads[1], x_halfs[0], y_quads[2]); + if (oct.@"4") self.rect(canvas, x_halfs[1], y_quads[1], self.metrics.cell_width, y_quads[2]); + if (oct.@"5") self.rect(canvas, 0, y_quads[3], x_halfs[0], y_quads[4]); + if (oct.@"6") self.rect(canvas, x_halfs[1], y_quads[3], self.metrics.cell_width, y_quads[4]); + if (oct.@"7") self.rect(canvas, 0, y_quads[5], x_halfs[0], self.metrics.cell_height); + if (oct.@"8") self.rect(canvas, x_halfs[1], y_quads[5], self.metrics.cell_width, self.metrics.cell_height); } +/// xHalfs[0] should be used as the right edge of a left-aligned half. +/// xHalfs[1] should be used as the left edge of a right-aligned half. fn xHalfs(self: Box) [2]u32 { + const float_width: f64 = @floatFromInt(self.metrics.cell_width); + const half_width: u32 = @intFromFloat(@round(0.5 * float_width)); + return .{ half_width, self.metrics.cell_width - half_width }; +} + +/// Use these values as such: +/// yThirds[0] bottom edge of the first third. +/// yThirds[1] top edge of the second third. +/// yThirds[2] bottom edge of the second third. +/// yThirds[3] top edge of the final third. +fn yThirds(self: Box) [4]u32 { + const float_height: f64 = @floatFromInt(self.metrics.cell_height); + const one_third_height: u32 = @intFromFloat(@round(one_third * float_height)); + const two_thirds_height: u32 = @intFromFloat(@round(two_thirds * float_height)); return .{ - @as(u32, @intFromFloat(@round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2))), - @as(u32, @intFromFloat(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2)), + one_third_height, + self.metrics.cell_height - two_thirds_height, + two_thirds_height, + self.metrics.cell_height - one_third_height, }; } -fn yThirds(self: Box) [2]u32 { - return switch (@mod(self.metrics.cell_height, 3)) { - 0 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 }, - 1 => .{ self.metrics.cell_height / 3, 2 * self.metrics.cell_height / 3 + 1 }, - 2 => .{ self.metrics.cell_height / 3 + 1, 2 * self.metrics.cell_height / 3 }, - else => unreachable, - }; -} - -// assume octants might be striped across multiple rows of cells. to maximize -// distance between excess pixellines, we want (1) an arbitrary region (there -// will be a pattern of 1'-3-1'-3-1'-3 no matter what), (2) discontiguous -// regions (0 and 2 or 1 and 3), and (3) an arbitrary three regions (there will -// be a pattern of 3-1-3-1-3-1 no matter what). -fn yQuads(self: Box) [3]u32 { - return switch (@mod(self.metrics.cell_height, 4)) { - 0 => .{ self.metrics.cell_height / 4, 2 * self.metrics.cell_height / 4, 3 * self.metrics.cell_height / 4 }, - 1 => .{ self.metrics.cell_height / 4, 2 * self.metrics.cell_height / 4 + 1, 3 * self.metrics.cell_height / 4 }, - 2 => .{ self.metrics.cell_height / 4 + 1, 2 * self.metrics.cell_height / 4, 3 * self.metrics.cell_height / 4 + 1 }, - 3 => .{ self.metrics.cell_height / 4 + 1, 2 * self.metrics.cell_height / 4 + 1, 3 * self.metrics.cell_height / 4 }, - else => unreachable, +/// Use these values as such: +/// yQuads[0] bottom edge of first quarter. +/// yQuads[1] top edge of second quarter. +/// yQuads[2] bottom edge of second quarter. +/// yQuads[3] top edge of third quarter. +/// yQuads[4] bottom edge of third quarter +/// yQuads[5] top edge of fourth quarter. +fn yQuads(self: Box) [6]u32 { + const float_height: f64 = @floatFromInt(self.metrics.cell_height); + const quarter_height: u32 = @intFromFloat(@round(0.25 * float_height)); + const half_height: u32 = @intFromFloat(@round(0.50 * float_height)); + const three_quarters_height: u32 = @intFromFloat(@round(0.75 * float_height)); + return .{ + quarter_height, + self.metrics.cell_height - three_quarters_height, + half_height, + self.metrics.cell_height - half_height, + three_quarters_height, + self.metrics.cell_height - quarter_height, }; } @@ -2591,8 +2607,12 @@ fn draw_smooth_mosaic( ) !void { const y_thirds = self.yThirds(); const top: f64 = 0.0; - const upper: f64 = @floatFromInt(y_thirds[0]); - const lower: f64 = @floatFromInt(y_thirds[1]); + // We average the edge positions for the y_thirds boundaries here + // rather than having to deal with varying alignments depending on + // the surrounding pieces. The most this will be off by is half of + // a pixel, so hopefully it's not noticeable. + const upper: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[0])) + @as(f64, @floatFromInt(y_thirds[1]))); + const lower: f64 = 0.5 * (@as(f64, @floatFromInt(y_thirds[2])) + @as(f64, @floatFromInt(y_thirds[3]))); const bottom: f64 = @floatFromInt(self.metrics.cell_height); const left: f64 = 0.0; const center: f64 = @round(@as(f64, @floatFromInt(self.metrics.cell_width)) / 2); diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm index 0feb3ebe49e45f69c4e877b1a086a56822f4af20..d5a6cc72906318b235e33bc3ea53be8f88b67193 100644 GIT binary patch delta 1279 zcmbQ(;4rblp`nGbg{g(Pg{6hHg{_6Xg`c*E~YgFX;0`fLPYk9ejC}rW+{p$V@*V z%gaAeK?o{*nTvgU>`$H_T+<8sc$Z>Ga%_)d<#jZH+iC-{)fU}WnEg5+LpGop0LJjv4!nDRzIbsB@*rZZjPH&4X$ z1OpzO=?-4JEg=8tOi$q8ZGoz4oc_RIV%Rf-@5Vo*6co6w-l%Lj^+rI3Kx z^Ml7}`ko&=7Sj!G^RR8N^WYBa;HJP0`{TK^a3y5Zctj# z*2gGm0`sF1`AN$C?LxeLLLCQfL( NLIabm$WS811pqoKzb*g( delta 1380 zcmbQ(;4rblp`nGbg{g(Pg{6hHg{_6Xg`u3U2i)xD0^aKMQ#pxH6c@)92Fq5_y81b%PoEE~!HvK|2kL+}V z241Pj0q+H-E3op~O?Tknbp$KNWtbRPcA|nY*bCba*z%TOx^yYrZt3X;w|Us6+k5Z^ zZNK2jyP0vifFrN@^bck{+0zx8cvZosOh4es+Yi#Bvt1yNmz^EvRAli`-UyB94$|DP zumy+6^XVV1bLmV^_`x%4dVv)$|1<#}4Ty;f`nbha1t@lI!) zp5Vfx0}i(722ISY(-$Q1vQJ-N#oGWDo2UQ|;lZH`nm82dd3~o}=;w9csF27!{X#j9 h*z^Pz-b%20H!36%A++6LEB95dQIhfy*r+g<8304Z_Avke From 2b9e781933834f1123024b275eea1a44afff468d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 30 May 2025 16:08:57 -0700 Subject: [PATCH 336/642] gtk: clean up per-surface cgroup on close Fixes #6766 This ensures that during surface deinit the cgroup is removed. By the time the surface is deinitialized, the subprocess should already be dead so the cgroup can be safely removed. If the cgroup cannot be removed for any reason we log a warning. --- src/apprt/gtk/Surface.zig | 16 +++++++++++++++- src/os/cgroup.zig | 19 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 3d16e9fbb..e51109015 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -746,7 +746,21 @@ pub fn deinit(self: *Surface) void { self.core_surface.deinit(); self.core_surface = undefined; - if (self.cgroup_path) |path| self.app.core_app.alloc.free(path); + // Remove the cgroup if we have one. We do this after deiniting the core + // surface to ensure all processes have exited. + if (self.cgroup_path) |path| { + internal_os.cgroup.remove(path) catch |err| { + // We don't want this to be fatal in any way so we just log + // and continue. A dangling empty cgroup is not a big deal + // and this should be rare. + log.warn( + "failed to remove cgroup for surface path={s} err={}", + .{ path, err }, + ); + }; + + self.app.core_app.alloc.free(path); + } // Free all our GTK stuff // diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 5645e337a..4f13921c5 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -56,6 +56,25 @@ pub fn create( } } +/// Remove a cgroup. This will only succeed if the cgroup is empty +/// (has no processes). The cgroup path should be relative to the +/// cgroup root (e.g. "/user.slice/surfaces/abc123.scope"). +pub fn remove(cgroup: []const u8) !void { + assert(cgroup.len > 0); + assert(cgroup[0] == '/'); + + var buf: [std.fs.max_path_bytes]u8 = undefined; + const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}", .{cgroup}); + std.fs.cwd().deleteDir(path) catch |err| switch (err) { + // If it doesn't exist, that's fine - maybe it was already cleaned up + error.FileNotFound => {}, + + // Any other error we failed to delete it so we want to notify + // the user. + else => return err, + }; +} + /// Move the given PID into the given cgroup. pub fn moveInto( cgroup: []const u8, From 5306e7cf567ccb37028701a00504bcf28484b155 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Jun 2025 08:34:03 -0700 Subject: [PATCH 337/642] config: add launched-from to specify launch source Related to #7433 This extracts our "launched from desktop" logic into a config option. The default value is detection using the same logic as before, but now this can be overridden by the user. This also adds the systemd and dbus activation sources from #7433. There are a number of reasons why we decided to do this: 1. It automatically gets us caching since the configuration is only loaded once (per reload, a rare occurrence). 2. It allows us to override the logic when testing. Previously, we had to do more complex environment faking to get the same behavior. 3. It forces exhaustive switches in any desktop handling code, which will make it easier to ensure valid behaviors if we introduce new launch sources (as we are in #7433). 4. It lowers code complexity since callsites don't need to have N `launchedFromX()` checks and can use a single value. --- src/apprt/embedded.zig | 5 +++- src/apprt/gtk/App.zig | 5 +++- src/config/Config.zig | 66 +++++++++++++++++++++++++++++++++++++----- src/os/dbus.zig | 21 ++++++++++++++ src/os/locale.zig | 7 ++--- src/os/main.zig | 4 +++ src/os/systemd.zig | 65 +++++++++++++++++++++++++++++++++++++++++ 7 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 src/os/dbus.zig create mode 100644 src/os/systemd.zig diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 97466e9b5..67aeeaf7c 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -842,7 +842,10 @@ pub const Surface = struct { // our translation settings for Ghostty. If we aren't from // the desktop then we didn't set our LANGUAGE var so we // don't need to remove it. - if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE"); + switch (self.app.config.@"launched-from".?) { + .desktop => env.remove("LANGUAGE"), + .dbus, .systemd, .cli => {}, + } } return env; diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index d1c8f2c59..d69102bda 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -273,7 +273,10 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { const single_instance = switch (config.@"gtk-single-instance") { .true => true, .false => false, - .desktop => internal_os.launchedFromDesktop(), + .desktop => switch (config.@"launched-from".?) { + .desktop, .systemd, .dbus => true, + .cli => false, + }, }; // Setup the flags for our application. diff --git a/src/config/Config.zig b/src/config/Config.zig index 344c118d7..bf6d26f4b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2428,6 +2428,23 @@ term: []const u8 = "xterm-ghostty", /// running. Defaults to an empty string if not set. @"enquiry-response": []const u8 = "", +/// The mechanism used to launch Ghostty. This should generally not be +/// set by users, see the warning below. +/// +/// WARNING: This is a low-level configuration that is not intended to be +/// modified by users. All the values will be automatically detected as they +/// are needed by Ghostty. This is only here in case our detection logic is +/// incorrect for your environment or for developers who want to test +/// Ghostty's behavior in different, forced environments. +/// +/// This is set using the standard `no-[value]`, `[value]` syntax separated +/// by commas. Example: "no-desktop,systemd". Specific details about the +/// available values are documented on LaunchProperties in the code. Since +/// 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. +@"launched-from": ?LaunchSource = null, + /// Configures the low-level API to use for async IO, eventing, etc. /// /// Most users should leave this set to `auto`. This will automatically detect @@ -3111,6 +3128,11 @@ pub fn finalize(self: *Config) !void { const alloc = self._arena.?.allocator(); + // Ensure our launch source is properly set. + if (self.@"launched-from" == null) { + self.@"launched-from" = .detect(); + } + // If we have a font-family set and don't set the others, default // the others to the font family. This way, if someone does // --font-family=foo, then we try to get the stylized versions of @@ -3135,14 +3157,11 @@ pub fn finalize(self: *Config) !void { } // The default for the working directory depends on the system. - const wd = self.@"working-directory" orelse wd: { + const wd = self.@"working-directory" orelse switch (self.@"launched-from".?) { // If we have no working directory set, our default depends on - // whether we were launched from the desktop or CLI. - if (internal_os.launchedFromDesktop()) { - break :wd "home"; - } - - break :wd "inherit"; + // whether we were launched from the desktop or elsewhere. + .desktop => "home", + .cli, .dbus, .systemd => "inherit", }; // If we are missing either a command or home directory, we need @@ -3165,7 +3184,10 @@ pub fn finalize(self: *Config) !void { // If we were launched from the desktop, our SHELL env var // will represent our SHELL at login time. We want to use the // latest shell from /etc/passwd or directory services. - if (internal_os.launchedFromDesktop()) break :shell_env; + switch (self.@"launched-from".?) { + .desktop, .dbus, .systemd => break :shell_env, + .cli => {}, + } if (std.process.getEnvVarOwned(alloc, "SHELL")) |value| { log.info("default shell source=env value={s}", .{value}); @@ -6595,6 +6617,34 @@ pub const Duration = struct { } }; +pub const LaunchSource = enum { + /// Ghostty was launched via the CLI. This is the default if + /// no other source is detected. + cli, + + /// Ghostty was launched in a desktop environment (not via the CLI). + /// This is used to determine some behaviors such as how to read + /// settings, whether single instance defaults to true, etc. + desktop, + + /// Ghostty was started via dbus activation. + dbus, + + /// Ghostty was started via systemd activation. + systemd, + + pub fn detect() LaunchSource { + return if (internal_os.launchedFromDesktop()) + .desktop + else if (internal_os.launchedByDbusActivation()) + .dbus + else if (internal_os.launchedBySystemd()) + .systemd + else + .cli; + } +}; + pub const WindowPadding = struct { const Self = @This(); diff --git a/src/os/dbus.zig b/src/os/dbus.zig new file mode 100644 index 000000000..99824db71 --- /dev/null +++ b/src/os/dbus.zig @@ -0,0 +1,21 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +/// Returns true if the program was launched by D-Bus activation. +/// +/// On Linux GTK, this returns true if the program was launched using D-Bus +/// activation. It will return false if Ghostty was launched any other way. +/// +/// For other platforms and app runtimes, this returns false. +pub fn launchedByDbusActivation() bool { + return switch (builtin.os.tag) { + // On Linux, D-Bus activation sets `DBUS_STARTER_ADDRESS` and + // `DBUS_STARTER_BUS_TYPE`. If these environment variables are present + // (no matter the value) we were launched by D-Bus activation. + .linux => std.posix.getenv("DBUS_STARTER_ADDRESS") != null and + std.posix.getenv("DBUS_STARTER_BUS_TYPE") != null, + + // No other system supports D-Bus so always return false. + else => false, + }; +} diff --git a/src/os/locale.zig b/src/os/locale.zig index 17e4d163c..b391d690f 100644 --- a/src/os/locale.zig +++ b/src/os/locale.zig @@ -108,11 +108,8 @@ fn setLangFromCocoa() void { } // Get our preferred languages and set that to the LANGUAGE - // env var in case our language differs from our locale. We only - // do this when the app is launched from the desktop because then - // we're in an app bundle and we are expected to read from our - // Bundle's preferred languages. - if (internal_os.launchedFromDesktop()) language: { + // env var in case our language differs from our locale. + language: { var buf: [1024]u8 = undefined; const pref_ = preferredLanguageFromCocoa( &buf, diff --git a/src/os/main.zig b/src/os/main.zig index 36833f427..582ac75cd 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -2,6 +2,7 @@ //! system. These aren't restricted to syscalls or low-level operations, but //! also OS-specific features and conventions. +const dbus = @import("dbus.zig"); const desktop = @import("desktop.zig"); const env = @import("env.zig"); const file = @import("file.zig"); @@ -12,6 +13,7 @@ const mouse = @import("mouse.zig"); const openpkg = @import("open.zig"); const pipepkg = @import("pipe.zig"); const resourcesdir = @import("resourcesdir.zig"); +const systemd = @import("systemd.zig"); // Namespaces pub const args = @import("args.zig"); @@ -35,6 +37,8 @@ pub const getenv = env.getenv; pub const setenv = env.setenv; pub const unsetenv = env.unsetenv; pub const launchedFromDesktop = desktop.launchedFromDesktop; +pub const launchedByDbusActivation = dbus.launchedByDbusActivation; +pub const launchedBySystemd = systemd.launchedBySystemd; pub const desktopEnvironment = desktop.desktopEnvironment; pub const rlimit = file.rlimit; pub const fixMaxFiles = file.fixMaxFiles; diff --git a/src/os/systemd.zig b/src/os/systemd.zig new file mode 100644 index 000000000..9b67296d6 --- /dev/null +++ b/src/os/systemd.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +const log = std.log.scoped(.systemd); + +/// Returns true if the program was launched as a systemd service. +/// +/// On Linux, this returns true if the program was launched as a systemd +/// service. It will return false if Ghostty was launched any other way. +/// +/// For other platforms and app runtimes, this returns false. +pub fn launchedBySystemd() bool { + return switch (builtin.os.tag) { + .linux => linux: { + // On Linux, systemd sets the `INVOCATION_ID` (v232+) and the + // `JOURNAL_STREAM` (v231+) environment variables. If these + // environment variables are not present we were not launched by + // systemd. + if (std.posix.getenv("INVOCATION_ID") == null) break :linux false; + if (std.posix.getenv("JOURNAL_STREAM") == null) break :linux false; + + // If `INVOCATION_ID` and `JOURNAL_STREAM` are present, check to make sure + // that our parent process is actually `systemd`, not some other terminal + // emulator that doesn't clean up those environment variables. + const ppid = std.os.linux.getppid(); + if (ppid == 1) break :linux true; + + // If the parent PID is not 1 we need to check to see if we were launched by + // a user systemd daemon. Do that by checking the `/proc//comm` + // to see if it ends with `systemd`. + var comm_path_buf: [std.fs.max_path_bytes]u8 = undefined; + const comm_path = std.fmt.bufPrint(&comm_path_buf, "/proc/{d}/comm", .{ppid}) catch { + log.err("unable to format comm path for pid {d}", .{ppid}); + break :linux false; + }; + const comm_file = std.fs.openFileAbsolute(comm_path, .{ .mode = .read_only }) catch { + log.err("unable to open '{s}' for reading", .{comm_path}); + break :linux false; + }; + defer comm_file.close(); + + // The maximum length of the command name is defined by + // `TASK_COMM_LEN` in the Linux kernel. This is usually 16 + // bytes at the time of writing (Jun 2025) so its set to that. + // Also, since we only care to compare to "systemd", anything + // longer can be assumed to not be systemd. + const TASK_COMM_LEN = 16; + var comm_data_buf: [TASK_COMM_LEN]u8 = undefined; + const comm_size = comm_file.readAll(&comm_data_buf) catch { + log.err("problems reading from '{s}'", .{comm_path}); + break :linux false; + }; + const comm_data = comm_data_buf[0..comm_size]; + + break :linux std.mem.eql( + u8, + std.mem.trimRight(u8, comm_data, "\n"), + "systemd", + ); + }, + + // No other system supports systemd so always return false. + else => false, + }; +} From 85beda9c49066001df5c5fdbd351c106d6c3c2a7 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 1 Jun 2025 14:04:14 -0700 Subject: [PATCH 338/642] Fix reset zoom button visibility in macOS "tabs" mode when no tabs --- .../Features/Terminal/TerminalWindow.swift | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 62b8dc5bf..48384a827 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -372,21 +372,10 @@ class TerminalWindow: NSWindow { private func updateResetZoomTitlebarButtonVisibility() { guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } - let isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed - - if titlebarTabs { - resetZoomToolbarButton.isHidden = isHidden - - for (index, vc) in titlebarAccessoryViewControllers.enumerated() { - guard vc == resetZoomTitlebarAccessoryViewController else { return } - removeTitlebarAccessoryViewController(at: index) - } - } else { - if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) { - addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController) - } - resetZoomTitlebarAccessoryViewController.view.isHidden = isHidden - } + if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) { + addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController) + } + resetZoomTitlebarAccessoryViewController.view.isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed } private func generateResetZoomButton() -> NSButton { From 12a01c046031045b315c8c20146066511fc4cf00 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 1 Jun 2025 15:14:04 -0700 Subject: [PATCH 339/642] Hide main title when covered by tabs --- .../Features/Terminal/TerminalToolbar.swift | 10 ++++++++++ .../Features/Terminal/TerminalWindow.swift | 16 +++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift index aa4ca31cd..9da14562c 100644 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ b/macos/Sources/Features/Terminal/TerminalToolbar.swift @@ -25,6 +25,16 @@ class TerminalToolbar: NSToolbar, NSToolbarDelegate { } } + var titleIsHidden: Bool { + get { + titleTextField.isHidden + } + + set { + titleTextField.isHidden = newValue + } + } + override init(identifier: NSToolbar.Identifier) { super.init(identifier: identifier) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 48384a827..9a2bdc60f 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -153,7 +153,7 @@ class TerminalWindow: NSWindow { // This is required because the removeTitlebarAccessoryViewController hook does not // catch the creation of a new window by "tearing off" a tab from a tabbed window. if let tabGroup = self.tabGroup, tabGroup.windows.count < 2 { - hideCustomTabBarViews() + resetCustomTabBarViews() } super.becomeKey() @@ -538,17 +538,22 @@ class TerminalWindow: NSWindow { let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.TabBarController super.removeTitlebarAccessoryViewController(at: index) if (isTabBar) { - hideCustomTabBarViews() + resetCustomTabBarViews() } } // To be called immediately after the tab bar is disabled. - private func hideCustomTabBarViews() { + private func resetCustomTabBarViews() { // Hide the window buttons backdrop. windowButtonsBackdrop?.isHidden = true // Hide the window drag handle. windowDragHandle?.isHidden = true + + // Reenable the main toolbar title + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleIsHidden = false + } } private func pushTabsToTitlebar(_ tabBarController: NSTitlebarAccessoryViewController) { @@ -557,6 +562,11 @@ class TerminalWindow: NSWindow { generateToolbar() } + // The main title conflicts with titlebar tabs, so hide it + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleIsHidden = true + } + // HACK: wait a tick before doing anything, to avoid edge cases during startup... :/ // If we don't do this then on launch windows with restored state with tabs will end // up with messed up tab bars that don't show all tabs. From 232a46d2dc155c5371b8265d7e66437cb480e65e Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Sun, 1 Jun 2025 14:02:09 -0700 Subject: [PATCH 340/642] Add option to hide macOS traffic lights --- .../Terminal/TerminalController.swift | 19 +++++++++++--- .../Features/Terminal/TerminalWindow.swift | 14 ++++++++++- macos/Sources/Ghostty/Ghostty.Config.swift | 11 ++++++++ macos/Sources/Ghostty/Package.swift | 6 +++++ src/config/Config.zig | 25 +++++++++++++++++++ 5 files changed, 71 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f2868adb0..78245d5a6 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -377,6 +377,14 @@ class TerminalController: BaseTerminalController { shouldCascadeWindows = false } + fileprivate func hideWindowButtons() { + guard let window else { return } + + window.standardWindowButton(.closeButton)?.isHidden = true + window.standardWindowButton(.miniaturizeButton)?.isHidden = true + window.standardWindowButton(.zoomButton)?.isHidden = true + } + fileprivate func applyHiddenTitlebarStyle() { guard let window else { return } @@ -398,9 +406,7 @@ class TerminalController: BaseTerminalController { window.titlebarAppearsTransparent = true // Hide the traffic lights (window control buttons) - window.standardWindowButton(.closeButton)?.isHidden = true - window.standardWindowButton(.miniaturizeButton)?.isHidden = true - window.standardWindowButton(.zoomButton)?.isHidden = true + hideWindowButtons() // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. window.tabbingMode = .disallowed @@ -456,6 +462,10 @@ class TerminalController: BaseTerminalController { y: config.windowPositionY, windowDecorations: config.windowDecorations) + if config.macosWindowButtons == .hidden { + hideWindowButtons() + } + // Make sure our theme is set on the window so styling is correct. if let windowTheme = config.windowTheme { window.windowTheme = .init(rawValue: windowTheme) @@ -872,17 +882,20 @@ class TerminalController: BaseTerminalController { struct DerivedConfig { let backgroundColor: Color + let macosWindowButtons: Ghostty.MacOSWindowButtons let macosTitlebarStyle: String let maximize: Bool init() { self.backgroundColor = Color(NSColor.windowBackgroundColor) + self.macosWindowButtons = .visible self.macosTitlebarStyle = "system" self.maximize = false } init(_ config: Ghostty.Config) { self.backgroundColor = config.backgroundColor + self.macosWindowButtons = config.macosWindowButtons self.macosTitlebarStyle = config.macosTitlebarStyle self.maximize = config.maximize } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 9a2bdc60f..5e90d0696 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -45,6 +45,18 @@ class TerminalWindow: NSWindow { }, ] + private var hasWindowButtons: Bool { + get { + if let close = standardWindowButton(.closeButton), + let miniaturize = standardWindowButton(.miniaturizeButton), + let zoom = standardWindowButton(.zoomButton) { + return !(close.isHidden && miniaturize.isHidden && zoom.isHidden) + } else { + return false + } + } + } + // Both of these must be true for windows without decorations to be able to // still become key/main and receive events. override var canBecomeKey: Bool { return true } @@ -613,7 +625,7 @@ class TerminalWindow: NSWindow { view.translatesAutoresizingMaskIntoConstraints = false view.leftAnchor.constraint(equalTo: toolbarView.leftAnchor).isActive = true - view.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: 78).isActive = true + view.rightAnchor.constraint(equalTo: toolbarView.leftAnchor, constant: hasWindowButtons ? 78 : 0).isActive = true view.topAnchor.constraint(equalTo: toolbarView.topAnchor).isActive = true view.heightAnchor.constraint(equalTo: toolbarView.heightAnchor).isActive = true diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index d7be4eb5b..cce14ca0f 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -250,6 +250,17 @@ extension Ghostty { return String(cString: ptr) } + var macosWindowButtons: MacOSWindowButtons { + let defaultValue = MacOSWindowButtons.visible + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "macos-window-buttons" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return MacOSWindowButtons(rawValue: str) ?? defaultValue + } + var macosTitlebarStyle: String { let defaultValue = "transparent" guard let config = self.config else { return defaultValue } diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 30d5573df..82721c17e 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -239,6 +239,12 @@ extension Ghostty { case chrome } + /// Enum for the macos-window-buttons config option + enum MacOSWindowButtons: String { + case visible + case hidden + } + /// Enum for the macos-titlebar-proxy-icon config option enum MacOSTitlebarProxyIcon: String { case visible diff --git a/src/config/Config.zig b/src/config/Config.zig index bf6d26f4b..03fc53321 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2053,6 +2053,25 @@ keybind: Keybinds = .{}, /// it will retain the previous setting until fullscreen is exited. @"macos-non-native-fullscreen": NonNativeFullscreen = .false, +/// Whether the window buttons in the macOS titlebar are visible. The window +/// buttons are the colored buttons in the upper left corner of most macOS apps, +/// also known as the traffic lights, that allow you to close, miniaturize, and +/// zoom the window. +/// +/// This setting has no effect when `window-decoration = false` or +/// `macos-titlebar-style = hidden`, as the window buttons are always hidden in +/// these modes. +/// +/// Valid values are: +/// +/// * `visible` - Show the window buttons. +/// * `hidden` - Hide the window buttons. +/// +/// The default value is `visible`. +/// +/// Changing this option at runtime only applies to new windows. +@"macos-window-buttons": MacWindowButtons = .visible, + /// The style of the macOS titlebar. Available values are: "native", /// "transparent", "tabs", and "hidden". /// @@ -5803,6 +5822,12 @@ pub const WindowColorspace = enum { @"display-p3", }; +/// See macos-window-buttons +pub const MacWindowButtons = enum { + visible, + hidden, +}; + /// See macos-titlebar-style pub const MacTitlebarStyle = enum { native, From 5244f8d6ac5161f59457a4aa83d286192e5db7a7 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Mon, 2 Jun 2025 10:14:52 -0700 Subject: [PATCH 341/642] Follow-up to #7462: var -> let --- macos/Sources/Features/Global Keybinds/GlobalEventTap.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift index 644285c9a..ae77535be 100644 --- a/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift +++ b/macos/Sources/Features/Global Keybinds/GlobalEventTap.swift @@ -141,7 +141,7 @@ fileprivate func cgEventFlagsChangedHandler( guard let event: NSEvent = .init(cgEvent: cgEvent) else { return result } // Build our event input and call ghostty - var key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) + let key_ev = event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS) if (ghostty_app_key(ghostty, key_ev)) { GlobalEventTap.logger.info("global key event handled event=\(event)") return nil From d1f1be883386fa68763fba512ce2c371afe5ea4d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Jun 2025 13:57:33 -0700 Subject: [PATCH 342/642] macos: fix small memory leak in surface tree when closing splits This fixes a small memory leak I found where the `SplitNode.Leaf` was not being deinitialized properly when closing a split. It would get deinitialized the next time a split was made or the window was closed, so the leak wasn't big. The surface view underneath the split was also properly deinitialized because we forced it, so again, the leak was quite small. But conceptually this is a big problem, because when we change the surface tree we expect the deinit chain to propagate properly through the whole thing, _including_ to the SurfaceView. This fixes that by removing the `id(node)` call. I don't find this to be necessary anymore. I don't know when that happened but we've changed quite a lot in our split system since it was introduced. I'm also not 100% sure why the `id(node)` was causing a strong reference to begin with... which bothers me a bit. AI note: While I manually hunted this down, I started up Claude Code and Codex in separate tabs to also hunt for the memory leak. They both failed to find it and offered solutions that didn't work. --- .../Terminal/BaseTerminalController.swift | 4 +--- macos/Sources/Ghostty/Ghostty.SplitNode.swift | 15 +-------------- .../Sources/Ghostty/Ghostty.TerminalSplit.swift | 5 +---- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 16 ++++------------ 4 files changed, 7 insertions(+), 33 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 9862e1288..fd5ca9ffb 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -149,10 +149,8 @@ class BaseTerminalController: NSWindowController, /// /// Subclasses should call super first. func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) { - // If our surface tree becomes nil then ensure all surfaces - // in the old tree have closed. + // If our surface tree becomes nil then we have no focused surface. if (to == nil) { - from?.close() focusedSurface = nil } } diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift index 95c019b1f..97b20acd3 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitNode.swift @@ -102,19 +102,6 @@ extension Ghostty { } } - /// Close the surface associated with this node. This will likely deinitialize the - /// surface. At this point, the surface view in this node tree can never be used again. - func close() { - switch (self) { - case .leaf(let leaf): - leaf.surface.close() - - case .split(let container): - container.topLeft.close() - container.bottomRight.close() - } - } - /// Returns true if any surface in the split stack requires quit confirmation. func needsConfirmQuit() -> Bool { switch (self) { @@ -224,7 +211,7 @@ extension Ghostty { self.app = app self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid) } - + // MARK: - Hashable func hash(into hasher: inout Hasher) { diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index 3e942d774..92528ace7 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -75,7 +75,6 @@ extension Ghostty { .onReceive(pubZoom) { onZoom(notification: $0) } } } - .id(node) // Needed for change detection on node } else { // On these events we want to reset the split state and call it. let pubSplit = center.publisher(for: Notification.ghosttyNewSplit, object: zoomedSurface!) @@ -289,7 +288,7 @@ extension Ghostty { let neighbors: SplitNode.Neighbors @Binding var node: SplitNode? - @StateObject var container: SplitNode.Container + @ObservedObject var container: SplitNode.Container var body: some View { SplitView( @@ -331,7 +330,6 @@ extension Ghostty { } // Closing - container.topLeft.close() node = container.bottomRight switch (node) { @@ -362,7 +360,6 @@ extension Ghostty { } // Closing - container.bottomRight.close() node = container.topLeft switch (node) { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 8e8838471..99f901792 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -279,22 +279,14 @@ extension Ghostty { // Remove ourselves from secure input if we have to SecureInput.shared.removeScoped(ObjectIdentifier(self)) - guard let surface = self.surface else { return } - ghostty_surface_free(surface) - } - - /// Close the surface early. This will free the associated Ghostty surface and the view will - /// no longer render. The view can never be used again. This is a way for us to free the - /// Ghostty resources while references may still be held to this view. I've found that SwiftUI - /// tends to hold this view longer than it should so we free the expensive stuff explicitly. - func close() { // Remove any notifications associated with this surface let identifiers = Array(self.notificationIdentifiers) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) - guard let surface = self.surface else { return } - ghostty_surface_free(surface) - self.surface = nil + // Free our core surface resources + if let surface = self.surface { + ghostty_surface_free(surface) + } } func focusDidChange(_ focused: Bool) { From 652f551bec02e7dd5f9856ff24dbf20fa6e088ec Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 2 Jun 2025 20:03:08 -0400 Subject: [PATCH 343/642] macos: simplify some ServiceProvider code First, remove the always-inlined openTerminalFromPasteboard code and combine it with openTerminal. Now that we're doing a bit of work inside openTerminal, there's little better to having an intermediate, inlined function. Second, combine some type-casting operations (saving a .map() call). Lastly, adjust some variable names because a generic `objs` or `urls` was a little ambiguous now that we're all in one function scope. --- .../Features/Services/ServiceProvider.swift | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index a06e7d151..043f5d704 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -5,7 +5,7 @@ class ServiceProvider: NSObject { static private let errorNoString = NSString(string: "Could not load any text from the clipboard.") /// The target for an open operation - enum OpenTarget { + private enum OpenTarget { case tab case window } @@ -15,7 +15,7 @@ class ServiceProvider: NSObject { userData: String?, error: AutoreleasingUnsafeMutablePointer ) { - openTerminalFromPasteboard(pasteboard: pasteboard, target: .tab, error: error) + openTerminal(from: pasteboard, target: .tab, error: error) } @objc func openWindow( @@ -23,40 +23,33 @@ class ServiceProvider: NSObject { userData: String?, error: AutoreleasingUnsafeMutablePointer ) { - openTerminalFromPasteboard(pasteboard: pasteboard, target: .window, error: error) + openTerminal(from: pasteboard, target: .window, error: error) } - @inline(__always) - private func openTerminalFromPasteboard( - pasteboard: NSPasteboard, + private func openTerminal( + from pasteboard: NSPasteboard, target: OpenTarget, error: AutoreleasingUnsafeMutablePointer ) { - guard let objs = pasteboard.readObjects(forClasses: [NSURL.self]) as? [NSURL] else { + guard let delegate = NSApp.delegate as? AppDelegate else { return } + let terminalManager = delegate.terminalManager + + guard let pathURLs = pasteboard.readObjects(forClasses: [NSURL.self]) as? [URL] else { error.pointee = Self.errorNoString return } - let urlObjects = objs.map { $0 as URL } - openTerminal(urlObjects, target: target) - } - - private func openTerminal(_ urls: [URL], target: OpenTarget) { - guard let delegateRaw = NSApp.delegate else { return } - guard let delegate = delegateRaw as? AppDelegate else { return } - let terminalManager = delegate.terminalManager - - let uniqueCwds: Set = Set( - urls.map { url -> URL in - // We only open in directories. + // Build a set of unique directory URLs to open. File paths are truncated + // to their directories because that's the only thing we can open. + let directoryURLs = Set( + pathURLs.map { url -> URL in url.hasDirectoryPath ? url : url.deletingLastPathComponent() } ) - for cwd in uniqueCwds { - // Build our config + for url in directoryURLs { var config = Ghostty.SurfaceConfiguration() - config.workingDirectory = cwd.path(percentEncoded: false) + config.workingDirectory = url.path(percentEncoded: false) switch (target) { case .window: From 58cece07f0e8f0fc4cf5cb42faa8b86d3cfbcf19 Mon Sep 17 00:00:00 2001 From: Leorize Date: Mon, 2 Jun 2025 20:22:41 -0500 Subject: [PATCH 344/642] gtk/GlobalShortcuts: don't request session with no shortcuts There aren't any reason to pay the D-Bus tax if you don't use global shortcuts. --- src/apprt/gtk/GlobalShortcuts.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/GlobalShortcuts.zig b/src/apprt/gtk/GlobalShortcuts.zig index 7d960d7bf..ac9dbaa8a 100644 --- a/src/apprt/gtk/GlobalShortcuts.zig +++ b/src/apprt/gtk/GlobalShortcuts.zig @@ -117,7 +117,9 @@ pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void { ); } - try self.request(.create_session); + if (self.map.count() > 0) { + try self.request(.create_session); + } } fn shortcutActivated( From 1183ac897236fef95bd56af0778aa7c1c7272bac Mon Sep 17 00:00:00 2001 From: Leorize Date: Mon, 2 Jun 2025 21:02:16 -0500 Subject: [PATCH 345/642] flatpak: rename .Devel variant to .ghostty-debug This is done to match against the default application id when Ghostty is built using debug configuration, done to prepare the Flatpak version for D-Bus activation support. --- ...ellh.ghostty.Devel.yml => com.mitchellh.ghostty-debug.yml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename flatpak/{com.mitchellh.ghostty.Devel.yml => com.mitchellh.ghostty-debug.yml} (95%) diff --git a/flatpak/com.mitchellh.ghostty.Devel.yml b/flatpak/com.mitchellh.ghostty-debug.yml similarity index 95% rename from flatpak/com.mitchellh.ghostty.Devel.yml rename to flatpak/com.mitchellh.ghostty-debug.yml index fe24a7c56..8a2c0056e 100644 --- a/flatpak/com.mitchellh.ghostty.Devel.yml +++ b/flatpak/com.mitchellh.ghostty-debug.yml @@ -1,4 +1,4 @@ -app-id: com.mitchellh.ghostty.Devel +app-id: com.mitchellh.ghostty-debug runtime: org.gnome.Platform runtime-version: "48" sdk: org.gnome.Sdk @@ -10,7 +10,7 @@ command: ghostty rename-desktop-file: com.mitchellh.ghostty.desktop rename-appdata-file: com.mitchellh.ghostty.metainfo.xml rename-icon: com.mitchellh.ghostty -desktop-file-name-suffix: " (Devel)" +desktop-file-name-suffix: " (Debug)" finish-args: # 3D rendering - --device=dri From 4e39144d39c0dbb88ebc0060dffc3b145556df3c Mon Sep 17 00:00:00 2001 From: Leorize Date: Tue, 3 Jun 2025 01:34:37 -0500 Subject: [PATCH 346/642] gtk/TabView: do not closeTab within close-page signal handler `TabView` assumes to be the sole owner of all `Tab`s within a Window. As such, it could close the managed `Window` once all tabs are removed from its widget. However, during `AdwTabView::close-page` signal triggered by libadwaita, the `Tab` to be closed will gain an another reference for the duration of the signal, breaking `TabView.closeTab` (called via `Tab.closeWithConfirmation`) assumptions that having no tabs meant they are all destroyed. This commit solves the issue by scheduling `Tab.closeWithConfirmation` to be run after `AdwTabView::close-page` signal has finished processing. --- src/apprt/gtk/TabView.zig | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/TabView.zig b/src/apprt/gtk/TabView.zig index 29a069a6d..8a4145b5f 100644 --- a/src/apprt/gtk/TabView.zig +++ b/src/apprt/gtk/TabView.zig @@ -7,6 +7,7 @@ const std = @import("std"); const gtk = @import("gtk"); const adw = @import("adw"); const gobject = @import("gobject"); +const glib = @import("glib"); const Window = @import("Window.zig"); const Tab = @import("Tab.zig"); @@ -243,7 +244,14 @@ fn adwClosePage( const child = page.getChild().as(gobject.Object); const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0)); self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close)); - if (!self.forcing_close) tab.closeWithConfirmation(); + if (!self.forcing_close) { + // We cannot trigger a close directly in here as the page will stay + // alive until this handler returns, breaking the assumption where + // no pages means they are all destroyed. + // + // Schedule the close request to happen in the next event cycle. + _ = glib.idleAddOnce(glibIdleOnceCloseTab, tab); + } return 1; } @@ -269,3 +277,8 @@ fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callcon const title = page.getTitle(); self.window.setTitle(std.mem.span(title)); } + +fn glibIdleOnceCloseTab(data: ?*anyopaque) callconv(.c) void { + const tab: *Tab = @ptrCast(@alignCast(data orelse return)); + tab.closeWithConfirmation(); +} From 037d4732a6f16c3e89b96b059f3bf83e69b82097 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Jun 2025 00:46:01 +0000 Subject: [PATCH 347/642] build(deps): bump namespacelabs/nscloud-cache-action from 1.2.7 to 1.2.8 Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.7 to 1.2.8. - [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases) - [Commits](https://github.com/namespacelabs/nscloud-cache-action/compare/v1.2.7...v1.2.8) --- updated-dependencies: - dependency-name: namespacelabs/nscloud-cache-action dependency-version: 1.2.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/nix.yml | 2 +- .github/workflows/release-tag.yml | 2 +- .github/workflows/release-tip.yml | 2 +- .github/workflows/test.yml | 38 +++++++++++------------ .github/workflows/update-colorschemes.yml | 2 +- 5 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 11521c9c6..a905531c2 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -36,7 +36,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 6190bed16..db8049df7 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -83,7 +83,7 @@ jobs: - uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index b4a341a5d..42626288c 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -107,7 +107,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1401f8325..e0b0ded6b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,7 +67,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -98,7 +98,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -134,7 +134,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -163,7 +163,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -196,7 +196,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -240,7 +240,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -382,7 +382,7 @@ jobs: mkdir dist tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -492,7 +492,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -523,7 +523,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -568,7 +568,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -607,7 +607,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -662,7 +662,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -689,7 +689,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -716,7 +716,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -743,7 +743,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -770,7 +770,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -797,7 +797,7 @@ jobs: steps: - uses: actions/checkout@v4 # Check out repo so we can lint it - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -832,7 +832,7 @@ jobs: uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix @@ -890,7 +890,7 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 27b35b441..2533285e6 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -22,7 +22,7 @@ jobs: fetch-depth: 0 - name: Setup Cache - uses: namespacelabs/nscloud-cache-action@v1.2.7 + uses: namespacelabs/nscloud-cache-action@v1.2.8 with: path: | /nix From 2c8d6ba944bfb85d32e02ded4af693fa9e8ac911 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Wed, 4 Jun 2025 15:07:58 +0200 Subject: [PATCH 348/642] core: document keybind actions better The current documentation for actions are very sparse and would leave someone (even including contributors) as to what exactly they do. On top of that there are many stylistic and grammatical problems that are simply no longer in line with our current standards, and certainly not on par with our configuration options reference. Hence, I've taken it upon myself to add, clarify, supplement, edit and even rewrite the documentation for most of these actions, in a wider effort of trying to offer better, clearer documentation for our users. --- src/input/Binding.zig | 427 ++++++++++++++++++++++++++++-------------- src/input/command.zig | 2 +- 2 files changed, 285 insertions(+), 144 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index e5d434265..7818fac1e 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -222,114 +222,191 @@ pub fn lessThan(_: void, lhs: Binding, rhs: Binding) bool { /// The set of actions that a keybinding can take. pub const Action = union(enum) { - /// Ignore this key combination, don't send it to the child process, - /// pretend that it never happened at the Ghostty level. The key - /// combination may still be processed by the OS or other - /// applications. + /// Ignore this key combination. + /// + /// Ghostty will not process this combination nor forward it to the child + /// process within the terminal, but it may still be processed by the OS or + /// other applications. ignore, - /// This action is used to flag that the binding should be removed from - /// the set. This should never exist in an active set and `set.put` has an - /// assertion to verify this. + /// Unbind a previously bound key binding. /// - /// This is only able to unbind bindings that were previously - /// bound to Ghostty. This cannot unbind bindings that were not - /// bound by Ghostty (e.g. bindings set by the OS or some other - /// application). + /// This cannot unbind bindings that were not bound by Ghostty or the user + /// (e.g. bindings set by the OS or some other application). unbind, - /// Send a CSI sequence. The value should be the CSI sequence without the - /// CSI header (`ESC [` or `\x1b[`). + /// Send a CSI sequence. + /// + /// The value should be the CSI sequence without the CSI header (`ESC [` or + /// `\x1b[`). + /// + /// For example, `csi:0m` can be sent to reset all styles of the current text. csi: []const u8, /// Send an `ESC` sequence. esc: []const u8, - /// Send the given text. Uses Zig string literal syntax. This is currently - /// not validated. If the text is invalid (i.e. contains an invalid escape - /// sequence), the error will currently only show up in logs. + /// Send the specified text. + /// + /// Uses Zig string literal syntax. This is currently not validated. + /// If the text is invalid (i.e. contains an invalid escape sequence), + /// the error will currently only show up in logs. text: []const u8, /// Send data to the pty depending on whether cursor key mode is enabled /// (`application`) or disabled (`normal`). cursor_key: CursorKey, - /// Reset the terminal. This can fix a lot of issues when a running - /// program puts the terminal into a broken state. This is equivalent to - /// when you type "reset" and press enter. + /// Reset the terminal. + /// + /// This can fix a lot of issues when a running program puts the terminal + /// into a broken state, equivalent to running the `reset` command. /// /// If you do this while in a TUI program such as vim, this may break /// the program. If you do this while in a shell, you may have to press /// enter after to get a new prompt. reset, - /// Copy and paste. + /// Copy the selected text to the clipboard. copy_to_clipboard, + + /// Paste the contents of the default clipboard. paste_from_clipboard, + + /// Paste the contents of the selection clipboard. paste_from_selection, - /// Copy the URL under the cursor to the clipboard. If there is no - /// URL under the cursor, this does nothing. + /// If there is a URL under the cursor, copy it to the default clipboard. copy_url_to_clipboard, - /// Increase/decrease the font size by a certain amount. + /// Increase the font size by the specified amount in points (pt). + /// + /// For example, `increase_font_size:1.5` will increase the font size + /// by 1.5 points. increase_font_size: f32, + + /// Decrease the font size by the specified amount in points (pt). + /// + /// For example, `decrease_font_size:1.5` will decrease the font size + /// by 1.5 points. decrease_font_size: f32, /// Reset the font size to the original configured size. reset_font_size, - /// Clear the screen. This also clears all scrollback. + /// Clear the screen and all scrollback. clear_screen, /// Select all text on the screen. select_all, - /// Scroll the screen varying amounts. + /// Scroll to the top of the screen. scroll_to_top, + + /// Scroll to the bottom of the screen. scroll_to_bottom, + + /// Scroll to the selected text. scroll_to_selection, + + /// Scroll the screen up by one page. scroll_page_up, + + /// Scroll the screen down by one page. scroll_page_down, + + /// Scroll the screen by the specified fraction of a page. + /// + /// Positive values scroll downwards, and negative values scroll upwards. + /// + /// For example, `scroll_page_fractional:0.5` would scroll the screen + /// downwards by half a page, while `scroll_page_fractional:-1.5` would + /// scroll it upwards by one and a half pages. scroll_page_fractional: f32, + + /// Scroll the screen by the specified amount of lines. + /// + /// Positive values scroll downwards, and negative values scroll upwards. + /// + /// For example, `scroll_page_lines:3` would scroll the screen downwards + /// by 3 lines, while `scroll_page_lines:-10` would scroll it upwards by 10 + /// lines. scroll_page_lines: i16, - /// Adjust the current selection in a given direction. Does nothing if no - /// selection exists. + /// Adjust the current selection in the given direction or position, + /// relative to the cursor. /// - /// Arguments: - /// - left, right, up, down, page_up, page_down, home, end, - /// beginning_of_line, end_of_line + /// WARNING: This does not create a new selection, and does nothing when + /// there currently isn't one. + /// + /// Valid arguments are: + /// + /// - `left`, `right` + /// + /// Adjust the selection one cell to the left or right respectively. + /// + /// - `up`, `down` + /// + /// Adjust the selection one line upwards or downwards respectively. + /// + /// - `page_up`, `page_down` + /// + /// Adjust the selection one page upwards or downwards respectively. + /// + /// - `home`, `end` + /// + /// Adjust the selection to the top-left or the bottom-right corner + /// of the screen respectively. + /// + /// - `beginning_of_line`, `end_of_line` + /// + /// Adjust the selection to the beginning or the end of the line + /// respectively. /// - /// Example: Extend selection to the right - /// keybind = shift+right=adjust_selection:right adjust_selection: AdjustSelection, - /// Jump the viewport forward or back by prompt. Positive number is the - /// number of prompts to jump forward, negative is backwards. + /// Jump the viewport forward or back by the given number of prompts. + /// + /// Requires shell integration. + /// + /// Positive values scroll downwards, and negative values scroll upwards. jump_to_prompt: i16, - /// Write the entire scrollback into a temporary file. The action - /// determines what to do with the filepath. Valid values are: + /// Write the entire scrollback into a temporary file with the specified + /// action. The action determines what to do with the filepath. + /// + /// Valid actions are: + /// + /// - `paste` + /// + /// Paste the file path into the terminal. + /// + /// - `open` + /// + /// Open the file in the default OS editor for text files. /// - /// - "paste": Paste the file path into the terminal. - /// - "open": Open the file in the default OS editor for text files. /// The default OS editor is determined by using `open` on macOS /// and `xdg-open` on Linux. /// write_scrollback_file: WriteScreenAction, - /// Same as write_scrollback_file but writes the full screen contents. - /// See write_scrollback_file for available values. + /// Write the contents of the screen into a temporary file with the + /// specified action. + /// + /// See `write_scrollback_file` for possible actions. write_screen_file: WriteScreenAction, - /// Same as write_scrollback_file but writes the selected text. - /// If there is no selected text this does nothing (it doesn't - /// even create an empty file). See write_scrollback_file for - /// available values. + /// Write the currently selected text into a temporary file with the + /// specified action. + /// + /// See `write_scrollback_file` for possible actions. + /// + /// Does nothing when no text is selected. write_selection_file: WriteScreenAction, - /// Open a new window. If the application isn't currently focused, + /// Open a new window. + /// + /// If the application isn't currently focused, /// this will bring it to the front. new_window, @@ -342,190 +419,246 @@ pub const Action = union(enum) { /// Go to the next tab. next_tab, - /// Go to the last tab (the one with the highest index) + /// Go to the last tab. last_tab, - /// Go to the tab with the specific number, 1-indexed. If the tab number - /// is higher than the number of tabs, this will go to the last tab. + /// Go to the tab with the specific index, starting from 1. + /// + /// If the tab number is higher than the number of tabs, + /// this will go to the last tab. goto_tab: usize, /// Moves a tab by a relative offset. - /// Adjusts the tab position based on `offset`. For example `move_tab:-1` for left, `move_tab:1` for right. - /// If the new position is out of bounds, it wraps around cyclically within the tab range. + /// + /// Positive values move the tab forwards, and negative values move it + /// backwards. If the new position is out of bounds, it is wrapped around + /// cyclically within the tab list. + /// + /// For example, `move_tab:1` moves the tab one position forwards, and if + /// it was already the last tab in the list, it wraps around and becomes + /// the first tab in the list. Likewise, `move_tab:-1` moves the tab one + /// position backwards, and if it was the first tab, then it will become + /// the last tab. move_tab: isize, /// Toggle the tab overview. - /// This only works with libadwaita version 1.4.0 or newer. + /// + /// This is only supported on Linux and when the system's libadwaita + /// version is 1.4 or newer. The current libadwaita version can be + /// found by running `ghostty +version`. toggle_tab_overview, - /// Change the title of the current focused surface via a prompt. + /// Change the title of the current focused surface via a pop-up prompt. + /// + /// This requires libadwaita 1.5 or newer on Linux. The current libadwaita + /// version can be found by running `ghostty +version`. prompt_surface_title, - /// Create a new split in the given direction. + /// Create a new split in the specified direction. /// - /// Arguments: - /// - right, down, left, up, auto (splits along the larger direction) + /// Valid arguments: + /// + /// - `right`, `down`, `left`, `up` + /// + /// Creates a new split in the corresponding direction. + /// + /// - `auto` + /// + /// Creates a new split along the larger direction. + /// For example, if the parent split is currently wider than it is tall, + /// then a left-right split would be created, and vice versa. /// - /// Example: Create split on the right - /// keybind = cmd+shift+d=new_split:right new_split: SplitDirection, - /// Focus on a split in a given direction. For example `goto_split:up`. - /// Valid values are left, right, up, down, previous and next. + /// Focus on a split either in the specified direction (`right`, `down`, + /// `left` and `up`), or in the adjacent split in the order of creation + /// (`previous` and `next`). goto_split: SplitFocusDirection, - /// zoom/unzoom the current split. + /// Zoom in or out of the current split. + /// + /// When a split is zoomed into, it will take up the entire space in + /// the current tab, hiding other splits. The tab or tab bar would also + /// reflect this by displaying an icon indicating the zoomed state. toggle_split_zoom, - /// Resize the current split in a given direction. - /// - /// Arguments: - /// - up, down, left, right - /// - the number of pixels to resize the split by - /// - /// Example: Move divider up 10 pixels - /// keybind = cmd+shift+up=resize_split:up,10 + /// Resize the current split in the specified direction and amount in + /// pixels. The two arguments should be joined with a comma (`,`), + /// like in `resize_split:up,10`. resize_split: SplitResizeParameter, - /// Equalize all splits in the current window + /// Equalize the size of all splits in the current window. equalize_splits, /// Reset the window to the default size. The "default size" is the /// size that a new window would be created with. This has no effect /// if the window is fullscreen. + /// + /// Only implemented on macOS. reset_window_size, - /// Control the terminal inspector visibility. + /// Control the visibility of the terminal inspector. /// - /// Arguments: - /// - toggle, show, hide - /// - /// Example: Toggle inspector visibility - /// keybind = cmd+i=inspector:toggle + /// Valid arguments: `toggle`, `show`, `hide`. inspector: InspectorMode, /// Show the GTK inspector. + /// + /// Has no effect on macOS. show_gtk_inspector, - /// Open the configuration file in the default OS editor. If your default OS - /// editor isn't configured then this will fail. Currently, any failures to - /// open the configuration will show up only in the logs. + /// Open the configuration file in the default OS editor. + /// + /// If your default OS editor isn't configured then this will fail. + /// Currently, any failures to open the configuration will show up only in + /// the logs. open_config, - /// Reload the configuration. The exact meaning depends on the app runtime - /// in use but this usually involves re-reading the configuration file - /// and applying any changes. Note that not all changes can be applied at - /// runtime. + /// Reload the configuration. + /// + /// The exact meaning depends on the app runtime in use, but this usually + /// involves re-reading the configuration file and applying any changes + /// Note that not all changes can be applied at runtime. reload_config, /// Close the current "surface", whether that is a window, tab, split, etc. - /// This only closes ONE surface. This will trigger close confirmation as - /// configured. + /// + /// This might trigger a close confirmation popup, depending on the value + /// of the `confirm-close-surface` configuration setting. close_surface, - /// Close the current tab, regardless of how many splits there may be. - /// This will trigger close confirmation as configured. + /// Close the current tab and all splits therein. + /// + /// This might trigger a close confirmation popup, depending on the value + /// of the `confirm-close-surface` configuration setting. close_tab, - /// Close the window, regardless of how many tabs or splits there may be. - /// This will trigger close confirmation as configured. + /// Close the current window and all tabs and splits therein. + /// + /// This might trigger a close confirmation popup, depending on the value + /// of the `confirm-close-surface` configuration setting. close_window, - /// Close all windows. This will trigger close confirmation as configured. - /// This only works for macOS currently. + /// Close all windows. + /// + /// WARNING: This action has been deprecated and has no effect on either + /// Linux or macOS. Users are instead encouraged to use `all:close_window` + /// instead. close_all_windows, - /// Toggle maximized window state. This only works on Linux. + /// Maximize or unmaximize the current window. + /// + /// This has no effect on macOS as it does not have the concept of + /// maximized windows. toggle_maximize, - /// Toggle fullscreen mode of window. + /// Fullscreen or unfullscreen the current window. toggle_fullscreen, - /// Toggle window decorations on and off. This only works on Linux. + /// Toggle window decorations (titlebar, buttons, etc.) for the current window. + /// + /// Only implemented on Linux. toggle_window_decorations, - /// Toggle whether the terminal window is always on top of other - /// windows even when it is not focused. Terminal windows always start - /// as normal (not always on top) windows. + /// Toggle whether the terminal window should always float on top of other + /// windows even when unfocused. /// - /// This only works on macOS. + /// Terminal windows always start as normal (not float-on-top) windows. + /// + /// Only implemented on macOS. toggle_window_float_on_top, - /// Toggle secure input mode on or off. This is used to prevent apps - /// that monitor input from seeing what you type. This is useful for - /// entering passwords or other sensitive information. + /// Toggle secure input mode. /// - /// This applies to the entire application, not just the focused - /// terminal. You must toggle it off to disable it, or quit Ghostty. + /// This is used to prevent apps from monitoring your keyboard input + /// when entering passwords or other sensitive information. /// - /// This only works on macOS, since this is a system API on macOS. + /// This applies to the entire application, not just the focused terminal. + /// You must manually untoggle it or quit Ghostty entirely to disable it. + /// + /// Only implemented on macOS, as this uses a built-in system API. toggle_secure_input, - /// Toggle the command palette. The command palette is a UI element - /// that lets you see what actions you can perform, their associated - /// keybindings (if any), a search bar to filter the actions, and - /// the ability to then execute the action. + /// Toggle the command palette. + /// + /// The command palette is a popup that lets you see what actions + /// you can perform, their associated keybindings (if any), a search bar + /// to filter the actions, and the ability to then execute the action. + /// + /// This requires libadwaita 1.5 or newer on Linux. The current libadwaita + /// version can be found by running `ghostty +version`. toggle_command_palette, - /// Toggle the "quick" terminal. The quick terminal is a terminal that - /// appears on demand from a keybinding, often sliding in from a screen - /// edge such as the top. This is useful for quick access to a terminal - /// without having to open a new window or tab. + /// Toggle the quick terminal. /// - /// When the quick terminal loses focus, it disappears. The terminal state - /// is preserved between appearances, so you can always press the keybinding - /// to bring it back up. + /// The quick terminal, also known as the "Quake-style" or drop-down + /// terminal, is a terminal window that appears on demand from a keybinding, + /// often sliding in from a screen edge such as the top. This is useful for + /// quick access to a terminal without having to open a new window or tab. /// - /// To enable the quick terminal globally so that Ghostty doesn't - /// have to be focused, prefix your keybind with `global`. Example: + /// The terminal state is preserved between appearances, so showing the + /// quick terminal after it was already hidden would display the same + /// window instead of creating a new one. + /// + /// As quick terminals are often useful when other windows are currently + /// focused, they are best used with *global* keybinds. For example, one + /// can define the following key bind to toggle the quick terminal from + /// anywhere within the system by pressing `` Cmd+` ``: /// /// ```ini - /// keybind = global:cmd+grave_accent=toggle_quick_terminal + /// keybind = global:cmd+backquote=toggle_quick_terminal /// ``` /// /// The quick terminal has some limitations: /// - /// - It is a singleton; only one instance can exist at a time. - /// - It does not support tabs, but it does support splits. - /// - It will not be restored when the application is restarted - /// (for systems that support window restoration). - /// - It supports fullscreen, but fullscreen will always be a non-native - /// fullscreen (macos-non-native-fullscreen = true). This only applies - /// to the quick terminal window. This is a requirement due to how - /// the quick terminal is rendered. + /// - Only one quick terminal instance can exist at a time. + /// + /// - Unlike normal terminal windows, the quick terminal will not be + /// restored when the application is restarted on systems that support + /// window restoration like macOS. + /// + /// - On Linux, the quick terminal is only supported on Wayland and not + /// X11, and only on Wayland compositors that support the `wlr-layer-shell-v1` + /// protocol. In practice, this means that only GNOME users would not be + /// able to use this feature. + /// + /// - On Linux, slide-in animations are only supported on KDE, and when + /// the "Sliding Popups" KWin plugin is enabled. + /// + /// If you do not have this plugin enabled, open System Settings > Apps + /// & Windows > Window Management > Desktop Effects, and enable the + /// plugin in the plugin list. Ghostty would then need to be restarted + /// fully for this to take effect. + /// + /// - Quick terminal tabs are only supported on Linux and not on macOS. + /// This is because tabs on macOS require a title bar. + /// + /// - On macOS, a fullscreened quick terminal will always be in non-native + /// fullscreen mode. This is a requirement due to how the quick terminal + /// is rendered. /// /// See the various configurations for the quick terminal in the /// configuration file to customize its behavior. - /// - /// Supported on macOS and some desktop environments on Linux, namely - /// those that support the `wlr-layer-shell` Wayland protocol - /// (i.e. most desktop environments and window managers except GNOME). - /// - /// Slide-in animations on Linux are only supported on KDE when the - /// "Sliding Popups" KWin plugin is enabled. If you do not have this - /// plugin enabled, open System Settings > Apps & Windows > Window - /// Management > Desktop Effects, and enable the plugin in the plugin list. - /// Ghostty would then need to be restarted for this to take effect. toggle_quick_terminal, - /// Show/hide all windows. If all windows become shown, we also ensure + /// Show or hide all windows. If all windows become shown, we also ensure /// Ghostty becomes focused. When hiding all windows, focus is yielded /// to the next application as determined by the OS. /// /// Note: When the focused surface is fullscreen, this method does nothing. /// - /// This currently only works on macOS. + /// Only implemented on macOS. toggle_visibility, /// Check for updates. /// - /// This currently only works on macOS. + /// Only implemented on macOS. check_for_updates, - /// Quit ghostty. + /// Quit Ghostty. quit, - /// Crash ghostty in the desired thread for the focused surface. + /// Crash Ghostty in the desired thread for the focused surface. /// /// WARNING: This is a hard crash (panic) and data can be lost. /// @@ -535,9 +668,17 @@ pub const Action = union(enum) { /// /// The value determines the crash location: /// - /// - "main" - crash on the main (GUI) thread. - /// - "io" - crash on the IO thread for the focused surface. - /// - "render" - crash on the render thread for the focused surface. + /// - `main` + /// + /// Crash on the main (GUI) thread. + /// + /// - `io` + /// + /// Crash on the IO thread for the focused surface. + /// + /// - `render` + /// + /// Crash on the render thread for the focused surface. /// crash: CrashThread, diff --git a/src/input/command.zig b/src/input/command.zig index 53d1b6b3d..1ce6aa7cb 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -119,7 +119,7 @@ fn actionCommands(action: Action.Key) []const Command { .paste_from_clipboard => comptime &.{.{ .action = .paste_from_clipboard, .title = "Paste from Clipboard", - .description = "Paste the contents of the clipboard.", + .description = "Paste the contents of the main clipboard.", }}, .paste_from_selection => comptime &.{.{ From 77479feee6aefd039254b071613416a4cfd448e8 Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Tue, 3 Jun 2025 12:18:13 +0200 Subject: [PATCH 349/642] gtk: make requesting attention configurable --- src/apprt/gtk/Surface.zig | 12 +++++++----- src/config/Config.zig | 34 ++++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index e51109015..1e5b1bfe8 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2454,6 +2454,13 @@ pub fn ringBell(self: *Surface) !void { media_stream.play(); } + if (features.attention) { + // Request user attention + window.winproto.setUrgent(true) catch |err| { + log.err("failed to request user attention={}", .{err}); + }; + } + // Mark tab as needing attention if (self.container.tab()) |tab| tab: { const page = window.notebook.getTabPage(tab) orelse break :tab; @@ -2461,11 +2468,6 @@ pub fn ringBell(self: *Surface) !void { // Need attention if we're not the currently selected tab if (page.getSelected() == 0) page.setNeedsAttention(@intFromBool(true)); } - - // Request user attention - window.winproto.setUrgent(true) catch |err| { - log.err("failed to request user attention={}", .{err}); - }; } /// Handle a stream that is in an error state. diff --git a/src/config/Config.zig b/src/config/Config.zig index 344c118d7..094058c5d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1963,7 +1963,7 @@ keybind: Keybinds = .{}, /// /// * `system` /// -/// Instructs the system to notify the user using built-in system functions. +/// Instruct the system to notify the user using built-in system functions. /// This could result in an audiovisual effect, a notification, or something /// else entirely. Changing these effects require altering system settings: /// for instance under the "Sound > Alert Sound" setting in GNOME, @@ -1973,15 +1973,31 @@ keybind: Keybinds = .{}, /// /// Play a custom sound. (GTK only) /// -/// Example: `audio`, `no-audio`, `system`, `no-system`: +/// * `attention` *(enabled by default)* /// -/// On macOS, if the app is unfocused, it will bounce the app icon in the dock -/// once. Additionally, the title of the window with the alerted terminal -/// surface will contain a bell emoji (🔔) until the terminal is focused -/// or a key is pressed. These are not currently configurable since they're -/// considered unobtrusive. +/// Request the user's attention when Ghostty is unfocused, until it has +/// received focus again. On macOS, this will bounce the app icon in the +/// dock once. On Linux, the behavior depends on the desktop environment +/// and/or the window manager/compositor: /// -/// By default, no bell features are enabled. +/// - On KDE, the background of the desktop icon in the task bar would be +/// highlighted; +/// +/// - On GNOME, you may receive a notification that, when clicked, would +/// bring the Ghostty window into focus; +/// +/// - On Sway, the window may be decorated with a distinctly colored border; +/// +/// - On other systems this may have no effect at all. +/// +/// * `title` *(enabled by default)* +/// +/// Prepend a bell emoji (🔔) to the title of the alerted surface until the +/// terminal is re-focused or interacted with (such as on keyboard input). +/// +/// Only implemented on macOS. +/// +/// Example: `audio`, `no-audio`, `system`, `no-system` @"bell-features": BellFeatures = .{}, /// If `audio` is an enabled bell feature, this is a path to an audio file. If @@ -5857,6 +5873,8 @@ pub const AppNotifications = packed struct { pub const BellFeatures = packed struct { system: bool = false, audio: bool = false, + attention: bool = true, + title: bool = true, }; /// See mouse-shift-capture From 170715944185885ea404ed3dc4c12513f9a4678e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 2 Jun 2025 16:56:57 -0700 Subject: [PATCH 350/642] new SplitTree --- macos/Ghostty.xcodeproj/project.pbxproj | 16 + macos/Sources/Features/Splits/SplitTree.swift | 314 ++++++++++++++++++ .../Splits/TerminalSplitTreeView.swift | 62 ++++ .../Terminal/BaseTerminalController.swift | 70 +++- .../Terminal/TerminalController.swift | 3 + .../Features/Terminal/TerminalView.swift | 10 +- .../Features/Terminal/TerminalWindow.swift | 2 + 7 files changed, 468 insertions(+), 9 deletions(-) create mode 100644 macos/Sources/Features/Splits/SplitTree.swift create mode 100644 macos/Sources/Features/Splits/TerminalSplitTreeView.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a34c4685f..459b2b994 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -59,6 +59,8 @@ A571AB1D2A206FCF00248498 /* GhosttyKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5D495A1299BEC7E00DD1313 /* GhosttyKit.xcframework */; }; A57D79272C9C879B001D522E /* SecureInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = A57D79262C9C8798001D522E /* SecureInput.swift */; }; A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; }; + A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; }; + A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */; }; A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; }; A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; @@ -164,6 +166,8 @@ A571AB1C2A206FC600248498 /* Ghostty-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Ghostty-Info.plist"; sourceTree = ""; }; A57D79262C9C8798001D522E /* SecureInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureInput.swift; sourceTree = ""; }; A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = ""; }; + A586365E2DEE6C2100E04A10 /* SplitTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitTree.swift; sourceTree = ""; }; + A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSplitTreeView.swift; sourceTree = ""; }; A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = ""; }; A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -275,6 +279,7 @@ A5CBD05A2CA0C5910017A1AE /* QuickTerminal */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A57D79252C9C8782001D522E /* Secure Input */, + A58636622DEF955100E04A10 /* Splits */, A53A29742DB2E04900B6E02C /* Command Palette */, A534263E2A7DCC5800EBB7A2 /* Settings */, A51BFC1C2B2FB5AB00E92F16 /* About */, @@ -428,6 +433,15 @@ path = "Secure Input"; sourceTree = ""; }; + A58636622DEF955100E04A10 /* Splits */ = { + isa = PBXGroup; + children = ( + A586365E2DEE6C2100E04A10 /* SplitTree.swift */, + A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */, + ); + path = Splits; + sourceTree = ""; + }; A5874D9B2DAD781100E83852 /* Private */ = { isa = PBXGroup; children = ( @@ -685,6 +699,7 @@ A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, + A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, @@ -734,6 +749,7 @@ A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */, A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */, A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, + A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */, A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */, AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */, C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */, diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift new file mode 100644 index 000000000..6f98dfefc --- /dev/null +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -0,0 +1,314 @@ +import AppKit + +/// SplitTree represents a tree of views that can be divided. +struct SplitTree { + /// The root of the tree. This can be nil to indicate the tree is empty. + let root: Node? + + /// The node that is currently zoomed. A zoomed split is expected to take up the full + /// size of the view area where the splits are shown. + let zoomed: Node? + + /// A single node in the tree is either a leaf node (a view) or a split (has a + /// left/right or top/bottom). + indirect enum Node { + case leaf(view: NSView) + case split(Split) + + struct Split: Equatable { + let direction: Direction + let ratio: Double + let left: Node + let right: Node + } + } + + enum Direction { + case horizontal // Splits are laid out left and right + case vertical // Splits are laid out top and bottom + } + + /// The path to a specific node in the tree. + struct Path { + let path: [Component] + + var isEmpty: Bool { path.isEmpty } + + enum Component { + case left + case right + } + } + + enum SplitError: Error { + case viewNotFound + } + + enum NewDirection { + case left + case right + case down + case up + } +} + +// MARK: SplitTree + +extension SplitTree { + var isEmpty: Bool { + root == nil + } + + init() { + self.init(root: nil, zoomed: nil) + } + + init(view: NSView) { + self.init(root: .leaf(view: view), zoomed: nil) + } + + /// Insert a new view at the given view point by creating a split in the given direction. + func insert(view: NSView, at: NSView, direction: NewDirection) throws -> Self { + guard let root else { throw SplitError.viewNotFound } + return .init( + root: try root.insert(view: view, at: at, direction: direction), + zoomed: zoomed) + } + + /// Remove a node from the tree. If the node being removed is part of a split, + /// the sibling node takes the place of the parent split. + func remove(_ target: Node) -> Self { + guard let root else { return self } + + // If we're removing the root itself, return an empty tree + if root == target { + return .init(root: nil, zoomed: nil) + } + + // Otherwise, try to remove from the tree + let newRoot = root.remove(target) + + // Update zoomed if it was the removed node + let newZoomed = (zoomed == target) ? nil : zoomed + + return .init(root: newRoot, zoomed: newZoomed) + } +} + +// MARK: SplitTree.Node + +extension SplitTree.Node { + typealias Node = SplitTree.Node + typealias NewDirection = SplitTree.NewDirection + typealias SplitError = SplitTree.SplitError + typealias Path = SplitTree.Path + + /// Returns the node in the tree that contains the given view. + func node(view: NSView) -> Node? { + switch (self) { + case .leaf(view): + return self + + case .split(let split): + if let result = split.left.node(view: view) { + return result + } else if let result = split.right.node(view: view) { + return result + } + + return nil + + default: + return nil + } + } + + /// Returns the path to a given node in the tree. If the returned value is nil then the + /// node doesn't exist. + func path(to node: Self) -> Path? { + var components: [Path.Component] = [] + func search(_ current: Self) -> Bool { + if current == node { + return true + } + + switch current { + case .leaf: + return false + + case .split(let split): + // Try left branch + components.append(.left) + if search(split.left) { + return true + } + components.removeLast() + + // Try right branch + components.append(.right) + if search(split.right) { + return true + } + components.removeLast() + + return false + } + } + + return search(self) ? Path(path: components) : nil + } + + /// Inserts a new view into the split tree by creating a split at the location of an existing view. + /// + /// This method creates a new split node containing both the existing view and the new view, + /// The position of the new view relative to the existing view is determined by the direction parameter. + /// + /// - Parameters: + /// - view: The new view to insert into the tree + /// - at: The existing view at whose location the split should be created + /// - direction: The direction relative to the existing view where the new view should be placed + /// + /// - Note: If the existing view (`at`) is not found in the tree, this method does nothing. We should + /// maybe throw instead but at the moment we just do nothing. + func insert(view: NSView, at: NSView, direction: NewDirection) throws -> Self { + // Get the path to our insertion point. If it doesn't exist we do + // nothing. + guard let path = path(to: .leaf(view: at)) else { + throw SplitError.viewNotFound + } + + // Determine split direction and which side the new view goes on + let splitDirection: SplitTree.Direction + let newViewOnLeft: Bool + switch direction { + case .left: + splitDirection = .horizontal + newViewOnLeft = true + case .right: + splitDirection = .horizontal + newViewOnLeft = false + case .up: + splitDirection = .vertical + newViewOnLeft = true + case .down: + splitDirection = .vertical + newViewOnLeft = false + } + + // Create the new split node + let newNode: Node = .leaf(view: view) + let existingNode: Node = .leaf(view: at) + let newSplit: Node = .split(.init( + direction: splitDirection, + ratio: 0.5, + left: newViewOnLeft ? newNode : existingNode, + right: newViewOnLeft ? existingNode : newNode + )) + + // Replace the node at the path with the new split + return try replaceNode(at: path, with: newSplit) + } + + /// Helper function to replace a node at the given path from the root + private func replaceNode(at path: Path, with newNode: Self) throws -> Self { + // If path is empty, replace the root + if path.isEmpty { + return newNode + } + + // Otherwise, we need to replace the proper left/right all along + // the way since Node is a value type (enum). To do that, we need + // recursion. We can't use a simple iterative approach because we + // can't update in-place. + func replaceInner(current: Node, pathOffset: Int) throws -> Node { + // Base case: if we've consumed the entire path, replace this node + if pathOffset >= path.path.count { + return newNode + } + + // We need to go deeper, so current must be a split for the path + // to be valid. Otherwise, the path is invalid. + guard case .split(let split) = current else { + throw SplitError.viewNotFound + } + + let component = path.path[pathOffset] + switch component { + case .left: + return .split(.init( + direction: split.direction, + ratio: split.ratio, + left: try replaceInner(current: split.left, pathOffset: pathOffset + 1), + right: split.right + )) + case .right: + return .split(.init( + direction: split.direction, + ratio: split.ratio, + left: split.left, + right: try replaceInner(current: split.right, pathOffset: pathOffset + 1) + )) + } + } + + return try replaceInner(current: self, pathOffset: 0) + } + + /// Remove a node from the tree. Returns the modified tree, or nil if removing + /// the node results in an empty tree. + func remove(_ target: Node) -> Node? { + // If we're removing ourselves, return nil + if self == target { + return nil + } + + switch self { + case .leaf: + // A leaf that isn't the target stays as is + return self + + case .split(let split): + // Neither child is directly the target, so we need to recursively + // try to remove from both children + let newLeft = split.left.remove(target) + let newRight = split.right.remove(target) + + // If both are nil then we remove everything. This shouldn't ever + // happen because duplicate nodes shouldn't exist, but we want to + // be robust against it. + if newLeft == nil && newRight == nil { + return nil + } else if newLeft == nil { + return newRight + } else if newRight == nil { + return newLeft + } + + // Both children still exist after removal + return .split(.init( + direction: split.direction, + ratio: split.ratio, + left: newLeft!, + right: newRight! + )) + } + } +} + +// MARK: SplitTree.Node Protocols + +extension SplitTree.Node: Equatable { + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.leaf(leftView), .leaf(rightView)): + // Compare NSView instances by object identity + return leftView === rightView + + case let (.split(split1), .split(split2)): + return split1 == split2 + + default: + return false + } + } +} diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift new file mode 100644 index 000000000..c55192e44 --- /dev/null +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -0,0 +1,62 @@ +import SwiftUI + +struct TerminalSplitTreeView: View { + let tree: SplitTree + + var body: some View { + if let node = tree.root { + TerminalSplitSubtreeView(node: node, isRoot: true) + } + } +} + +struct TerminalSplitSubtreeView: View { + let node: SplitTree.Node + var isRoot: Bool = false + + var body: some View { + switch (node) { + case .leaf(let leafView): + // TODO: Fix the as! + Ghostty.InspectableSurface( + surfaceView: leafView as! Ghostty.SurfaceView, + isSplit: !isRoot) + + case .split(let split): + TerminalSplitSplitView(split: split) + } + } +} + +struct TerminalSplitSplitView: View { + @EnvironmentObject var ghostty: Ghostty.App + + let split: SplitTree.Node.Split + + private var splitViewDirection: SplitViewDirection { + switch (split.direction) { + case .horizontal: .horizontal + case .vertical: .vertical + } + } + + var body: some View { + SplitView( + splitViewDirection, + .init(get: { + CGFloat(split.ratio) + }, set: { _ in + // TODO + }), + dividerColor: ghostty.config.splitDividerColor, + resizeIncrements: .init(width: 1, height: 1), + resizePublisher: .init(), + left: { + TerminalSplitSubtreeView(node: split.left) + }, + right: { + TerminalSplitSubtreeView(node: split.right) + } + ) + } +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index fd5ca9ffb..628c0acbf 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -46,6 +46,8 @@ class BaseTerminalController: NSWindowController, didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } } + @Published var surfaceTree2: SplitTree = .init() + /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false @@ -97,6 +99,9 @@ class BaseTerminalController: NSWindowController, guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base)) + let firstView = Ghostty.SurfaceView(ghostty_app, baseConfig: base) + self.surfaceTree2 = .init(view: firstView) + // Setup our notifications for behaviors let center = NotificationCenter.default center.addObserver( @@ -124,6 +129,18 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyMaximizeDidToggle(_:)), name: .ghosttyMaximizeDidToggle, object: nil) + + // Splits + center.addObserver( + self, + selector: #selector(ghosttyDidCloseSurface(_:)), + name: Ghostty.Notification.ghosttyCloseSurface, + object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidNewSplit(_:)), + name: Ghostty.Notification.ghosttyNewSplit, + object: nil) center.addObserver( self, selector: #selector(ghosttyDidEqualizeSplits(_:)), @@ -252,7 +269,58 @@ class BaseTerminalController: NSWindowController, guard surfaceTree?.contains(view: surfaceView) ?? false else { return } window.zoom(nil) } - + + @objc private func ghosttyDidCloseSurface(_ notification: Notification) { + // The target must be within our tree + guard let oldView = notification.object as? Ghostty.SurfaceView else { return } + guard let node = surfaceTree2.root?.node(view: oldView) else { return } + + // Remove it + surfaceTree2 = surfaceTree2.remove(node) + + // TODO: fix focus + } + + @objc private func ghosttyDidNewSplit(_ notification: Notification) { + // The target must be within our tree + guard let oldView = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree2.root?.node(view: oldView) != nil else { return } + + // Notification must contain our base config + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] + let config = configAny as? Ghostty.SurfaceConfiguration + + // Determine our desired direction + guard let directionAny = notification.userInfo?["direction"] else { return } + guard let direction = directionAny as? ghostty_action_split_direction_e else { return } + let splitDirection: SplitTree.NewDirection + switch (direction) { + case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .right + case GHOSTTY_SPLIT_DIRECTION_LEFT: splitDirection = .left + case GHOSTTY_SPLIT_DIRECTION_DOWN: splitDirection = .down + case GHOSTTY_SPLIT_DIRECTION_UP: splitDirection = .up + default: return + } + + // Create a new surface view + guard let ghostty_app = ghostty.app else { return } + let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config) + + // Do the split + do { + surfaceTree2 = try surfaceTree2.insert(view: newView, at: oldView, direction: splitDirection) + } catch { + // If splitting fails for any reason (it should not), then we just log + // and return. The new view we created will be deinitialized and its + // no big deal. + // TODO: log + return + } + + // Once we've split, we need to move focus to the new split + Ghostty.moveFocus(to: newView, from: oldView) + } + @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f2868adb0..d8f42bb1a 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -228,6 +228,9 @@ class TerminalController: BaseTerminalController { // Update our window light/darkness based on our updated background color window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor + // Sync our zoom state for splits + window.surfaceIsZoomed = surfaceTree2.zoomed != nil + // If our window is not visible, then we do nothing. Some things such as blurring // have no effect if the window is not visible. Ultimately, we'll have this called // at some point when a surface becomes focused. diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 7caceb071..d2f4d8bdb 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -31,7 +31,7 @@ protocol TerminalViewDelegate: AnyObject { protocol TerminalViewModel: ObservableObject { /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView /// and children. This should be @Published. - var surfaceTree: Ghostty.SplitNode? { get set } + var surfaceTree2: SplitTree { get set } /// The command palette state. var commandPaletteIsShowing: Bool { get set } @@ -81,7 +81,7 @@ struct TerminalView: View { DebugBuildWarningView() } - Ghostty.TerminalSplit(node: $viewModel.surfaceTree) + TerminalSplitTreeView(tree: viewModel.surfaceTree2) .environmentObject(ghostty) .focused($focused) .onAppear { self.focused = true } @@ -100,12 +100,6 @@ struct TerminalView: View { guard let size = newValue else { return } self.delegate?.cellSizeDidChange(to: size) } - .onChange(of: viewModel.surfaceTree?.hashValue) { _ in - // This is funky, but its the best way I could think of to detect - // ANY CHANGE within the deeply nested surface tree -- detecting a change - // in the hash value. - self.delegate?.surfaceTreeDidChange() - } .onChange(of: zoomedSplit) { newValue in self.delegate?.zoomStateDidChange(to: newValue ?? false) } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 9a2bdc60f..f244b95ee 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -30,6 +30,7 @@ class TerminalWindow: NSWindow { observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in guard let tabGroup = self?.tabGroup else { return } + Ghostty.logger.warning("WOW \(window.surfaceIsZoomed)") self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed self?.updateResetZoomTitlebarButtonVisibility() }, @@ -375,6 +376,7 @@ class TerminalWindow: NSWindow { if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) { addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController) } + resetZoomTitlebarAccessoryViewController.view.isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed } From e3bc3422dce86b58834e83e181b6640451eee4c3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2025 15:36:40 -0700 Subject: [PATCH 351/642] macos: handle split resizing --- macos/Sources/Features/Splits/SplitTree.swift | 40 +++++++++++- .../Splits/TerminalSplitTreeView.swift | 62 ++++++++----------- .../Terminal/BaseTerminalController.swift | 15 +++-- .../Terminal/TerminalController.swift | 10 ++- .../Features/Terminal/TerminalView.swift | 11 ++-- .../Features/Terminal/TerminalWindow.swift | 1 - 6 files changed, 86 insertions(+), 53 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 6f98dfefc..6829a742e 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -93,6 +93,24 @@ extension SplitTree { return .init(root: newRoot, zoomed: newZoomed) } + + /// Replace a node in the tree with a new node. + func replace(node: Node, with newNode: Node) throws -> Self { + guard let root else { throw SplitError.viewNotFound } + + // Get the path to the node we want to replace + guard let path = root.path(to: node) else { + throw SplitError.viewNotFound + } + + // Replace the node + let newRoot = try root.replaceNode(at: path, with: newNode) + + // Update zoomed if it was the replaced node + let newZoomed = (zoomed == node) ? newNode : zoomed + + return .init(root: newRoot, zoomed: newZoomed) + } } // MARK: SplitTree.Node @@ -210,7 +228,7 @@ extension SplitTree.Node { } /// Helper function to replace a node at the given path from the root - private func replaceNode(at path: Path, with newNode: Self) throws -> Self { + func replaceNode(at path: Path, with newNode: Self) throws -> Self { // If path is empty, replace the root if path.isEmpty { return newNode @@ -293,6 +311,26 @@ extension SplitTree.Node { )) } } + + /// Resize a split node to the specified ratio. + /// For leaf nodes, this returns the node unchanged. + /// For split nodes, this creates a new split with the updated ratio. + func resize(to ratio: Double) -> Self { + switch self { + case .leaf: + // Leaf nodes don't have a ratio to resize + return self + + case .split(let split): + // Create a new split with the updated ratio + return .split(.init( + direction: split.direction, + ratio: ratio, + left: split.left, + right: split.right + )) + } + } } // MARK: SplitTree.Node Protocols diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index c55192e44..8f78dcbf8 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -2,17 +2,21 @@ import SwiftUI struct TerminalSplitTreeView: View { let tree: SplitTree + let onResize: (SplitTree.Node, Double) -> Void var body: some View { if let node = tree.root { - TerminalSplitSubtreeView(node: node, isRoot: true) + TerminalSplitSubtreeView(node: node, isRoot: true, onResize: onResize) } } } struct TerminalSplitSubtreeView: View { + @EnvironmentObject var ghostty: Ghostty.App + let node: SplitTree.Node var isRoot: Bool = false + let onResize: (SplitTree.Node, Double) -> Void var body: some View { switch (node) { @@ -23,40 +27,28 @@ struct TerminalSplitSubtreeView: View { isSplit: !isRoot) case .split(let split): - TerminalSplitSplitView(split: split) - } - } -} - -struct TerminalSplitSplitView: View { - @EnvironmentObject var ghostty: Ghostty.App - - let split: SplitTree.Node.Split - - private var splitViewDirection: SplitViewDirection { - switch (split.direction) { - case .horizontal: .horizontal - case .vertical: .vertical - } - } - - var body: some View { - SplitView( - splitViewDirection, - .init(get: { - CGFloat(split.ratio) - }, set: { _ in - // TODO - }), - dividerColor: ghostty.config.splitDividerColor, - resizeIncrements: .init(width: 1, height: 1), - resizePublisher: .init(), - left: { - TerminalSplitSubtreeView(node: split.left) - }, - right: { - TerminalSplitSubtreeView(node: split.right) + let splitViewDirection: SplitViewDirection = switch (split.direction) { + case .horizontal: .horizontal + case .vertical: .vertical } - ) + + SplitView( + splitViewDirection, + .init(get: { + CGFloat(split.ratio) + }, set: { + onResize(node, $0) + }), + dividerColor: ghostty.config.splitDividerColor, + resizeIncrements: .init(width: 1, height: 1), + resizePublisher: .init(), + left: { + TerminalSplitSubtreeView(node: split.left, onResize: onResize) + }, + right: { + TerminalSplitSubtreeView(node: split.right, onResize: onResize) + } + ) + } } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 628c0acbf..cb5a15f1b 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -366,11 +366,6 @@ class BaseTerminalController: NSWindowController, // MARK: TerminalViewDelegate - // Note: this is different from surfaceDidTreeChange(from:,to:) because this is called - // when the currently set value changed in place and the from:to: variant is called - // when the variable was set. - func surfaceTreeDidChange() {} - func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { let lastFocusedSurface = focusedSurface focusedSurface = to @@ -420,6 +415,16 @@ class BaseTerminalController: NSWindowController, func zoomStateDidChange(to: Bool) {} + func splitDidResize(node: SplitTree.Node, to newRatio: Double) { + let resizedNode = node.resize(to: newRatio) + do { + surfaceTree2 = try surfaceTree2.replace(node: node, with: resizedNode) + } catch { + // TODO: log + return + } + } + func performAction(_ action: String, on surfaceView: Ghostty.SurfaceView) { guard let surface = surfaceView.surface else { return } let len = action.utf8CString.count diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d8f42bb1a..82491e76d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -107,6 +107,10 @@ class TerminalController: BaseTerminalController { override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) { super.surfaceTreeDidChange(from: from, to: to) + + // Whenever our surface tree changes in any way (new split, close split, etc.) + // we want to invalidate our state. + invalidateRestorableState() // If our surface tree is now nil then we close our window. if (to == nil) { @@ -696,12 +700,6 @@ class TerminalController: BaseTerminalController { } } - override func surfaceTreeDidChange() { - // Whenever our surface tree changes in any way (new split, close split, etc.) - // we want to invalidate our state. - invalidateRestorableState() - } - override func zoomStateDidChange(to: Bool) { guard let window = window as? TerminalWindow else { return } window.surfaceIsZoomed = to diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d2f4d8bdb..2970f19c6 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -14,15 +14,14 @@ protocol TerminalViewDelegate: AnyObject { /// The cell size changed. func cellSizeDidChange(to: NSSize) - /// The surface tree did change in some way, i.e. a split was added, removed, etc. This is - /// not called initially. - func surfaceTreeDidChange() - /// This is called when a split is zoomed. func zoomStateDidChange(to: Bool) /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) + + /// A split is resizing to a given value. + func splitDidResize(node: SplitTree.Node, to newRatio: Double) } /// The view model is a required implementation for TerminalView callers. This contains @@ -81,7 +80,9 @@ struct TerminalView: View { DebugBuildWarningView() } - TerminalSplitTreeView(tree: viewModel.surfaceTree2) + TerminalSplitTreeView( + tree: viewModel.surfaceTree2, + onResize: { delegate?.splitDidResize(node: $0, to: $1) }) .environmentObject(ghostty) .focused($focused) .onAppear { self.focused = true } diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index f244b95ee..1c440be33 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -30,7 +30,6 @@ class TerminalWindow: NSWindow { observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in guard let tabGroup = self?.tabGroup else { return } - Ghostty.logger.warning("WOW \(window.surfaceIsZoomed)") self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed self?.updateResetZoomTitlebarButtonVisibility() }, From 672d276276931400cc3d1ba46476c66de8ff33ce Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2025 15:52:50 -0700 Subject: [PATCH 352/642] macos: confirm close on split close --- .../Terminal/BaseTerminalController.swift | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index cb5a15f1b..b30cc7afb 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -272,13 +272,55 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidCloseSurface(_ notification: Notification) { // The target must be within our tree - guard let oldView = notification.object as? Ghostty.SurfaceView else { return } - guard let node = surfaceTree2.root?.node(view: oldView) else { return } - - // Remove it - surfaceTree2 = surfaceTree2.remove(node) + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard let node = surfaceTree2.root?.node(view: target) else { return } // TODO: fix focus + + var processAlive = false + if let valueAny = notification.userInfo?["process_alive"] { + if let value = valueAny as? Bool { + processAlive = value + } + } + + // If the child process is not alive, then we exit immediately + guard processAlive else { + surfaceTree2 = surfaceTree2.remove(node) + return + } + + // If we don't have a window to attach our modal to, we also exit immediately. + // This should NOT happen. + guard let window = target.window else { + surfaceTree2 = surfaceTree2.remove(node) + return + } + + // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog + // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that + // confirmationDialog allows the user to Cmd-W close the alert, but when doing + // so SwiftUI does not update any of the bindings to note that window is no longer + // being shown, and provides no callback to detect this. + let alert = NSAlert() + alert.messageText = "Close Terminal?" + alert.informativeText = "The terminal still has a running process. If you close the " + + "terminal the process will be killed." + alert.addButton(withTitle: "Close the Terminal") + alert.addButton(withTitle: "Cancel") + alert.alertStyle = .warning + alert.beginSheetModal(for: window, completionHandler: { [weak self] response in + switch (response) { + case .alertFirstButtonReturn: + alert.window.orderOut(nil) + if let self { + self.surfaceTree2 = self.surfaceTree2.remove(node) + } + + default: + break + } + }) } @objc private func ghosttyDidNewSplit(_ notification: Notification) { From 33d94521ea77efb42b39ae8037a23b8dcfec0952 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2025 15:57:01 -0700 Subject: [PATCH 353/642] macos: setup sequence for SplitTree --- macos/Sources/Features/Splits/SplitTree.swift | 27 ++++++++++ .../Terminal/BaseTerminalController.swift | 52 +++++++++---------- macos/Sources/Ghostty/Ghostty.SplitNode.swift | 2 +- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 6829a742e..c3b9afa28 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -350,3 +350,30 @@ extension SplitTree.Node: Equatable { } } } + +// MARK: SplitTree Sequences + +extension SplitTree.Node { + /// Returns all leaf views in this subtree + func leaves() -> [NSView] { + switch self { + case .leaf(let view): + return [view] + + case .split(let split): + return split.left.leaves() + split.right.leaves() + } + } +} + +extension SplitTree: Sequence { + func makeIterator() -> [NSView].Iterator { + return root?.leaves().makeIterator() ?? [].makeIterator() + } +} + +extension SplitTree.Node: Sequence { + func makeIterator() -> [NSView].Iterator { + return leaves().makeIterator() + } +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b30cc7afb..a93896732 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -175,16 +175,16 @@ class BaseTerminalController: NSWindowController, /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// what surface is focused. This must be called whenever a surface OR window changes focus. func syncFocusToSurfaceTree() { - guard let tree = self.surfaceTree else { return } - - for leaf in tree { - // Our focus state requires that this window is key and our currently - // focused surface is the surface in this leaf. - let focused: Bool = (window?.isKeyWindow ?? false) && - !commandPaletteIsShowing && - focusedSurface != nil && - leaf.surface == focusedSurface! - leaf.surface.focusDidChange(focused) + for view in surfaceTree2 { + if let surfaceView = view as? Ghostty.SurfaceView { + // Our focus state requires that this window is key and our currently + // focused surface is the surface in this view. + let focused: Bool = (window?.isKeyWindow ?? false) && + !commandPaletteIsShowing && + focusedSurface != nil && + surfaceView == focusedSurface! + surfaceView.focusDidChange(focused) + } } } @@ -387,20 +387,18 @@ class BaseTerminalController: NSWindowController, } private func localEventFlagsChanged(_ event: NSEvent) -> NSEvent? { - // Go through all our surfaces and notify it that the flags changed. - if let surfaceTree { - var surfaces: [Ghostty.SurfaceView] = surfaceTree.map { $0.surface } - - // If we're the main window receiving key input, then we want to avoid - // calling this on our focused surface because that'll trigger a double - // flagsChanged call. - if NSApp.mainWindow == window { - surfaces = surfaces.filter { $0 != focusedSurface } - } - - for surface in surfaces { - surface.flagsChanged(with: event) - } + // Also update surfaceTree2 + var surfaces2: [Ghostty.SurfaceView] = surfaceTree2.compactMap { $0 as? Ghostty.SurfaceView } + + // If we're the main window receiving key input, then we want to avoid + // calling this on our focused surface because that'll trigger a double + // flagsChanged call. + if NSApp.mainWindow == window { + surfaces2 = surfaces2.filter { $0 != focusedSurface } + } + + for surface in surfaces2 { + surface.flagsChanged(with: event) } return event @@ -675,10 +673,10 @@ class BaseTerminalController: NSWindowController, } func windowDidChangeOcclusionState(_ notification: Notification) { - guard let surfaceTree = self.surfaceTree else { return } let visible = self.window?.occlusionState.contains(.visible) ?? false - for leaf in surfaceTree { - if let surface = leaf.surface.surface { + for view in surfaceTree2 { + if let surfaceView = view as? Ghostty.SurfaceView, + let surface = surfaceView.surface { ghostty_surface_set_occlusion(surface, visible) } } diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift index 97b20acd3..ff60e7c56 100644 --- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift +++ b/macos/Sources/Ghostty/Ghostty.SplitNode.swift @@ -11,7 +11,7 @@ extension Ghostty { /// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These /// values can further be split infinitely. /// - enum SplitNode: Equatable, Hashable, Codable, Sequence { + enum SplitNode: Equatable, Hashable, Codable { case leaf(Leaf) case split(Container) From d1dce1e37213282981738dd9f127a9c78d11c687 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2025 16:05:23 -0700 Subject: [PATCH 354/642] macos: restoration for new split tree --- macos/Sources/Features/Splits/SplitTree.swift | 64 +++++++++++++++---- .../Splits/TerminalSplitTreeView.swift | 11 ++-- .../Terminal/BaseTerminalController.swift | 42 ++++++------ .../Terminal/TerminalController.swift | 5 +- .../Features/Terminal/TerminalManager.swift | 5 +- .../Terminal/TerminalRestorable.swift | 34 ++++++++-- .../Features/Terminal/TerminalView.swift | 4 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 31 ++++++++- 8 files changed, 142 insertions(+), 54 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index c3b9afa28..a66d4abe7 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -1,7 +1,7 @@ import AppKit /// SplitTree represents a tree of views that can be divided. -struct SplitTree { +struct SplitTree: Codable { /// The root of the tree. This can be nil to indicate the tree is empty. let root: Node? @@ -11,11 +11,11 @@ struct SplitTree { /// A single node in the tree is either a leaf node (a view) or a split (has a /// left/right or top/bottom). - indirect enum Node { - case leaf(view: NSView) + indirect enum Node: Codable { + case leaf(view: ViewType) case split(Split) - struct Split: Equatable { + struct Split: Equatable, Codable { let direction: Direction let ratio: Double let left: Node @@ -23,7 +23,7 @@ struct SplitTree { } } - enum Direction { + enum Direction: Codable { case horizontal // Splits are laid out left and right case vertical // Splits are laid out top and bottom } @@ -63,12 +63,12 @@ extension SplitTree { self.init(root: nil, zoomed: nil) } - init(view: NSView) { + init(view: ViewType) { self.init(root: .leaf(view: view), zoomed: nil) } /// Insert a new view at the given view point by creating a split in the given direction. - func insert(view: NSView, at: NSView, direction: NewDirection) throws -> Self { + func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { guard let root else { throw SplitError.viewNotFound } return .init( root: try root.insert(view: view, at: at, direction: direction), @@ -122,7 +122,7 @@ extension SplitTree.Node { typealias Path = SplitTree.Path /// Returns the node in the tree that contains the given view. - func node(view: NSView) -> Node? { + func node(view: ViewType) -> Node? { switch (self) { case .leaf(view): return self @@ -188,7 +188,7 @@ extension SplitTree.Node { /// /// - Note: If the existing view (`at`) is not found in the tree, this method does nothing. We should /// maybe throw instead but at the moment we just do nothing. - func insert(view: NSView, at: NSView, direction: NewDirection) throws -> Self { + func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { // Get the path to our insertion point. If it doesn't exist we do // nothing. guard let path = path(to: .leaf(view: at)) else { @@ -351,11 +351,51 @@ extension SplitTree.Node: Equatable { } } +// MARK: SplitTree Codable + +extension SplitTree.Node { + enum CodingKeys: String, CodingKey { + case view + case split + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if container.contains(.view) { + let view = try container.decode(ViewType.self, forKey: .view) + self = .leaf(view: view) + } else if container.contains(.split) { + let split = try container.decode(Split.self, forKey: .split) + self = .split(split) + } else { + throw DecodingError.dataCorrupted( + DecodingError.Context( + codingPath: decoder.codingPath, + debugDescription: "No valid node type found" + ) + ) + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .leaf(let view): + try container.encode(view, forKey: .view) + + case .split(let split): + try container.encode(split, forKey: .split) + } + } +} + // MARK: SplitTree Sequences extension SplitTree.Node { /// Returns all leaf views in this subtree - func leaves() -> [NSView] { + func leaves() -> [ViewType] { switch self { case .leaf(let view): return [view] @@ -367,13 +407,13 @@ extension SplitTree.Node { } extension SplitTree: Sequence { - func makeIterator() -> [NSView].Iterator { + func makeIterator() -> [ViewType].Iterator { return root?.leaves().makeIterator() ?? [].makeIterator() } } extension SplitTree.Node: Sequence { - func makeIterator() -> [NSView].Iterator { + func makeIterator() -> [ViewType].Iterator { return leaves().makeIterator() } } diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 8f78dcbf8..3969b2e74 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -1,8 +1,8 @@ import SwiftUI struct TerminalSplitTreeView: View { - let tree: SplitTree - let onResize: (SplitTree.Node, Double) -> Void + let tree: SplitTree + let onResize: (SplitTree.Node, Double) -> Void var body: some View { if let node = tree.root { @@ -14,16 +14,15 @@ struct TerminalSplitTreeView: View { struct TerminalSplitSubtreeView: View { @EnvironmentObject var ghostty: Ghostty.App - let node: SplitTree.Node + let node: SplitTree.Node var isRoot: Bool = false - let onResize: (SplitTree.Node, Double) -> Void + let onResize: (SplitTree.Node, Double) -> Void var body: some View { switch (node) { case .leaf(let leafView): - // TODO: Fix the as! Ghostty.InspectableSurface( - surfaceView: leafView as! Ghostty.SurfaceView, + surfaceView: leafView, isSplit: !isRoot) case .split(let split): diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index a93896732..b3409c437 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -46,7 +46,7 @@ class BaseTerminalController: NSWindowController, didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } } - @Published var surfaceTree2: SplitTree = .init() + @Published var surfaceTree2: SplitTree = .init() /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false @@ -88,7 +88,8 @@ class BaseTerminalController: NSWindowController, init(_ ghostty: Ghostty.App, baseConfig base: Ghostty.SurfaceConfiguration? = nil, - surfaceTree tree: Ghostty.SplitNode? = nil + surfaceTree tree: Ghostty.SplitNode? = nil, + surfaceTree2 tree2: SplitTree? = nil ) { self.ghostty = ghostty self.derivedConfig = DerivedConfig(ghostty.config) @@ -98,9 +99,7 @@ class BaseTerminalController: NSWindowController, // Initialize our initial surface. guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base)) - - let firstView = Ghostty.SurfaceView(ghostty_app, baseConfig: base) - self.surfaceTree2 = .init(view: firstView) + self.surfaceTree2 = tree2 ?? .init(view: Ghostty.SurfaceView(ghostty_app, baseConfig: base)) // Setup our notifications for behaviors let center = NotificationCenter.default @@ -175,16 +174,14 @@ class BaseTerminalController: NSWindowController, /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// what surface is focused. This must be called whenever a surface OR window changes focus. func syncFocusToSurfaceTree() { - for view in surfaceTree2 { - if let surfaceView = view as? Ghostty.SurfaceView { - // Our focus state requires that this window is key and our currently - // focused surface is the surface in this view. - let focused: Bool = (window?.isKeyWindow ?? false) && - !commandPaletteIsShowing && - focusedSurface != nil && - surfaceView == focusedSurface! - surfaceView.focusDidChange(focused) - } + for surfaceView in surfaceTree2 { + // Our focus state requires that this window is key and our currently + // focused surface is the surface in this view. + let focused: Bool = (window?.isKeyWindow ?? false) && + !commandPaletteIsShowing && + focusedSurface != nil && + surfaceView == focusedSurface! + surfaceView.focusDidChange(focused) } } @@ -335,7 +332,7 @@ class BaseTerminalController: NSWindowController, // Determine our desired direction guard let directionAny = notification.userInfo?["direction"] else { return } guard let direction = directionAny as? ghostty_action_split_direction_e else { return } - let splitDirection: SplitTree.NewDirection + let splitDirection: SplitTree.NewDirection switch (direction) { case GHOSTTY_SPLIT_DIRECTION_RIGHT: splitDirection = .right case GHOSTTY_SPLIT_DIRECTION_LEFT: splitDirection = .left @@ -388,16 +385,16 @@ class BaseTerminalController: NSWindowController, private func localEventFlagsChanged(_ event: NSEvent) -> NSEvent? { // Also update surfaceTree2 - var surfaces2: [Ghostty.SurfaceView] = surfaceTree2.compactMap { $0 as? Ghostty.SurfaceView } - + var surfaces: [Ghostty.SurfaceView] = surfaceTree2.map { $0 } + // If we're the main window receiving key input, then we want to avoid // calling this on our focused surface because that'll trigger a double // flagsChanged call. if NSApp.mainWindow == window { - surfaces2 = surfaces2.filter { $0 != focusedSurface } + surfaces = surfaces.filter { $0 != focusedSurface } } - for surface in surfaces2 { + for surface in surfaces { surface.flagsChanged(with: event) } @@ -455,7 +452,7 @@ class BaseTerminalController: NSWindowController, func zoomStateDidChange(to: Bool) {} - func splitDidResize(node: SplitTree.Node, to newRatio: Double) { + func splitDidResize(node: SplitTree.Node, to newRatio: Double) { let resizedNode = node.resize(to: newRatio) do { surfaceTree2 = try surfaceTree2.replace(node: node, with: resizedNode) @@ -675,8 +672,7 @@ class BaseTerminalController: NSWindowController, func windowDidChangeOcclusionState(_ notification: Notification) { let visible = self.window?.occlusionState.contains(.visible) ?? false for view in surfaceTree2 { - if let surfaceView = view as? Ghostty.SurfaceView, - let surface = surfaceView.surface { + if let surface = view.surface { ghostty_surface_set_occlusion(surface, visible) } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 82491e76d..5c2f58dab 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -32,7 +32,8 @@ class TerminalController: BaseTerminalController { init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree tree: Ghostty.SplitNode? = nil + withSurfaceTree tree: Ghostty.SplitNode? = nil, + withSurfaceTree2 tree2: SplitTree? = nil ) { // The window we manage is not restorable if we've specified a command // to execute. We do this because the restored window is meaningless at the @@ -44,7 +45,7 @@ class TerminalController: BaseTerminalController { // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - super.init(ghostty, baseConfig: base, surfaceTree: tree) + super.init(ghostty, baseConfig: base, surfaceTree: tree, surfaceTree2: tree2) // Setup our notifications for behaviors let center = NotificationCenter.default diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 07735cb58..2968f8abd 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -197,9 +197,10 @@ class TerminalManager { /// Creates a window controller, adds it to our managed list, and returns it. func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree tree: Ghostty.SplitNode? = nil) -> TerminalController { + withSurfaceTree tree: Ghostty.SplitNode? = nil, + withSurfaceTree2 tree2: SplitTree? = nil) -> TerminalController { // Initialize our controller to load the window - let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree: tree) + let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree: tree, withSurfaceTree2: tree2) // Create a listener for when the window is closed so we can remove it. let pubClose = NotificationCenter.default.publisher( diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index b9d9b0ac0..5531494a5 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -4,14 +4,16 @@ import Cocoa class TerminalRestorableState: Codable { static let selfKey = "state" static let versionKey = "version" - static let version: Int = 2 + static let version: Int = 3 let focusedSurface: String? let surfaceTree: Ghostty.SplitNode? + let surfaceTree2: SplitTree? init(from controller: TerminalController) { self.focusedSurface = controller.focusedSurface?.uuid.uuidString self.surfaceTree = controller.surfaceTree + self.surfaceTree2 = controller.surfaceTree2 } init?(coder aDecoder: NSCoder) { @@ -27,6 +29,7 @@ class TerminalRestorableState: Codable { } self.surfaceTree = v.value.surfaceTree + self.surfaceTree2 = v.value.surfaceTree2 self.focusedSurface = v.value.focusedSurface } @@ -83,18 +86,37 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // can be found for events from libghostty. This uses the low-level // createWindow so that AppKit can place the window wherever it should // be. - let c = appDelegate.terminalManager.createWindow(withSurfaceTree: state.surfaceTree) + let c = appDelegate.terminalManager.createWindow( + withSurfaceTree: state.surfaceTree, + withSurfaceTree2: state.surfaceTree2 + ) guard let window = c.window else { completionHandler(nil, TerminalRestoreError.windowDidNotLoad) return } // Setup our restored state on the controller + // First try to find the focused surface in surfaceTree2 if let focusedStr = state.focusedSurface, - let focusedUUID = UUID(uuidString: focusedStr), - let view = c.surfaceTree?.findUUID(uuid: focusedUUID) { - c.focusedSurface = view - restoreFocus(to: view, inWindow: window) + let focusedUUID = UUID(uuidString: focusedStr) { + // Try surfaceTree2 first + var foundView: Ghostty.SurfaceView? + for view in c.surfaceTree2 { + if view.uuid.uuidString == focusedStr { + foundView = view + break + } + } + + // Fall back to surfaceTree if not found + if foundView == nil { + foundView = c.surfaceTree?.findUUID(uuid: focusedUUID) + } + + if let view = foundView { + c.focusedSurface = view + restoreFocus(to: view, inWindow: window) + } } completionHandler(window, nil) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 2970f19c6..d13de4a72 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -21,7 +21,7 @@ protocol TerminalViewDelegate: AnyObject { func performAction(_ action: String, on: Ghostty.SurfaceView) /// A split is resizing to a given value. - func splitDidResize(node: SplitTree.Node, to newRatio: Double) + func splitDidResize(node: SplitTree.Node, to newRatio: Double) } /// The view model is a required implementation for TerminalView callers. This contains @@ -30,7 +30,7 @@ protocol TerminalViewDelegate: AnyObject { protocol TerminalViewModel: ObservableObject { /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView /// and children. This should be @Published. - var surfaceTree2: SplitTree { get set } + var surfaceTree2: SplitTree { get set } /// The command palette state. var commandPaletteIsShowing: Bool { get set } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 99f901792..0aecef6ad 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -6,7 +6,7 @@ import GhosttyKit extension Ghostty { /// The NSView implementation for a terminal surface. - class SurfaceView: OSView, ObservableObject { + class SurfaceView: OSView, ObservableObject, Codable { /// Unique ID per surface let uuid: UUID @@ -1431,6 +1431,35 @@ extension Ghostty { self.windowAppearance = .init(ghosttyConfig: config) } } + + // MARK: - Codable + + enum CodingKeys: String, CodingKey { + case pwd + case uuid + } + + required convenience init(from decoder: Decoder) throws { + // Decoding uses the global Ghostty app + guard let del = NSApplication.shared.delegate, + let appDel = del as? AppDelegate, + let app = appDel.ghostty.app else { + throw TerminalRestoreError.delegateInvalid + } + + let container = try decoder.container(keyedBy: CodingKeys.self) + let uuid = UUID(uuidString: try container.decode(String.self, forKey: .uuid)) + var config = Ghostty.SurfaceConfiguration() + config.workingDirectory = try container.decode(String?.self, forKey: .pwd) + + self.init(app, baseConfig: config, uuid: uuid) + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(pwd, forKey: .pwd) + try container.encode(uuid.uuidString, forKey: .uuid) + } } } From b84b715ddbf6de59c74d969bcb1597c06aaaa945 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2025 16:43:20 -0700 Subject: [PATCH 355/642] macos: unify confirm close in our terminal controllers --- .../Terminal/BaseTerminalController.swift | 92 ++++++++++--------- .../Terminal/TerminalController.swift | 23 ----- 2 files changed, 47 insertions(+), 68 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b3409c437..1ffea9b4f 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -194,6 +194,40 @@ class BaseTerminalController: NSWindowController, savedFrame = .init(window: window.frame, screen: screen.visibleFrame) } + func confirmClose( + messageText: String, + informativeText: String, + completion: @escaping () -> Void + ) { + // If we already have an alert, we need to wait for that one. + guard alert == nil else { return } + + // If there is no window to attach the modal then we assume success + // since we'll never be able to show the modal. + guard let window else { + completion() + return + } + + // If we need confirmation by any, show one confirmation for all windows + // in the tab group. + let alert = NSAlert() + alert.messageText = messageText + alert.informativeText = informativeText + alert.addButton(withTitle: "Close") + alert.addButton(withTitle: "Cancel") + alert.alertStyle = .warning + alert.beginSheetModal(for: window) { response in + self.alert = nil + if response == .alertFirstButtonReturn { + completion() + } + } + + // Store our alert so we only ever show one. + self.alert = alert + } + // MARK: Notifications @objc private func didChangeScreenParametersNotification(_ notification: Notification) { @@ -287,37 +321,19 @@ class BaseTerminalController: NSWindowController, return } - // If we don't have a window to attach our modal to, we also exit immediately. - // This should NOT happen. - guard let window = target.window else { - surfaceTree2 = surfaceTree2.remove(node) - return - } - // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that // confirmationDialog allows the user to Cmd-W close the alert, but when doing // so SwiftUI does not update any of the bindings to note that window is no longer // being shown, and provides no callback to detect this. - let alert = NSAlert() - alert.messageText = "Close Terminal?" - alert.informativeText = "The terminal still has a running process. If you close the " + - "terminal the process will be killed." - alert.addButton(withTitle: "Close the Terminal") - alert.addButton(withTitle: "Cancel") - alert.alertStyle = .warning - alert.beginSheetModal(for: window, completionHandler: { [weak self] response in - switch (response) { - case .alertFirstButtonReturn: - alert.window.orderOut(nil) - if let self { - self.surfaceTree2 = self.surfaceTree2.remove(node) - } - - default: - break + confirmClose( + messageText: "Close Terminal?", + informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." + ) { [weak self] in + if let self { + self.surfaceTree2 = self.surfaceTree2.remove(node) } - }) + } } @objc private func ghosttyDidNewSplit(_ notification: Notification) { @@ -624,26 +640,12 @@ class BaseTerminalController: NSWindowController, if (!node.needsConfirmQuit()) { return true } // We require confirmation, so show an alert as long as we aren't already. - let alert = NSAlert() - alert.messageText = "Close Terminal?" - alert.informativeText = "The terminal still has a running process. If you close the " + - "terminal the process will be killed." - alert.addButton(withTitle: "Close the Terminal") - alert.addButton(withTitle: "Cancel") - alert.alertStyle = .warning - alert.beginSheetModal(for: window, completionHandler: { response in - self.alert = nil - switch (response) { - case .alertFirstButtonReturn: - alert.window.orderOut(nil) - window.close() - - default: - break - } - }) - - self.alert = alert + confirmClose( + messageText: "Close Terminal?", + informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." + ) { + window.close() + } return false } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 5c2f58dab..8e88952f0 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -587,27 +587,6 @@ class TerminalController: BaseTerminalController { ghostty.newTab(surface: surface) } - private func confirmClose( - window: NSWindow, - messageText: String, - informativeText: String, - completion: @escaping () -> Void - ) { - // If we need confirmation by any, show one confirmation for all windows - // in the tab group. - let alert = NSAlert() - alert.messageText = messageText - alert.informativeText = informativeText - alert.addButton(withTitle: "Close") - alert.addButton(withTitle: "Cancel") - alert.alertStyle = .warning - alert.beginSheetModal(for: window) { response in - if response == .alertFirstButtonReturn { - completion() - } - } - } - @IBAction func closeTab(_ sender: Any?) { guard let window = window else { return } guard window.tabGroup != nil else { @@ -618,7 +597,6 @@ class TerminalController: BaseTerminalController { if surfaceTree?.needsConfirmQuit() ?? false { confirmClose( - window: window, messageText: "Close Tab?", informativeText: "The terminal still has a running process. If you close the tab the process will be killed." ) { @@ -664,7 +642,6 @@ class TerminalController: BaseTerminalController { } confirmClose( - window: window, messageText: "Close Window?", informativeText: "All terminal sessions in this window will be terminated." ) { From 0fb58298a78979e75c67717d0587ffc3c94430e9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 3 Jun 2025 19:41:21 -0700 Subject: [PATCH 356/642] macos: focus split previous/next --- macos/Ghostty.xcodeproj/project.pbxproj | 12 ++++ macos/Sources/Features/Splits/SplitTree.swift | 72 +++++++++++++++++++ .../Terminal/BaseTerminalController.swift | 37 ++++++++++ macos/Sources/Ghostty/Ghostty.App.swift | 3 +- .../Helpers/Extensions/Array+Extension.swift | 19 +++++ 5 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 macos/Sources/Helpers/Extensions/Array+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 459b2b994..38e29a60e 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ A586167C2B7703CC009BDB1D /* fish in Resources */ = {isa = PBXBuildFile; fileRef = A586167B2B7703CC009BDB1D /* fish */; }; A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; }; A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */; }; + A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; }; A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; }; A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; @@ -168,6 +169,7 @@ A586167B2B7703CC009BDB1D /* fish */ = {isa = PBXFileReference; lastKnownFileType = folder; name = fish; path = "../zig-out/share/fish"; sourceTree = ""; }; A586365E2DEE6C2100E04A10 /* SplitTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitTree.swift; sourceTree = ""; }; A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSplitTreeView.swift; sourceTree = ""; }; + A586366A2DF0A98900E04A10 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = ""; }; A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -292,6 +294,7 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A58636692DF0A98100E04A10 /* Extensions */, A5874D9B2DAD781100E83852 /* Private */, A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, A5A6F7292CC41B8700B232A5 /* Xcode.swift */, @@ -442,6 +445,14 @@ path = Splits; sourceTree = ""; }; + A58636692DF0A98100E04A10 /* Extensions */ = { + isa = PBXGroup; + children = ( + A586366A2DF0A98900E04A10 /* Array+Extension.swift */, + ); + path = Extensions; + sourceTree = ""; + }; A5874D9B2DAD781100E83852 /* Private */ = { isa = PBXGroup; children = ( @@ -721,6 +732,7 @@ A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, + A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index a66d4abe7..a093934d8 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -50,6 +50,20 @@ struct SplitTree: Codable { case down case up } + + /// The direction that focus can move from a node. + enum FocusDirection { + // Follow a consistent tree-like structure. + case previous + case next + + // Geospatially-aware navigation targets. These take into account the + // dimensions of the view to find the correct node to go to. + case up + case down + case left + case right + } } // MARK: SplitTree @@ -111,6 +125,44 @@ extension SplitTree { return .init(root: newRoot, zoomed: newZoomed) } + + /// Find the next view to focus based on the current focused node and direction + func focusTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? { + guard let root else { return nil } + + switch direction { + case .previous: + // For previous, we traverse in order and find the previous leaf from our leftmost + let allLeaves = root.leaves() + let currentView = currentNode.leftmostLeaf() + guard let currentIndex = allLeaves.firstIndex(where: { $0 === currentView }) else { + // Shouldn't be possible leftmostLeaf can't return something that doesn't exist! + return nil + } + let index = allLeaves.indexWrapping(before: currentIndex) + return allLeaves[index] + + case .next: + // For previous, we traverse in order and find the next leaf from our rightmost + let allLeaves = root.leaves() + let currentView = currentNode.rightmostLeaf() + guard let currentIndex = allLeaves.firstIndex(where: { $0 === currentView }) else { + return nil + } + let index = allLeaves.indexWrapping(after: currentIndex) + return allLeaves[index] + + case .up, .down, .left, .right: + // For directional movement, we need to traverse the tree structure + return directionalTarget(for: direction, from: currentNode) + } + } + + /// Find focus target in a specific direction by traversing split boundaries + private func directionalTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? { + // TODO + return nil + } } // MARK: SplitTree.Node @@ -331,6 +383,26 @@ extension SplitTree.Node { )) } } + + /// Get the leftmost leaf in this subtree + func leftmostLeaf() -> ViewType { + switch self { + case .leaf(let view): + return view + case .split(let split): + return split.left.leftmostLeaf() + } + } + + /// Get the rightmost leaf in this subtree + func rightmostLeaf() -> ViewType { + switch self { + case .leaf(let view): + return view + case .split(let split): + return split.right.rightmostLeaf() + } + } } // MARK: SplitTree.Node Protocols diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 1ffea9b4f..b6b745e82 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -145,6 +145,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyDidEqualizeSplits(_:)), name: Ghostty.Notification.didEqualizeSplits, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidFocusSplit(_:)), + name: Ghostty.Notification.ghosttyFocusSplit, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -386,6 +391,38 @@ class BaseTerminalController: NSWindowController, _ = container.equalize() } } + + @objc private func ghosttyDidFocusSplit(_ notification: Notification) { + // The target must be within our tree + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree2.root?.node(view: target) != nil else { return } + + // Get the direction from the notification + guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return } + guard let direction = directionAny as? Ghostty.SplitFocusDirection else { return } + + // Convert Ghostty.SplitFocusDirection to our SplitTree.FocusDirection + let focusDirection: SplitTree.FocusDirection + switch direction { + case .previous: focusDirection = .previous + case .next: focusDirection = .next + case .up: focusDirection = .up + case .down: focusDirection = .down + case .left: focusDirection = .left + case .right: focusDirection = .right + } + + // Find the node for the target surface + guard let targetNode = surfaceTree2.root?.node(view: target) else { return } + + // Find the next surface to focus + guard let nextSurface = surfaceTree2.focusTarget(for: focusDirection, from: targetNode) else { + return + } + + // Move focus to the next surface + Ghostty.moveFocus(to: nextSurface, from: target) + } // MARK: Local Events diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index d8fdaa3ec..95e04fc1e 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -921,7 +921,8 @@ extension Ghostty { // we should only be returning true if we actually performed the action, // but this handles the most common case of caring about goto_split performability // which is the no-split case. - guard controller.surfaceTree?.isSplit ?? false else { return false } + // TODO: fix this + //guard controller.surfaceTree?.isSplit ?? false else { return false } NotificationCenter.default.post( name: Notification.ghosttyFocusSplit, diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift new file mode 100644 index 000000000..6f005a349 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -0,0 +1,19 @@ +extension Array { + /// Returns the index before i, with wraparound. Assumes i is a valid index. + func indexWrapping(before i: Int) -> Int { + if i == 0 { + return count - 1 + } + + return i - 1 + } + + /// Returns the index after i, with wraparound. Assumes i is a valid index. + func indexWrapping(after i: Int) -> Int { + if i == count - 1 { + return 0 + } + + return i + 1 + } +} From 7dcfebcd5d99fe76c5d861e2c30ba4eb92f16fd5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 09:28:18 -0700 Subject: [PATCH 357/642] macos: isSplit guarding on focus split directions works --- macos/Sources/Features/Splits/SplitTree.swift | 7 ++++++- macos/Sources/Ghostty/Ghostty.App.swift | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index a093934d8..c3a4e3097 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -72,7 +72,12 @@ extension SplitTree { var isEmpty: Bool { root == nil } - + + /// Returns true if this tree is split. + var isSplit: Bool { + if case .split = root { true } else { false } + } + init() { self.init(root: nil, zoomed: nil) } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 95e04fc1e..08c284b04 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -921,8 +921,7 @@ extension Ghostty { // we should only be returning true if we actually performed the action, // but this handles the most common case of caring about goto_split performability // which is the no-split case. - // TODO: fix this - //guard controller.surfaceTree?.isSplit ?? false else { return false } + guard controller.surfaceTree2.isSplit else { return false } NotificationCenter.default.post( name: Notification.ghosttyFocusSplit, From aef61661a01f6e4d08012f775161c0e46358a5f8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 09:42:12 -0700 Subject: [PATCH 358/642] macos: fix up command palette, focusing --- macos/Ghostty.xcodeproj/project.pbxproj | 2 +- .../Features/Terminal/BaseTerminalController.swift | 6 +++--- .../Features/Terminal/TerminalController.swift | 8 ++++---- .../Helpers/{ => Extensions}/NSView+Extension.swift | 13 +++++++++++++ 4 files changed, 21 insertions(+), 8 deletions(-) rename macos/Sources/Helpers/{ => Extensions}/NSView+Extension.swift (77%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 38e29a60e..8c73d55c5 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -314,7 +314,6 @@ A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, - C1F26EA62B738B9900404083 /* NSView+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, @@ -449,6 +448,7 @@ isa = PBXGroup; children = ( A586366A2DF0A98900E04A10 /* Array+Extension.swift */, + C1F26EA62B738B9900404083 /* NSView+Extension.swift */, ); path = Extensions; sourceTree = ""; diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index b6b745e82..6429f70a3 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -295,14 +295,14 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyCommandPaletteDidToggle(_ notification: Notification) { guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree?.contains(view: surfaceView) ?? false else { return } + guard surfaceTree2.contains(surfaceView) else { return } toggleCommandPalette(nil) } @objc private func ghosttyMaximizeDidToggle(_ notification: Notification) { guard let window else { return } guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree?.contains(view: surfaceView) ?? false else { return } + guard surfaceTree2.contains(surfaceView) else { return } window.zoom(nil) } @@ -468,7 +468,7 @@ class BaseTerminalController: NSWindowController, // want to care if the surface is in the tree so we don't listen to titles of // closed surfaces. if let titleSurface = focusedSurface ?? lastFocusedSurface, - surfaceTree?.contains(view: titleSurface) ?? false { + surfaceTree2.contains(titleSurface) { // If we have a surface, we want to listen for title changes. titleSurface.$title .sink { [weak self] in self?.titleDidChange(to: $0) } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 8e88952f0..a2687e1fe 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -159,7 +159,7 @@ class TerminalController: BaseTerminalController { // This is a surface-level config update. If we have the surface, we // update our appearance based on it. guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree?.contains(view: surfaceView) ?? false else { return } + guard surfaceTree2.contains(surfaceView) else { return } // We can't use surfaceView.derivedConfig because it may not be updated // yet since it also responds to notifications. @@ -815,19 +815,19 @@ class TerminalController: BaseTerminalController { @objc private func onCloseTab(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree?.contains(view: target) ?? false else { return } + guard surfaceTree2.contains(target) else { return } closeTab(self) } @objc private func onCloseWindow(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree?.contains(view: target) ?? false else { return } + guard surfaceTree2.contains(target) else { return } closeWindow(self) } @objc private func onResetWindowSize(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree?.contains(view: target) ?? false else { return } + guard surfaceTree2.contains(target) else { return } returnToDefaultSize(nil) } diff --git a/macos/Sources/Helpers/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift similarity index 77% rename from macos/Sources/Helpers/NSView+Extension.swift rename to macos/Sources/Helpers/Extensions/NSView+Extension.swift index b9234a49a..48284df74 100644 --- a/macos/Sources/Helpers/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -1,6 +1,19 @@ import AppKit extension NSView { + /// Returns true if this view is currently in the responder chain + var isInResponderChain: Bool { + var responder = window?.firstResponder + while let currentResponder = responder { + if currentResponder === self { + return true + } + responder = currentResponder.nextResponder + } + + return false + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { From a389926ca7345be7da1ee1b4608fe63776e97aac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 09:47:55 -0700 Subject: [PATCH 359/642] macos: use surfaceTree2 needsConfirmQuit --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 4 ++-- macos/Sources/Features/Terminal/TerminalController.swift | 4 ++-- macos/Sources/Features/Terminal/TerminalManager.swift | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 6429f70a3..6c5718371 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -668,13 +668,13 @@ class BaseTerminalController: NSWindowController, guard let window = self.window else { return true } // If we have no surfaces, close. - guard let node = self.surfaceTree else { return true } + if surfaceTree2.isEmpty { return true } // If we already have an alert, continue with it guard alert == nil else { return false } // If our surfaces don't require confirmation, close. - if (!node.needsConfirmQuit()) { return true } + if !surfaceTree2.contains(where: { $0.needsConfirmQuit }) { return true } // We require confirmation, so show an alert as long as we aren't already. confirmClose( diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a2687e1fe..78bb58cc8 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -595,7 +595,7 @@ class TerminalController: BaseTerminalController { return } - if surfaceTree?.needsConfirmQuit() ?? false { + if surfaceTree2.contains(where: { $0.needsConfirmQuit }) { confirmClose( messageText: "Close Tab?", informativeText: "The terminal still has a running process. If you close the tab the process will be killed." @@ -632,7 +632,7 @@ class TerminalController: BaseTerminalController { guard let controller = tabWindow.windowController as? TerminalController else { return false } - return controller.surfaceTree?.needsConfirmQuit() ?? false + return controller.surfaceTree2.contains(where: { $0.needsConfirmQuit }) } // If none need confirmation then we can just close all the windows. diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 2968f8abd..475b70ac9 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -269,7 +269,7 @@ class TerminalManager { func closeAllWindows() { var needsConfirm: Bool = false for w in self.windows { - if (w.controller.surfaceTree?.needsConfirmQuit() ?? false) { + if w.controller.surfaceTree2.contains(where: { $0.needsConfirmQuit }) { needsConfirm = true break } From ec7fd94d0ff325e2d7a6ead1ee792911b0e3b136 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 10:00:37 -0700 Subject: [PATCH 360/642] macos: equalize splits works with new tree --- macos/Sources/Features/Splits/SplitTree.swift | 44 +++++++++++++++++++ .../Terminal/BaseTerminalController.swift | 7 ++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index c3a4e3097..ed4e2dba3 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -168,6 +168,14 @@ extension SplitTree { // TODO return nil } + + /// Equalize all splits in the tree so that each split's ratio is based on the + /// relative weight (number of leaves) of its children. + func equalize() -> Self { + guard let root else { return self } + let newRoot = root.equalize() + return .init(root: newRoot, zoomed: zoomed) + } } // MARK: SplitTree.Node @@ -408,6 +416,42 @@ extension SplitTree.Node { return split.right.rightmostLeaf() } } + + /// Equalize this node and all its children, returning a new node with splits + /// adjusted so that each split's ratio is based on the relative weight + /// (number of leaves) of its children. + func equalize() -> Node { + let (equalizedNode, _) = equalizeWithWeight() + return equalizedNode + } + + /// Internal helper that equalizes and returns both the node and its weight. + private func equalizeWithWeight() -> (node: Node, weight: Int) { + switch self { + case .leaf: + // A leaf has weight 1 and doesn't change + return (self, 1) + + case .split(let split): + // Recursively equalize children + let (leftNode, leftWeight) = split.left.equalizeWithWeight() + let (rightNode, rightWeight) = split.right.equalizeWithWeight() + + // Calculate new ratio based on relative weights + let totalWeight = leftWeight + rightWeight + let newRatio = Double(leftWeight) / Double(totalWeight) + + // Create new split with equalized ratio + let newSplit = Split( + direction: split.direction, + ratio: newRatio, + left: leftNode, + right: rightNode + ) + + return (.split(newSplit), totalWeight) + } + } } // MARK: SplitTree.Node Protocols diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 6c5718371..5558aefe3 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -385,11 +385,10 @@ class BaseTerminalController: NSWindowController, guard let target = notification.object as? Ghostty.SurfaceView else { return } // Check if target surface is in current controller's tree - guard surfaceTree?.contains(view: target) ?? false else { return } + guard surfaceTree2.contains(target) else { return } - if case .split(let container) = surfaceTree { - _ = container.equalize() - } + // Equalize the splits + surfaceTree2 = surfaceTree2.equalize() } @objc private func ghosttyDidFocusSplit(_ notification: Notification) { From b7c01b5b4a6b1bc5a907807d91c9e9e70ab6af83 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 10:04:03 -0700 Subject: [PATCH 361/642] macos: spatial focus navigation --- macos/Sources/Features/Splits/SplitTree.swift | 356 +++++++++++++++++- .../Terminal/BaseTerminalController.swift | 8 +- 2 files changed, 343 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index ed4e2dba3..f458b5dee 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -40,6 +40,29 @@ struct SplitTree: Codable { } } + /// Spatial representation of the split tree. This can be used to better understand + /// its physical representation to perform tasks such as navigation. + struct Spatial { + let slots: [Slot] + + /// A single slot within the spatial mapping of a tree. Note that the bounds are + /// _relative_. They can't be mapped to physical pixels because the SplitTree + /// isn't aware of actual rendering. But relative to each other the bounds are + /// correct. + struct Slot { + let node: Node + let bounds: CGRect + } + + /// Direction for spatial navigation within the split tree. + enum Direction { + case left + case right + case up + case down + } + } + enum SplitError: Error { case viewNotFound } @@ -57,12 +80,10 @@ struct SplitTree: Codable { case previous case next - // Geospatially-aware navigation targets. These take into account the - // dimensions of the view to find the correct node to go to. - case up - case down - case left - case right + // Spatially-aware navigation targets. These take into account the + // layout to find the spatially correct node to move to. Spatial navigation + // is always from the top-left corner for now. + case spatial(Spatial.Direction) } } @@ -134,7 +155,7 @@ extension SplitTree { /// Find the next view to focus based on the current focused node and direction func focusTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? { guard let root else { return nil } - + switch direction { case .previous: // For previous, we traverse in order and find the previous leaf from our leftmost @@ -157,18 +178,33 @@ extension SplitTree { let index = allLeaves.indexWrapping(after: currentIndex) return allLeaves[index] - case .up, .down, .left, .right: - // For directional movement, we need to traverse the tree structure - return directionalTarget(for: direction, from: currentNode) + case .spatial(let spatialDirection): + // Get spatial representation and find best candidate + let spatial = root.spatial() + let nodes = spatial.slots(in: spatialDirection, from: currentNode) + + // If we have no nodes in the direction specified then we don't do + // anything. + if nodes.isEmpty { + return nil + } + + // Extract the view from the best candidate node + let bestNode = nodes[0].node + switch bestNode { + case .leaf(let view): + return view + case .split: + // If the best candidate is a split node, use its the leaf/rightmost + // depending on our spatial direction. + return switch (spatialDirection) { + case .up, .left: bestNode.leftmostLeaf() + case .down, .right: bestNode.rightmostLeaf() + } + } } } - - /// Find focus target in a specific direction by traversing split boundaries - private func directionalTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? { - // TODO - return nil - } - + /// Equalize all splits in the tree so that each split's ratio is based on the /// relative weight (number of leaves) of its children. func equalize() -> Self { @@ -452,6 +488,292 @@ extension SplitTree.Node { return (.split(newSplit), totalWeight) } } + + /// Calculate the bounds of all views in this subtree based on split ratios + func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] { + switch self { + case .leaf(let view): + return [(view, bounds)] + + case .split(let split): + // Calculate bounds for left and right based on split direction and ratio + let leftBounds: CGRect + let rightBounds: CGRect + + switch split.direction { + case .horizontal: + // Split horizontally: left | right + let splitX = bounds.minX + bounds.width * split.ratio + leftBounds = CGRect( + x: bounds.minX, + y: bounds.minY, + width: bounds.width * split.ratio, + height: bounds.height + ) + rightBounds = CGRect( + x: splitX, + y: bounds.minY, + width: bounds.width * (1 - split.ratio), + height: bounds.height + ) + + case .vertical: + // Split vertically: top / bottom + // Note: In our normalized coordinate system, Y increases upward + let splitY = bounds.minY + bounds.height * split.ratio + leftBounds = CGRect( + x: bounds.minX, + y: splitY, + width: bounds.width, + height: bounds.height * (1 - split.ratio) + ) + rightBounds = CGRect( + x: bounds.minX, + y: bounds.minY, + width: bounds.width, + height: bounds.height * split.ratio + ) + } + + // Recursively calculate bounds for children + return split.left.calculateViewBounds(in: leftBounds) + + split.right.calculateViewBounds(in: rightBounds) + } + } +} + +// MARK: SplitTree.Node Spatial + +extension SplitTree.Node { + /// Returns the spatial representation of this node and its subtree. + /// + /// This method creates a `Spatial` representation that maps the logical split tree structure + /// to 2D coordinate space. The coordinate system uses (0,0) as the top-left corner with + /// positive X extending right and positive Y extending down. + /// + /// The spatial representation provides: + /// - Relative bounds for each node based on split ratios + /// - Grid-like dimensions where each split adds 1 to the column/row count + /// - Accurate positioning that reflects the actual layout structure + /// + /// The bounds are pixel perfect based on assuming that each row and column are 1 pixel + /// tall or wide, respectively. This needs to be scaled up to the proper bounds for a real + /// layout. + /// + /// Example: + /// ``` + /// // For a layout like: + /// // +--------+----+ + /// // | A | B | + /// // +--------+----+ + /// // | C | D | + /// // +--------+----+ + /// // + /// // The spatial representation would have: + /// // - Total dimensions: (width: 2, height: 2) + /// // - Node bounds based on actual split ratios + /// ``` + /// + /// - Returns: A `Spatial` struct containing all slots with their calculated bounds + func spatial() -> SplitTree.Spatial { + // First, calculate the total dimensions needed + let dimensions = dimensions() + + // Calculate slots with relative bounds + let slots = spatialSlots( + in: CGRect(x: 0, y: 0, width: Double(dimensions.width), height: Double(dimensions.height)) + ) + + return SplitTree.Spatial(slots: slots) + } + + /// Calculates the grid dimensions (columns and rows) needed to represent this subtree. + /// + /// This method recursively analyzes the split tree structure to determine how many + /// columns and rows are needed to represent the layout in a 2D grid. Each leaf node + /// occupies one grid cell (1×1), and each split extends the grid in one direction: + /// + /// - **Horizontal splits**: Add columns (increase width) + /// - **Vertical splits**: Add rows (increase height) + /// + /// The calculation rules are: + /// - **Leaf nodes**: Always (1, 1) - one column, one row + /// - **Horizontal splits**: Width = sum of children widths, Height = max of children heights + /// - **Vertical splits**: Width = max of children widths, Height = sum of children heights + /// + /// Example: + /// ``` + /// // Single leaf: (1, 1) + /// // Horizontal split with 2 leaves: (2, 1) + /// // Vertical split with 2 leaves: (1, 2) + /// // Complex layout with both: (2, 2) or larger + /// ``` + /// + /// - Returns: A tuple containing (width: columns, height: rows) as unsigned integers + private func dimensions() -> (width: UInt, height: UInt) { + switch self { + case .leaf: + return (1, 1) + + case .split(let split): + let leftDimensions = split.left.dimensions() + let rightDimensions = split.right.dimensions() + + switch split.direction { + case .horizontal: + // Horizontal split: width is sum, height is max + return ( + width: leftDimensions.width + rightDimensions.width, + height: Swift.max(leftDimensions.height, rightDimensions.height) + ) + + case .vertical: + // Vertical split: height is sum, width is max + return ( + width: Swift.max(leftDimensions.width, rightDimensions.width), + height: leftDimensions.height + rightDimensions.height + ) + } + } + } + + /// Calculates the spatial slots (nodes with bounds) for this subtree within the given bounds. + /// + /// This method recursively traverses the split tree and calculates the precise bounds + /// for each node based on the split ratios and directions. The bounds are calculated + /// relative to the provided bounds rectangle. + /// + /// The calculation process: + /// 1. **Leaf nodes**: Create a single slot with the provided bounds + /// 2. **Split nodes**: + /// - Divide the bounds according to the split ratio and direction + /// - Create a slot for the split node itself + /// - Recursively calculate slots for both children + /// - Return all slots combined + /// + /// Split ratio interpretation: + /// - **Horizontal splits**: Ratio determines left/right width distribution + /// - Left child gets `ratio * width` + /// - Right child gets `(1 - ratio) * width` + /// - **Vertical splits**: Ratio determines top/bottom height distribution + /// - Top (left) child gets `ratio * height` + /// - Bottom (right) child gets `(1 - ratio) * height` + /// + /// Coordinate system: (0,0) is top-left, positive X goes right, positive Y goes down. + /// + /// - Parameter bounds: The bounding rectangle to subdivide for this subtree + /// - Returns: An array of `Spatial.Slot` objects, each containing a node and its bounds + private func spatialSlots(in bounds: CGRect) -> [SplitTree.Spatial.Slot] { + switch self { + case .leaf: + // A leaf takes up our full bounds. + return [.init(node: self, bounds: bounds)] + + case .split(let split): + let leftBounds: CGRect + let rightBounds: CGRect + + switch split.direction { + case .horizontal: + // Split horizontally: left | right using the ratio + let splitX = bounds.minX + bounds.width * split.ratio + leftBounds = CGRect( + x: bounds.minX, + y: bounds.minY, + width: bounds.width * split.ratio, + height: bounds.height + ) + rightBounds = CGRect( + x: splitX, + y: bounds.minY, + width: bounds.width * (1 - split.ratio), + height: bounds.height + ) + + case .vertical: + // Split vertically: top / bottom using the ratio + // Top-left is (0,0), so top (left) gets the upper portion + let splitY = bounds.minY + bounds.height * split.ratio + leftBounds = CGRect( + x: bounds.minX, + y: bounds.minY, + width: bounds.width, + height: bounds.height * split.ratio + ) + rightBounds = CGRect( + x: bounds.minX, + y: splitY, + width: bounds.width, + height: bounds.height * (1 - split.ratio) + ) + } + + // Recursively calculate slots for children and include a slot for this split + var slots: [SplitTree.Spatial.Slot] = [.init(node: self, bounds: bounds)] + slots += split.left.spatialSlots(in: leftBounds) + slots += split.right.spatialSlots(in: rightBounds) + + return slots + } + } +} + +// MARK: SplitTree.Spatial + +extension SplitTree.Spatial { + /// Returns all slots in the specified direction relative to the reference node. + /// + /// This method finds all slots positioned in the given direction from the reference node: + /// - **Left**: Slots with bounds to the left of the reference node + /// - **Right**: Slots with bounds to the right of the reference node + /// - **Up**: Slots with bounds above the reference node (Y=0 is top) + /// - **Down**: Slots with bounds below the reference node + /// + /// Results are sorted by distance from the reference node, with closest slots first. + /// Distance is calculated as the gap between the reference node and the candidate slot + /// in the direction of movement. + /// + /// - Parameters: + /// - direction: The direction to search for slots + /// - referenceNode: The node to use as the reference point + /// - Returns: An array of slots in the specified direction, sorted by distance (closest first) + func slots(in direction: Direction, from referenceNode: SplitTree.Node) -> [Slot] { + guard let refSlot = slots.first(where: { $0.node == referenceNode }) else { return [] } + + return switch direction { + case .left: + // Slots to the left: their right edge is at or left of reference's left edge + slots.filter { + $0.node != referenceNode && $0.bounds.maxX <= refSlot.bounds.minX + }.sorted { + (refSlot.bounds.minX - $0.bounds.maxX) < (refSlot.bounds.minX - $1.bounds.maxX) + } + + case .right: + // Slots to the right: their left edge is at or right of reference's right edge + slots.filter { + $0.node != referenceNode && $0.bounds.minX >= refSlot.bounds.maxX + }.sorted { + ($0.bounds.minX - refSlot.bounds.maxX) < ($1.bounds.minX - refSlot.bounds.maxX) + } + + case .up: + // Slots above: their bottom edge is at or above reference's top edge + slots.filter { + $0.node != referenceNode && $0.bounds.maxY <= refSlot.bounds.minY + }.sorted { + (refSlot.bounds.minY - $0.bounds.maxY) < (refSlot.bounds.minY - $1.bounds.maxY) + } + + case .down: + // Slots below: their top edge is at or below reference's bottom edge + slots.filter { + $0.node != referenceNode && $0.bounds.minY >= refSlot.bounds.maxY + }.sorted { + ($0.bounds.minY - refSlot.bounds.maxY) < ($1.bounds.minY - refSlot.bounds.maxY) + } + } + } } // MARK: SplitTree.Node Protocols diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 5558aefe3..9b65854ab 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -405,10 +405,10 @@ class BaseTerminalController: NSWindowController, switch direction { case .previous: focusDirection = .previous case .next: focusDirection = .next - case .up: focusDirection = .up - case .down: focusDirection = .down - case .left: focusDirection = .left - case .right: focusDirection = .right + case .up: focusDirection = .spatial(.up) + case .down: focusDirection = .spatial(.down) + case .left: focusDirection = .spatial(.left) + case .right: focusDirection = .spatial(.right) } // Find the node for the target surface From ea1ff438f897be634f975adb96f0ae9a7b31789b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 11:23:55 -0700 Subject: [PATCH 362/642] macos: handle split zooming --- .../Splits/TerminalSplitTreeView.swift | 7 ++++-- .../Terminal/BaseTerminalController.swift | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 3969b2e74..4a41afc42 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -5,8 +5,11 @@ struct TerminalSplitTreeView: View { let onResize: (SplitTree.Node, Double) -> Void var body: some View { - if let node = tree.root { - TerminalSplitSubtreeView(node: node, isRoot: true, onResize: onResize) + if let node = tree.zoomed ?? tree.root { + TerminalSplitSubtreeView( + node: node, + isRoot: node == tree.root, + onResize: onResize) } } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 9b65854ab..86522ac9a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -150,6 +150,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyDidFocusSplit(_:)), name: Ghostty.Notification.ghosttyFocusSplit, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidToggleSplitZoom(_:)), + name: Ghostty.Notification.didToggleSplitZoom, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -422,6 +427,24 @@ class BaseTerminalController: NSWindowController, // Move focus to the next surface Ghostty.moveFocus(to: nextSurface, from: target) } + + @objc private func ghosttyDidToggleSplitZoom(_ notification: Notification) { + // The target must be within our tree + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard let targetNode = surfaceTree2.root?.node(view: target) else { return } + + // Toggle the zoomed state + if surfaceTree2.zoomed == targetNode { + // Already zoomed, unzoom it + surfaceTree2 = SplitTree(root: surfaceTree2.root, zoomed: nil) + } else { + // Not zoomed or different node zoomed, zoom this node + surfaceTree2 = SplitTree(root: surfaceTree2.root, zoomed: targetNode) + } + + // Ensure focus stays on the target surface + Ghostty.moveFocus(to: target) + } // MARK: Local Events From 8b979d6dceae29d16d9806a171030ffb95cb6382 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 11:28:22 -0700 Subject: [PATCH 363/642] macos: handle surfaceTreeDidChange --- .../QuickTerminalController.swift | 4 ++-- .../Terminal/BaseTerminalController.swift | 21 ++++++++++--------- .../Terminal/TerminalController.swift | 16 +++++++------- .../Features/Terminal/TerminalView.swift | 7 ------- .../Ghostty/Ghostty.TerminalSplit.swift | 1 - macos/Sources/Ghostty/SurfaceView.swift | 9 -------- 6 files changed, 21 insertions(+), 37 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 1abe30da1..6de33e14f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -185,11 +185,11 @@ class QuickTerminalController: BaseTerminalController { // MARK: Base Controller Overrides - override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) { + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) // If our surface tree is nil then we animate the window out. - if (to == nil) { + if (to.isEmpty) { animateOut() } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 86522ac9a..f039e17ad 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -42,11 +42,11 @@ class BaseTerminalController: NSWindowController, } /// The surface tree for this window. - @Published var surfaceTree: Ghostty.SplitNode? = nil { - didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } - } + @Published var surfaceTree: Ghostty.SplitNode? = nil - @Published var surfaceTree2: SplitTree = .init() + @Published var surfaceTree2: SplitTree = .init() { + didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree2) } + } /// This can be set to show/hide the command palette. @Published var commandPaletteIsShowing: Bool = false @@ -174,9 +174,9 @@ class BaseTerminalController: NSWindowController, /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. - func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) { + func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { // If our surface tree becomes nil then we have no focused surface. - if (to == nil) { + if (to.isEmpty) { focusedSurface = nil } } @@ -442,8 +442,11 @@ class BaseTerminalController: NSWindowController, surfaceTree2 = SplitTree(root: surfaceTree2.root, zoomed: targetNode) } - // Ensure focus stays on the target surface - Ghostty.moveFocus(to: target) + // Ensure focus stays on the target surface. We lose focus when we do + // this so we need to grab it again. + DispatchQueue.main.async { + Ghostty.moveFocus(to: target) + } } // MARK: Local Events @@ -525,8 +528,6 @@ class BaseTerminalController: NSWindowController, self.window?.contentResizeIncrements = to } - func zoomStateDidChange(to: Bool) {} - func splitDidResize(node: SplitTree.Node, to newRatio: Double) { let resizedNode = node.resize(to: newRatio) do { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 78bb58cc8..120ac6377 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -106,15 +106,20 @@ class TerminalController: BaseTerminalController { // MARK: Base Controller Overrides - override func surfaceTreeDidChange(from: Ghostty.SplitNode?, to: Ghostty.SplitNode?) { + override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) // Whenever our surface tree changes in any way (new split, close split, etc.) // we want to invalidate our state. invalidateRestorableState() + // Update our zoom state + if let window = window as? TerminalWindow { + window.surfaceIsZoomed = to.zoomed != nil + } + // If our surface tree is now nil then we close our window. - if (to == nil) { + if (to.isEmpty) { self.window?.close() } } @@ -677,12 +682,7 @@ class TerminalController: BaseTerminalController { toolbar.titleText = to } } - - override func zoomStateDidChange(to: Bool) { - guard let window = window as? TerminalWindow else { return } - window.surfaceIsZoomed = to - } - + override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index d13de4a72..6c990d496 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -14,9 +14,6 @@ protocol TerminalViewDelegate: AnyObject { /// The cell size changed. func cellSizeDidChange(to: NSSize) - /// This is called when a split is zoomed. - func zoomStateDidChange(to: Bool) - /// Perform an action. At the time of writing this is only triggered by the command palette. func performAction(_ action: String, on: Ghostty.SurfaceView) @@ -56,7 +53,6 @@ struct TerminalView: View { // Various state values sent back up from the currently focused terminals. @FocusedValue(\.ghosttySurfaceView) private var focusedSurface @FocusedValue(\.ghosttySurfacePwd) private var surfacePwd - @FocusedValue(\.ghosttySurfaceZoomed) private var zoomedSplit @FocusedValue(\.ghosttySurfaceCellSize) private var cellSize // The pwd of the focused surface as a URL @@ -101,9 +97,6 @@ struct TerminalView: View { guard let size = newValue else { return } self.delegate?.cellSizeDidChange(to: size) } - .onChange(of: zoomedSplit) { newValue in - self.delegate?.zoomStateDidChange(to: newValue ?? false) - } } // Ignore safe area to extend up in to the titlebar region if we have the "hidden" titlebar style .ignoresSafeArea(.container, edges: ghostty.config.macosTitlebarStyle == "hidden" ? .top : []) diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift index 92528ace7..ccb7cca38 100644 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift @@ -30,7 +30,6 @@ extension Ghostty { InspectableSurface(surfaceView: surfaceView) } } - .focusedValue(\.ghosttySurfaceZoomed, zoomedSurface != nil) } } diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 1e9a4cfef..513e5af46 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -502,15 +502,6 @@ extension FocusedValues { typealias Value = String } - var ghosttySurfaceZoomed: Bool? { - get { self[FocusedGhosttySurfaceZoomed.self] } - set { self[FocusedGhosttySurfaceZoomed.self] = newValue } - } - - struct FocusedGhosttySurfaceZoomed: FocusedValueKey { - typealias Value = Bool - } - var ghosttySurfaceCellSize: OSSize? { get { self[FocusedGhosttySurfaceCellSize.self] } set { self[FocusedGhosttySurfaceCellSize.self] = newValue } From 22819f8a296e799b75904f03ddf1d4204fd9c6be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 11:40:54 -0700 Subject: [PATCH 364/642] macos: transfer doesBorderTop --- macos/Sources/Features/Splits/SplitTree.swift | 33 +++++++++++++++++++ .../Terminal/TerminalController.swift | 10 ++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index f458b5dee..78bed7120 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -774,6 +774,39 @@ extension SplitTree.Spatial { } } } + + /// Returns whether the given node borders the specified side of the spatial bounds. + /// + /// This method checks if a node's bounds touch the edge of the overall spatial area: + /// - **Up**: Node's top edge touches the top of the spatial area (Y=0) + /// - **Down**: Node's bottom edge touches the bottom of the spatial area (Y=maxY) + /// - **Left**: Node's left edge touches the left of the spatial area (X=0) + /// - **Right**: Node's right edge touches the right of the spatial area (X=maxX) + /// + /// - Parameters: + /// - side: The side of the spatial bounds to check + /// - node: The node to check if it borders the specified side + /// - Returns: True if the node borders the specified side, false otherwise + func doesBorder(side: Direction, from node: SplitTree.Node) -> Bool { + // Find the slot for this node + guard let slot = slots.first(where: { $0.node == node }) else { return false } + + // Calculate the overall bounds of all slots + let overallBounds = slots.reduce(CGRect.null) { result, slot in + result.union(slot.bounds) + } + + return switch side { + case .up: + slot.bounds.minY == overallBounds.minY + case .down: + slot.bounds.maxY == overallBounds.maxY + case .left: + slot.bounds.minX == overallBounds.minX + case .right: + slot.bounds.maxX == overallBounds.maxX + } + } } // MARK: SplitTree.Node Protocols diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 120ac6377..eb140b2a3 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -282,15 +282,19 @@ class TerminalController: BaseTerminalController { // If it does, we match the focused surface. If it doesn't, we use the app // configuration. let backgroundColor: OSColor - if let surfaceTree { - if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) { + if !surfaceTree2.isEmpty { + if let focusedSurface = focusedSurface, + let treeRoot = surfaceTree2.root, + let focusedNode = treeRoot.node(view: focusedSurface), + treeRoot.spatial().doesBorder(side: .up, from: focusedNode) { // Similar to above, an alpha component of "0" causes compositor issues, so // we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308 backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001) } else { // We don't have a focused surface or our surface doesn't border the // top. We choose to match the color of the top-left most surface. - backgroundColor = OSColor(surfaceTree.topLeft().backgroundColor ?? derivedConfig.backgroundColor) + let topLeftSurface = surfaceTree2.root?.leftmostLeaf() + backgroundColor = OSColor(topLeftSurface?.backgroundColor ?? derivedConfig.backgroundColor) } } else { backgroundColor = OSColor(self.derivedConfig.backgroundColor) From f1ed07caf441909871cde91e51a124930fa3f1f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 11:51:38 -0700 Subject: [PATCH 365/642] macos: Remove the legacy SurfaceTree --- macos/Sources/App/macOS/AppDelegate.swift | 6 ++++-- .../QuickTerminal/QuickTerminalController.swift | 17 +++++++++-------- .../Terminal/BaseTerminalController.swift | 10 +++------- .../Features/Terminal/TerminalController.swift | 8 ++++---- .../Features/Terminal/TerminalManager.swift | 3 +-- .../Features/Terminal/TerminalRestorable.swift | 17 +++-------------- 6 files changed, 24 insertions(+), 37 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 38b26f606..b5023370b 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -741,8 +741,10 @@ class AppDelegate: NSObject, func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in terminalManager.windows { - if let v = c.controller.surfaceTree?.findUUID(uuid: uuid) { - return v + for view in c.controller.surfaceTree2 { + if view.uuid == uuid { + return view + } } } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 6de33e14f..b338f8b47 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -30,11 +30,11 @@ class QuickTerminalController: BaseTerminalController { init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, baseConfig base: Ghostty.SurfaceConfiguration? = nil, - surfaceTree tree: Ghostty.SplitNode? = nil + surfaceTree2 tree2: SplitTree? = nil ) { self.position = position self.derivedConfig = DerivedConfig(ghostty.config) - super.init(ghostty, baseConfig: base, surfaceTree: tree) + super.init(ghostty, baseConfig: base, surfaceTree2: tree2) // Setup our notifications for behaviors let center = NotificationCenter.default @@ -233,13 +233,14 @@ class QuickTerminalController: BaseTerminalController { // Animate the window in animateWindowIn(window: window, from: position) - // If our surface tree is nil then we initialize a new terminal. The surface - // tree can be nil if for example we run "eixt" in the terminal and force + // If our surface tree is empty then we initialize a new terminal. The surface + // tree can be empty if for example we run "exit" in the terminal and force // animate out. - if (surfaceTree == nil) { - let leaf: Ghostty.SplitNode.Leaf = .init(ghostty.app!, baseConfig: nil) - surfaceTree = .leaf(leaf) - focusedSurface = leaf.surface + if surfaceTree2.isEmpty, + let ghostty_app = ghostty.app { + let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil) + surfaceTree2 = SplitTree(view: view) + focusedSurface = view } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index f039e17ad..028a4bece 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -41,9 +41,7 @@ class BaseTerminalController: NSWindowController, didSet { syncFocusToSurfaceTree() } } - /// The surface tree for this window. - @Published var surfaceTree: Ghostty.SplitNode? = nil - + /// The tree of splits within this terminal window. @Published var surfaceTree2: SplitTree = .init() { didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree2) } } @@ -88,7 +86,6 @@ class BaseTerminalController: NSWindowController, init(_ ghostty: Ghostty.App, baseConfig base: Ghostty.SurfaceConfiguration? = nil, - surfaceTree tree: Ghostty.SplitNode? = nil, surfaceTree2 tree2: SplitTree? = nil ) { self.ghostty = ghostty @@ -98,7 +95,6 @@ class BaseTerminalController: NSWindowController, // Initialize our initial surface. guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } - self.surfaceTree = tree ?? .leaf(.init(ghostty_app, baseConfig: base)) self.surfaceTree2 = tree2 ?? .init(view: Ghostty.SurfaceView(ghostty_app, baseConfig: base)) // Setup our notifications for behaviors @@ -171,11 +167,11 @@ class BaseTerminalController: NSWindowController, } } - /// Called when the surfaceTree variable changed. + /// Called when the surfaceTree2 variable changed. /// /// Subclasses should call super first. func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { - // If our surface tree becomes nil then we have no focused surface. + // If our surface tree becomes empty then we have no focused surface. if (to.isEmpty) { focusedSurface = nil } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index eb140b2a3..29f2710fe 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -45,7 +45,7 @@ class TerminalController: BaseTerminalController { // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - super.init(ghostty, baseConfig: base, surfaceTree: tree, surfaceTree2: tree2) + super.init(ghostty, baseConfig: base, surfaceTree2: tree2) // Setup our notifications for behaviors let center = NotificationCenter.default @@ -154,7 +154,7 @@ class TerminalController: BaseTerminalController { // If we have no surfaces in our window (is that possible?) then we update // our window appearance based on the root config. If we have surfaces, we // don't call this because the TODO - if surfaceTree == nil { + if surfaceTree2.isEmpty { syncAppearance(.init(config)) } @@ -456,10 +456,10 @@ class TerminalController: BaseTerminalController { // If we have only a single surface (no splits) and there is a default size then // we should resize to that default size. - if case let .leaf(leaf) = surfaceTree { + if case let .leaf(view) = surfaceTree2.root { // If this is our first surface then our focused surface will be nil // so we force the focused surface to the leaf. - focusedSurface = leaf.surface + focusedSurface = view if let defaultSize { window.setFrame(defaultSize, display: true) diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 475b70ac9..28b969d36 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -197,10 +197,9 @@ class TerminalManager { /// Creates a window controller, adds it to our managed list, and returns it. func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree tree: Ghostty.SplitNode? = nil, withSurfaceTree2 tree2: SplitTree? = nil) -> TerminalController { // Initialize our controller to load the window - let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree: tree, withSurfaceTree2: tree2) + let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree2: tree2) // Create a listener for when the window is closed so we can remove it. let pubClose = NotificationCenter.default.publisher( diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 5531494a5..6d5289955 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -7,12 +7,10 @@ class TerminalRestorableState: Codable { static let version: Int = 3 let focusedSurface: String? - let surfaceTree: Ghostty.SplitNode? - let surfaceTree2: SplitTree? + let surfaceTree2: SplitTree init(from controller: TerminalController) { self.focusedSurface = controller.focusedSurface?.uuid.uuidString - self.surfaceTree = controller.surfaceTree self.surfaceTree2 = controller.surfaceTree2 } @@ -28,7 +26,6 @@ class TerminalRestorableState: Codable { return nil } - self.surfaceTree = v.value.surfaceTree self.surfaceTree2 = v.value.surfaceTree2 self.focusedSurface = v.value.focusedSurface } @@ -87,7 +84,6 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // createWindow so that AppKit can place the window wherever it should // be. let c = appDelegate.terminalManager.createWindow( - withSurfaceTree: state.surfaceTree, withSurfaceTree2: state.surfaceTree2 ) guard let window = c.window else { @@ -96,10 +92,8 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { } // Setup our restored state on the controller - // First try to find the focused surface in surfaceTree2 - if let focusedStr = state.focusedSurface, - let focusedUUID = UUID(uuidString: focusedStr) { - // Try surfaceTree2 first + // Find the focused surface in surfaceTree2 + if let focusedStr = state.focusedSurface { var foundView: Ghostty.SurfaceView? for view in c.surfaceTree2 { if view.uuid.uuidString == focusedStr { @@ -108,11 +102,6 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { } } - // Fall back to surfaceTree if not found - if foundView == nil { - foundView = c.surfaceTree?.findUUID(uuid: focusedUUID) - } - if let view = foundView { c.focusedSurface = view restoreFocus(to: view, inWindow: window) From 77458ef3089214e8a073671fb6a56de565545033 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 11:53:19 -0700 Subject: [PATCH 366/642] macos: rename surfaceTree2 to surfaceTree --- macos/Sources/App/macOS/AppDelegate.swift | 2 +- .../QuickTerminalController.swift | 8 +-- .../Terminal/BaseTerminalController.swift | 57 +++++++++---------- .../Terminal/TerminalController.swift | 29 +++++----- .../Features/Terminal/TerminalManager.swift | 6 +- .../Terminal/TerminalRestorable.swift | 12 ++-- .../Features/Terminal/TerminalView.swift | 4 +- macos/Sources/Ghostty/Ghostty.App.swift | 2 +- 8 files changed, 59 insertions(+), 61 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index b5023370b..c6816d50c 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -741,7 +741,7 @@ class AppDelegate: NSObject, func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { for c in terminalManager.windows { - for view in c.controller.surfaceTree2 { + for view in c.controller.surfaceTree { if view.uuid == uuid { return view } diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index b338f8b47..0dcfce204 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -30,11 +30,11 @@ class QuickTerminalController: BaseTerminalController { init(_ ghostty: Ghostty.App, position: QuickTerminalPosition = .top, baseConfig base: Ghostty.SurfaceConfiguration? = nil, - surfaceTree2 tree2: SplitTree? = nil + surfaceTree tree: SplitTree? = nil ) { self.position = position self.derivedConfig = DerivedConfig(ghostty.config) - super.init(ghostty, baseConfig: base, surfaceTree2: tree2) + super.init(ghostty, baseConfig: base, surfaceTree: tree) // Setup our notifications for behaviors let center = NotificationCenter.default @@ -236,10 +236,10 @@ class QuickTerminalController: BaseTerminalController { // If our surface tree is empty then we initialize a new terminal. The surface // tree can be empty if for example we run "exit" in the terminal and force // animate out. - if surfaceTree2.isEmpty, + if surfaceTree.isEmpty, let ghostty_app = ghostty.app { let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil) - surfaceTree2 = SplitTree(view: view) + surfaceTree = SplitTree(view: view) focusedSurface = view } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 028a4bece..3cc5843a5 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -42,8 +42,8 @@ class BaseTerminalController: NSWindowController, } /// The tree of splits within this terminal window. - @Published var surfaceTree2: SplitTree = .init() { - didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree2) } + @Published var surfaceTree: SplitTree = .init() { + didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) } } /// This can be set to show/hide the command palette. @@ -86,7 +86,7 @@ class BaseTerminalController: NSWindowController, init(_ ghostty: Ghostty.App, baseConfig base: Ghostty.SurfaceConfiguration? = nil, - surfaceTree2 tree2: SplitTree? = nil + surfaceTree tree: SplitTree? = nil ) { self.ghostty = ghostty self.derivedConfig = DerivedConfig(ghostty.config) @@ -95,7 +95,7 @@ class BaseTerminalController: NSWindowController, // Initialize our initial surface. guard let ghostty_app = ghostty.app else { preconditionFailure("app must be loaded") } - self.surfaceTree2 = tree2 ?? .init(view: Ghostty.SurfaceView(ghostty_app, baseConfig: base)) + self.surfaceTree = tree ?? .init(view: Ghostty.SurfaceView(ghostty_app, baseConfig: base)) // Setup our notifications for behaviors let center = NotificationCenter.default @@ -167,7 +167,7 @@ class BaseTerminalController: NSWindowController, } } - /// Called when the surfaceTree2 variable changed. + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { @@ -180,7 +180,7 @@ class BaseTerminalController: NSWindowController, /// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about /// what surface is focused. This must be called whenever a surface OR window changes focus. func syncFocusToSurfaceTree() { - for surfaceView in surfaceTree2 { + for surfaceView in surfaceTree { // Our focus state requires that this window is key and our currently // focused surface is the surface in this view. let focused: Bool = (window?.isKeyWindow ?? false) && @@ -296,21 +296,21 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyCommandPaletteDidToggle(_ notification: Notification) { guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.contains(surfaceView) else { return } + guard surfaceTree.contains(surfaceView) else { return } toggleCommandPalette(nil) } @objc private func ghosttyMaximizeDidToggle(_ notification: Notification) { guard let window else { return } guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.contains(surfaceView) else { return } + guard surfaceTree.contains(surfaceView) else { return } window.zoom(nil) } @objc private func ghosttyDidCloseSurface(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard let node = surfaceTree2.root?.node(view: target) else { return } + guard let node = surfaceTree.root?.node(view: target) else { return } // TODO: fix focus @@ -323,7 +323,7 @@ class BaseTerminalController: NSWindowController, // If the child process is not alive, then we exit immediately guard processAlive else { - surfaceTree2 = surfaceTree2.remove(node) + surfaceTree = surfaceTree.remove(node) return } @@ -337,7 +337,7 @@ class BaseTerminalController: NSWindowController, informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." ) { [weak self] in if let self { - self.surfaceTree2 = self.surfaceTree2.remove(node) + self.surfaceTree = self.surfaceTree.remove(node) } } } @@ -345,7 +345,7 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidNewSplit(_ notification: Notification) { // The target must be within our tree guard let oldView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.root?.node(view: oldView) != nil else { return } + guard surfaceTree.root?.node(view: oldView) != nil else { return } // Notification must contain our base config let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] @@ -369,7 +369,7 @@ class BaseTerminalController: NSWindowController, // Do the split do { - surfaceTree2 = try surfaceTree2.insert(view: newView, at: oldView, direction: splitDirection) + surfaceTree = try surfaceTree.insert(view: newView, at: oldView, direction: splitDirection) } catch { // If splitting fails for any reason (it should not), then we just log // and return. The new view we created will be deinitialized and its @@ -386,16 +386,16 @@ class BaseTerminalController: NSWindowController, guard let target = notification.object as? Ghostty.SurfaceView else { return } // Check if target surface is in current controller's tree - guard surfaceTree2.contains(target) else { return } + guard surfaceTree.contains(target) else { return } // Equalize the splits - surfaceTree2 = surfaceTree2.equalize() + surfaceTree = surfaceTree.equalize() } @objc private func ghosttyDidFocusSplit(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.root?.node(view: target) != nil else { return } + guard surfaceTree.root?.node(view: target) != nil else { return } // Get the direction from the notification guard let directionAny = notification.userInfo?[Ghostty.Notification.SplitDirectionKey] else { return } @@ -413,10 +413,10 @@ class BaseTerminalController: NSWindowController, } // Find the node for the target surface - guard let targetNode = surfaceTree2.root?.node(view: target) else { return } + guard let targetNode = surfaceTree.root?.node(view: target) else { return } // Find the next surface to focus - guard let nextSurface = surfaceTree2.focusTarget(for: focusDirection, from: targetNode) else { + guard let nextSurface = surfaceTree.focusTarget(for: focusDirection, from: targetNode) else { return } @@ -427,15 +427,15 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidToggleSplitZoom(_ notification: Notification) { // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard let targetNode = surfaceTree2.root?.node(view: target) else { return } + guard let targetNode = surfaceTree.root?.node(view: target) else { return } // Toggle the zoomed state - if surfaceTree2.zoomed == targetNode { + if surfaceTree.zoomed == targetNode { // Already zoomed, unzoom it - surfaceTree2 = SplitTree(root: surfaceTree2.root, zoomed: nil) + surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil) } else { // Not zoomed or different node zoomed, zoom this node - surfaceTree2 = SplitTree(root: surfaceTree2.root, zoomed: targetNode) + surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode) } // Ensure focus stays on the target surface. We lose focus when we do @@ -458,8 +458,7 @@ class BaseTerminalController: NSWindowController, } private func localEventFlagsChanged(_ event: NSEvent) -> NSEvent? { - // Also update surfaceTree2 - var surfaces: [Ghostty.SurfaceView] = surfaceTree2.map { $0 } + var surfaces: [Ghostty.SurfaceView] = surfaceTree.map { $0 } // If we're the main window receiving key input, then we want to avoid // calling this on our focused surface because that'll trigger a double @@ -489,7 +488,7 @@ class BaseTerminalController: NSWindowController, // want to care if the surface is in the tree so we don't listen to titles of // closed surfaces. if let titleSurface = focusedSurface ?? lastFocusedSurface, - surfaceTree2.contains(titleSurface) { + surfaceTree.contains(titleSurface) { // If we have a surface, we want to listen for title changes. titleSurface.$title .sink { [weak self] in self?.titleDidChange(to: $0) } @@ -527,7 +526,7 @@ class BaseTerminalController: NSWindowController, func splitDidResize(node: SplitTree.Node, to newRatio: Double) { let resizedNode = node.resize(to: newRatio) do { - surfaceTree2 = try surfaceTree2.replace(node: node, with: resizedNode) + surfaceTree = try surfaceTree.replace(node: node, with: resizedNode) } catch { // TODO: log return @@ -687,13 +686,13 @@ class BaseTerminalController: NSWindowController, guard let window = self.window else { return true } // If we have no surfaces, close. - if surfaceTree2.isEmpty { return true } + if surfaceTree.isEmpty { return true } // If we already have an alert, continue with it guard alert == nil else { return false } // If our surfaces don't require confirmation, close. - if !surfaceTree2.contains(where: { $0.needsConfirmQuit }) { return true } + if !surfaceTree.contains(where: { $0.needsConfirmQuit }) { return true } // We require confirmation, so show an alert as long as we aren't already. confirmClose( @@ -729,7 +728,7 @@ class BaseTerminalController: NSWindowController, func windowDidChangeOcclusionState(_ notification: Notification) { let visible = self.window?.occlusionState.contains(.visible) ?? false - for view in surfaceTree2 { + for view in surfaceTree { if let surface = view.surface { ghostty_surface_set_occlusion(surface, visible) } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 29f2710fe..42eb7eca4 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -32,8 +32,7 @@ class TerminalController: BaseTerminalController { init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree tree: Ghostty.SplitNode? = nil, - withSurfaceTree2 tree2: SplitTree? = nil + withSurfaceTree tree: SplitTree? = nil ) { // The window we manage is not restorable if we've specified a command // to execute. We do this because the restored window is meaningless at the @@ -45,7 +44,7 @@ class TerminalController: BaseTerminalController { // Setup our initial derived config based on the current app config self.derivedConfig = DerivedConfig(ghostty.config) - super.init(ghostty, baseConfig: base, surfaceTree2: tree2) + super.init(ghostty, baseConfig: base, surfaceTree: tree) // Setup our notifications for behaviors let center = NotificationCenter.default @@ -154,7 +153,7 @@ class TerminalController: BaseTerminalController { // If we have no surfaces in our window (is that possible?) then we update // our window appearance based on the root config. If we have surfaces, we // don't call this because the TODO - if surfaceTree2.isEmpty { + if surfaceTree.isEmpty { syncAppearance(.init(config)) } @@ -164,7 +163,7 @@ class TerminalController: BaseTerminalController { // This is a surface-level config update. If we have the surface, we // update our appearance based on it. guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.contains(surfaceView) else { return } + guard surfaceTree.contains(surfaceView) else { return } // We can't use surfaceView.derivedConfig because it may not be updated // yet since it also responds to notifications. @@ -239,7 +238,7 @@ class TerminalController: BaseTerminalController { window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree2.zoomed != nil + window.surfaceIsZoomed = surfaceTree.zoomed != nil // If our window is not visible, then we do nothing. Some things such as blurring // have no effect if the window is not visible. Ultimately, we'll have this called @@ -282,9 +281,9 @@ class TerminalController: BaseTerminalController { // If it does, we match the focused surface. If it doesn't, we use the app // configuration. let backgroundColor: OSColor - if !surfaceTree2.isEmpty { + if !surfaceTree.isEmpty { if let focusedSurface = focusedSurface, - let treeRoot = surfaceTree2.root, + let treeRoot = surfaceTree.root, let focusedNode = treeRoot.node(view: focusedSurface), treeRoot.spatial().doesBorder(side: .up, from: focusedNode) { // Similar to above, an alpha component of "0" causes compositor issues, so @@ -293,7 +292,7 @@ class TerminalController: BaseTerminalController { } else { // We don't have a focused surface or our surface doesn't border the // top. We choose to match the color of the top-left most surface. - let topLeftSurface = surfaceTree2.root?.leftmostLeaf() + let topLeftSurface = surfaceTree.root?.leftmostLeaf() backgroundColor = OSColor(topLeftSurface?.backgroundColor ?? derivedConfig.backgroundColor) } } else { @@ -456,7 +455,7 @@ class TerminalController: BaseTerminalController { // If we have only a single surface (no splits) and there is a default size then // we should resize to that default size. - if case let .leaf(view) = surfaceTree2.root { + if case let .leaf(view) = surfaceTree.root { // If this is our first surface then our focused surface will be nil // so we force the focused surface to the leaf. focusedSurface = view @@ -604,7 +603,7 @@ class TerminalController: BaseTerminalController { return } - if surfaceTree2.contains(where: { $0.needsConfirmQuit }) { + if surfaceTree.contains(where: { $0.needsConfirmQuit }) { confirmClose( messageText: "Close Tab?", informativeText: "The terminal still has a running process. If you close the tab the process will be killed." @@ -641,7 +640,7 @@ class TerminalController: BaseTerminalController { guard let controller = tabWindow.windowController as? TerminalController else { return false } - return controller.surfaceTree2.contains(where: { $0.needsConfirmQuit }) + return controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) } // If none need confirmation then we can just close all the windows. @@ -819,19 +818,19 @@ class TerminalController: BaseTerminalController { @objc private func onCloseTab(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.contains(target) else { return } + guard surfaceTree.contains(target) else { return } closeTab(self) } @objc private func onCloseWindow(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.contains(target) else { return } + guard surfaceTree.contains(target) else { return } closeWindow(self) } @objc private func onResetWindowSize(notification: SwiftUI.Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - guard surfaceTree2.contains(target) else { return } + guard surfaceTree.contains(target) else { return } returnToDefaultSize(nil) } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 28b969d36..805ae6e93 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -197,9 +197,9 @@ class TerminalManager { /// Creates a window controller, adds it to our managed list, and returns it. func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree2 tree2: SplitTree? = nil) -> TerminalController { + withSurfaceTree tree: SplitTree? = nil) -> TerminalController { // Initialize our controller to load the window - let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree2: tree2) + let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree: tree) // Create a listener for when the window is closed so we can remove it. let pubClose = NotificationCenter.default.publisher( @@ -268,7 +268,7 @@ class TerminalManager { func closeAllWindows() { var needsConfirm: Bool = false for w in self.windows { - if w.controller.surfaceTree2.contains(where: { $0.needsConfirmQuit }) { + if w.controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) { needsConfirm = true break } diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 6d5289955..5229dc46e 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -7,11 +7,11 @@ class TerminalRestorableState: Codable { static let version: Int = 3 let focusedSurface: String? - let surfaceTree2: SplitTree + let surfaceTree: SplitTree init(from controller: TerminalController) { self.focusedSurface = controller.focusedSurface?.uuid.uuidString - self.surfaceTree2 = controller.surfaceTree2 + self.surfaceTree = controller.surfaceTree } init?(coder aDecoder: NSCoder) { @@ -26,7 +26,7 @@ class TerminalRestorableState: Codable { return nil } - self.surfaceTree2 = v.value.surfaceTree2 + self.surfaceTree = v.value.surfaceTree self.focusedSurface = v.value.focusedSurface } @@ -84,7 +84,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // createWindow so that AppKit can place the window wherever it should // be. let c = appDelegate.terminalManager.createWindow( - withSurfaceTree2: state.surfaceTree2 + withSurfaceTree: state.surfaceTree ) guard let window = c.window else { completionHandler(nil, TerminalRestoreError.windowDidNotLoad) @@ -92,10 +92,10 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { } // Setup our restored state on the controller - // Find the focused surface in surfaceTree2 + // Find the focused surface in surfaceTree if let focusedStr = state.focusedSurface { var foundView: Ghostty.SurfaceView? - for view in c.surfaceTree2 { + for view in c.surfaceTree { if view.uuid.uuidString == focusedStr { foundView = view break diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index 6c990d496..cb6f11bce 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -27,7 +27,7 @@ protocol TerminalViewDelegate: AnyObject { protocol TerminalViewModel: ObservableObject { /// The tree of terminal surfaces (splits) within the view. This is mutated by TerminalView /// and children. This should be @Published. - var surfaceTree2: SplitTree { get set } + var surfaceTree: SplitTree { get set } /// The command palette state. var commandPaletteIsShowing: Bool { get set } @@ -77,7 +77,7 @@ struct TerminalView: View { } TerminalSplitTreeView( - tree: viewModel.surfaceTree2, + tree: viewModel.surfaceTree, onResize: { delegate?.splitDidResize(node: $0, to: $1) }) .environmentObject(ghostty) .focused($focused) diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 08c284b04..4a9dc0ea6 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -921,7 +921,7 @@ extension Ghostty { // we should only be returning true if we actually performed the action, // but this handles the most common case of caring about goto_split performability // which is the no-split case. - guard controller.surfaceTree2.isSplit else { return false } + guard controller.surfaceTree.isSplit else { return false } NotificationCenter.default.post( name: Notification.ghosttyFocusSplit, From 6c97e4a59a22d8272acaf4147b0b093ef4826ff7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 12:01:50 -0700 Subject: [PATCH 367/642] macos: fix focus after closing splits --- .../Terminal/BaseTerminalController.swift | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 3cc5843a5..dfc8a2221 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -234,6 +234,39 @@ class BaseTerminalController: NSWindowController, self.alert = alert } + // MARK: Focus Management + + /// Find the next surface to focus when a node is being closed. + /// Goes to previous split unless we're the leftmost leaf, then goes to next. + private func findNextFocusTargetAfterClosing(node: SplitTree.Node) -> Ghostty.SurfaceView? { + guard let root = surfaceTree.root else { return nil } + + // If we're the leftmost, then we move to the next surface after closing. + // Otherwise, we move to the previous. + if root.leftmostLeaf() == node.leftmostLeaf() { + return surfaceTree.focusTarget(for: .next, from: node) + } else { + return surfaceTree.focusTarget(for: .previous, from: node) + } + } + + /// Remove a node from the surface tree and move focus appropriately. + private func removeSurfaceAndMoveFocus(_ node: SplitTree.Node) { + let nextTarget = findNextFocusTargetAfterClosing(node: node) + let oldFocused = focusedSurface + let focused = node.contains { $0 == focusedSurface } + + // Remove the node from the tree + surfaceTree = surfaceTree.remove(node) + + // Move focus if the closed surface was focused and we have a next target + if let nextTarget, focused { + DispatchQueue.main.async { + Ghostty.moveFocus(to: nextTarget, from: oldFocused) + } + } + } + // MARK: Notifications @objc private func didChangeScreenParametersNotification(_ notification: Notification) { @@ -312,8 +345,6 @@ class BaseTerminalController: NSWindowController, guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let node = surfaceTree.root?.node(view: target) else { return } - // TODO: fix focus - var processAlive = false if let valueAny = notification.userInfo?["process_alive"] { if let value = valueAny as? Bool { @@ -323,7 +354,7 @@ class BaseTerminalController: NSWindowController, // If the child process is not alive, then we exit immediately guard processAlive else { - surfaceTree = surfaceTree.remove(node) + removeSurfaceAndMoveFocus(node) return } @@ -337,7 +368,7 @@ class BaseTerminalController: NSWindowController, informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." ) { [weak self] in if let self { - self.surfaceTree = self.surfaceTree.remove(node) + self.removeSurfaceAndMoveFocus(node) } } } From 19a9156ae1d562250b42023d1914f2482805cec4 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 12:11:33 -0700 Subject: [PATCH 368/642] macos: address remaining todos --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 4 ++-- macos/Sources/Features/Terminal/TerminalController.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index dfc8a2221..be4b59e7a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -405,7 +405,7 @@ class BaseTerminalController: NSWindowController, // If splitting fails for any reason (it should not), then we just log // and return. The new view we created will be deinitialized and its // no big deal. - // TODO: log + Ghostty.logger.warning("failed to insert split: \(error)") return } @@ -559,7 +559,7 @@ class BaseTerminalController: NSWindowController, do { surfaceTree = try surfaceTree.replace(node: node, with: resizedNode) } catch { - // TODO: log + Ghostty.logger.warning("failed to replace node during split resize: \(error)") return } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 42eb7eca4..9c1e82b69 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -152,7 +152,7 @@ class TerminalController: BaseTerminalController { // If we have no surfaces in our window (is that possible?) then we update // our window appearance based on the root config. If we have surfaces, we - // don't call this because the TODO + // don't call this because focused surface changes will trigger appearance updates. if surfaceTree.isEmpty { syncAppearance(.init(config)) } From 5299f10e13ec73e47d0e67843e9e56bf560bdbd5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 12:17:50 -0700 Subject: [PATCH 369/642] macos: unzoom on new split and focus change --- macos/Sources/Features/Splits/SplitTree.swift | 3 ++- macos/Sources/Features/Terminal/BaseTerminalController.swift | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 78bed7120..d47e51bec 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -108,11 +108,12 @@ extension SplitTree { } /// Insert a new view at the given view point by creating a split in the given direction. + /// This will always reset the zoomed state of the tree. func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { guard let root else { throw SplitError.viewNotFound } return .init( root: try root.insert(view: view, at: at, direction: direction), - zoomed: zoomed) + zoomed: nil) } /// Remove a node from the tree. If the node being removed is part of a split, diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index be4b59e7a..ba57fbf70 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -451,6 +451,11 @@ class BaseTerminalController: NSWindowController, return } + // Remove the zoomed state for this surface tree. + if surfaceTree.zoomed != nil { + surfaceTree = .init(root: surfaceTree.root, zoomed: nil) + } + // Move focus to the next surface Ghostty.moveFocus(to: nextSurface, from: target) } From 69c3c359cb65cf0e09f8a3c8e7013b02f23c6005 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 12:53:31 -0700 Subject: [PATCH 370/642] macos: resize split keybind handling --- macos/Sources/Features/Splits/SplitTree.swift | 176 +++++++++++++++++- .../Terminal/BaseTerminalController.swift | 37 ++++ 2 files changed, 206 insertions(+), 7 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index d47e51bec..ab4b387a4 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -213,6 +213,108 @@ extension SplitTree { let newRoot = root.equalize() return .init(root: newRoot, zoomed: zoomed) } + + /// Resize a node in the tree by the given pixel amount in the specified direction. + /// + /// This method adjusts the split ratios of the tree to accommodate the requested resize + /// operation. For up/down resizing, it finds the nearest parent vertical split and adjusts + /// its ratio. For left/right resizing, it finds the nearest parent horizontal split. + /// The bounds parameter is used to construct the spatial tree representation which is + /// needed to calculate the current pixel dimensions. + /// + /// This will always reset the zoomed state. + /// + /// - Parameters: + /// - node: The node to resize + /// - by: The number of pixels to resize by + /// - direction: The direction to resize in (up, down, left, right) + /// - bounds: The bounds used to construct the spatial tree representation + /// - Returns: A new SplitTree with the adjusted split ratios + /// - Throws: SplitError.viewNotFound if the node is not found in the tree or no suitable parent split exists + func resize(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self { + guard let root else { throw SplitError.viewNotFound } + + // Find the path to the target node + guard let path = root.path(to: node) else { + throw SplitError.viewNotFound + } + + // Determine which type of split we need to find based on resize direction + let targetSplitDirection: Direction = switch direction { + case .up, .down: .vertical + case .left, .right: .horizontal + } + + // Find the nearest parent split of the correct type by walking up the path + var splitPath: Path? + var splitNode: Node? + + for i in stride(from: path.path.count - 1, through: 0, by: -1) { + let parentPath = Path(path: Array(path.path.prefix(i))) + if let parent = root.node(at: parentPath), case .split(let split) = parent { + if split.direction == targetSplitDirection { + splitPath = parentPath + splitNode = parent + break + } + } + } + + guard let splitPath = splitPath, + let splitNode = splitNode, + case .split(let split) = splitNode else { + throw SplitError.viewNotFound + } + + // Get current spatial representation to calculate pixel dimensions + let spatial = root.spatial(within: bounds.size) + guard let splitSlot = spatial.slots.first(where: { $0.node == splitNode }) else { + throw SplitError.viewNotFound + } + + // Calculate the new ratio based on pixel change + let pixelOffset = Double(pixels) + let newRatio: Double + + switch (split.direction, direction) { + case (.horizontal, .left): + // Moving left boundary: decrease left side + newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio - (pixelOffset / splitSlot.bounds.width))) + case (.horizontal, .right): + // Moving right boundary: increase left side + newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio + (pixelOffset / splitSlot.bounds.width))) + case (.vertical, .up): + // Moving top boundary: decrease top side + newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio - (pixelOffset / splitSlot.bounds.height))) + case (.vertical, .down): + // Moving bottom boundary: increase top side + newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio + (pixelOffset / splitSlot.bounds.height))) + default: + // Direction doesn't match split type - shouldn't happen due to earlier logic + throw SplitError.viewNotFound + } + + // Create new split with adjusted ratio + let newSplit = Node.Split( + direction: split.direction, + ratio: newRatio, + left: split.left, + right: split.right + ) + + // Replace the split node with the new one + let newRoot = try root.replaceNode(at: splitPath, with: .split(newSplit)) + return .init(root: newRoot, zoomed: nil) + } + + /// Returns the total bounds of the split hierarchy using NSView bounds. + /// Ignores x/y coordinates and assumes views are laid out in a perfect grid. + /// Also ignores any possible padding between views. + /// - Returns: The total width and height needed to contain all views + func viewBounds() -> CGSize { + guard let root else { return .zero } + return root.viewBounds() + } } // MARK: SplitTree.Node @@ -277,6 +379,27 @@ extension SplitTree.Node { return search(self) ? Path(path: components) : nil } + + /// Returns the node at the given path from this node as root. + func node(at path: Path) -> Node? { + if path.isEmpty { + return self + } + + guard case .split(let split) = self else { + return nil + } + + let component = path.path[0] + let remainingPath = Path(path: Array(path.path.dropFirst())) + + switch component { + case .left: + return split.left.node(at: remainingPath) + case .right: + return split.right.node(at: remainingPath) + } + } /// Inserts a new view into the split tree by creating a split at the location of an existing view. /// @@ -541,6 +664,36 @@ extension SplitTree.Node { split.right.calculateViewBounds(in: rightBounds) } } + + /// Returns the total bounds of this subtree using NSView bounds. + /// Ignores x/y coordinates and assumes views are laid out in a perfect grid. + /// - Returns: The total width and height needed to contain all views in this subtree + func viewBounds() -> CGSize { + switch self { + case .leaf(let view): + return view.bounds.size + + case .split(let split): + let leftBounds = split.left.viewBounds() + let rightBounds = split.right.viewBounds() + + switch split.direction { + case .horizontal: + // Horizontal split: width is sum, height is max + return CGSize( + width: leftBounds.width + rightBounds.width, + height: Swift.max(leftBounds.height, rightBounds.height) + ) + + case .vertical: + // Vertical split: height is sum, width is max + return CGSize( + width: Swift.max(leftBounds.width, rightBounds.width), + height: leftBounds.height + rightBounds.height + ) + } + } + } } // MARK: SplitTree.Node Spatial @@ -575,16 +728,25 @@ extension SplitTree.Node { /// // - Node bounds based on actual split ratios /// ``` /// + /// - Parameter bounds: Optional size constraints for the spatial representation. If nil, uses artificial dimensions based + /// on grid layout /// - Returns: A `Spatial` struct containing all slots with their calculated bounds - func spatial() -> SplitTree.Spatial { - // First, calculate the total dimensions needed - let dimensions = dimensions() + func spatial(within bounds: CGSize? = nil) -> SplitTree.Spatial { + // If we're not given bounds, we use artificial dimensions based on + // the total width/height in columns/rows. + let width: Double + let height: Double + if let bounds { + width = bounds.width + height = bounds.height + } else { + let (w, h) = self.dimensions() + width = Double(w) + height = Double(h) + } // Calculate slots with relative bounds - let slots = spatialSlots( - in: CGRect(x: 0, y: 0, width: Double(dimensions.width), height: Double(dimensions.height)) - ) - + let slots = spatialSlots(in: CGRect(x: 0, y: 0, width: width, height: height)) return SplitTree.Spatial(slots: slots) } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ba57fbf70..5e2777195 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -151,6 +151,11 @@ class BaseTerminalController: NSWindowController, selector: #selector(ghosttyDidToggleSplitZoom(_:)), name: Ghostty.Notification.didToggleSplitZoom, object: nil) + center.addObserver( + self, + selector: #selector(ghosttyDidResizeSplit(_:)), + name: Ghostty.Notification.didResizeSplit, + object: nil) // Listen for local events that we need to know of outside of // single surface handlers. @@ -480,6 +485,38 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: target) } } + + @objc private func ghosttyDidResizeSplit(_ notification: Notification) { + // The target must be within our tree + guard let target = notification.object as? Ghostty.SurfaceView else { return } + guard let targetNode = surfaceTree.root?.node(view: target) else { return } + + // Extract direction and amount from notification + guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return } + guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return } + + guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return } + guard let amount = amountAny as? UInt16 else { return } + + // Convert Ghostty.SplitResizeDirection to SplitTree.Spatial.Direction + let spatialDirection: SplitTree.Spatial.Direction + switch direction { + case .up: spatialDirection = .up + case .down: spatialDirection = .down + case .left: spatialDirection = .left + case .right: spatialDirection = .right + } + + // Use viewBounds for the spatial calculation bounds + let bounds = CGRect(origin: .zero, size: surfaceTree.viewBounds()) + + // Perform the resize using the new SplitTree resize method + do { + surfaceTree = try surfaceTree.resize(node: targetNode, by: amount, in: spatialDirection, with: bounds) + } catch { + Ghostty.logger.warning("failed to resize split: \(error)") + } + } // MARK: Local Events From 9474092f77164d5e85be395bd1b614a257923399 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 13:20:14 -0700 Subject: [PATCH 371/642] macos: remove the old split implementation --- macos/Ghostty.xcodeproj/project.pbxproj | 8 - macos/Sources/Ghostty/Ghostty.SplitNode.swift | 481 ------------------ .../Ghostty/Ghostty.TerminalSplit.swift | 468 ----------------- macos/Sources/Ghostty/SurfaceView.swift | 54 ++ 4 files changed, 54 insertions(+), 957 deletions(-) delete mode 100644 macos/Sources/Ghostty/Ghostty.SplitNode.swift delete mode 100644 macos/Sources/Ghostty/Ghostty.TerminalSplit.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 8c73d55c5..bb9e860f3 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -70,8 +70,6 @@ A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309B2AEE1C9E00D64628 /* TerminalController.swift */; }; A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309D2AEE1D6C00D64628 /* TerminalView.swift */; }; A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309F2AEF6AEB00D64628 /* TerminalManager.swift */; }; - A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */; }; - A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */; }; A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CE62C33060F00C57AD3 /* man in Resources */ = {isa = PBXBuildFile; fileRef = A5985CE52C33060F00C57AD3 /* man */; }; @@ -178,8 +176,6 @@ A596309B2AEE1C9E00D64628 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = ""; }; A596309D2AEE1D6C00D64628 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; A596309F2AEF6AEB00D64628 /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = ""; }; - A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.TerminalSplit.swift; sourceTree = ""; }; - A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.SplitNode.swift; sourceTree = ""; }; A5985CD62C320C4500C57AD3 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; A5985CE52C33060F00C57AD3 /* man */ = {isa = PBXFileReference; lastKnownFileType = folder; name = man; path = "../zig-out/share/man"; sourceTree = ""; }; A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAppearance+Extension.swift"; sourceTree = ""; }; @@ -409,8 +405,6 @@ A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, - A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */, - A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */, A55685DF29A03A9F004303CE /* AppError.swift */, A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */, A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */, @@ -690,7 +684,6 @@ buildActionMask = 2147483647; files = ( A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */, - A59630A42AF059BB00D64628 /* Ghostty.SplitNode.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, @@ -737,7 +730,6 @@ A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, - A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, diff --git a/macos/Sources/Ghostty/Ghostty.SplitNode.swift b/macos/Sources/Ghostty/Ghostty.SplitNode.swift deleted file mode 100644 index ff60e7c56..000000000 --- a/macos/Sources/Ghostty/Ghostty.SplitNode.swift +++ /dev/null @@ -1,481 +0,0 @@ -import SwiftUI -import Combine -import GhosttyKit - -extension Ghostty { - /// This enum represents the possible states that a node in the split tree can be in. It is either: - /// - /// - noSplit - This is an unsplit, single pane. This contains only a "leaf" which has a single - /// terminal surface to render. - /// - horizontal/vertical - This is split into the horizontal or vertical direction. This contains a - /// "container" which has a recursive top/left SplitNode and bottom/right SplitNode. These - /// values can further be split infinitely. - /// - enum SplitNode: Equatable, Hashable, Codable { - case leaf(Leaf) - case split(Container) - - /// The parent of this node. - var parent: Container? { - get { - switch (self) { - case .leaf(let leaf): - return leaf.parent - - case .split(let container): - return container.parent - } - } - - set { - switch (self) { - case .leaf(let leaf): - leaf.parent = newValue - - case .split(let container): - container.parent = newValue - } - } - } - - /// Returns true if the tree is split. - var isSplit: Bool { - return if case .leaf = self { - false - } else { - true - } - } - - func topLeft() -> SurfaceView { - switch (self) { - case .leaf(let leaf): - return leaf.surface - - case .split(let container): - return container.topLeft.topLeft() - } - } - - /// Returns the view that would prefer receiving focus in this tree. This is always the - /// top-left-most view. This is used when creating a split or closing a split to find the - /// next view to send focus to. - func preferredFocus(_ direction: SplitFocusDirection = .up) -> SurfaceView { - let container: Container - switch (self) { - case .leaf(let leaf): - // noSplit is easy because there is only one thing to focus - return leaf.surface - - case .split(let c): - container = c - } - - let node: SplitNode - switch (direction) { - case .previous, .up, .left: - node = container.bottomRight - - case .next, .down, .right: - node = container.topLeft - } - - return node.preferredFocus(direction) - } - - /// When direction is either next or previous, return the first or last - /// leaf. This can be used when the focus needs to move to a leaf even - /// after hitting the bottom-right-most or top-left-most surface. - /// When the direction is not next or previous (such as top, bottom, - /// left, right), it will be ignored and no leaf will be returned. - func firstOrLast(_ direction: SplitFocusDirection) -> Leaf? { - // If there is no parent, simply ignore. - guard let root = self.parent?.rootContainer() else { return nil } - - switch (direction) { - case .next: - return root.firstLeaf() - case .previous: - return root.lastLeaf() - default: - return nil - } - } - - /// Returns true if any surface in the split stack requires quit confirmation. - func needsConfirmQuit() -> Bool { - switch (self) { - case .leaf(let leaf): - return leaf.surface.needsConfirmQuit - - case .split(let container): - return container.topLeft.needsConfirmQuit() || - container.bottomRight.needsConfirmQuit() - } - } - - /// Returns true if the split tree contains the given view. - func contains(view: SurfaceView) -> Bool { - return leaf(for: view) != nil - } - - /// Find a surface view by UUID. - func findUUID(uuid: UUID) -> SurfaceView? { - switch (self) { - case .leaf(let leaf): - if (leaf.surface.uuid == uuid) { - return leaf.surface - } - - return nil - - case .split(let container): - return container.topLeft.findUUID(uuid: uuid) ?? - container.bottomRight.findUUID(uuid: uuid) - } - } - - /// Returns true if the surface borders the top. Assumes the view is in the tree. - func doesBorderTop(view: SurfaceView) -> Bool { - switch (self) { - case .leaf(let leaf): - return leaf.surface == view - - case .split(let container): - switch (container.direction) { - case .vertical: - return container.topLeft.doesBorderTop(view: view) - - case .horizontal: - return container.topLeft.doesBorderTop(view: view) || - container.bottomRight.doesBorderTop(view: view) - } - } - } - - /// Return the node for the given view if its in the tree. - func leaf(for view: SurfaceView) -> Leaf? { - switch (self) { - case .leaf(let leaf): - if leaf.surface == view { - return leaf - } else { - return nil - } - - case .split(let container): - return container.topLeft.leaf(for: view) ?? - container.bottomRight.leaf(for: view) - } - } - - // MARK: - Sequence - - func makeIterator() -> IndexingIterator<[Leaf]> { - return leaves().makeIterator() - } - - /// Return all the leaves in this split node. This isn't very efficient but our split trees are never super - /// deep so its not an issue. - private func leaves() -> [Leaf] { - switch (self) { - case .leaf(let leaf): - return [leaf] - - case .split(let container): - return container.topLeft.leaves() + container.bottomRight.leaves() - } - } - - // MARK: - Equatable - - static func == (lhs: SplitNode, rhs: SplitNode) -> Bool { - switch (lhs, rhs) { - case (.leaf(let lhs_v), .leaf(let rhs_v)): - return lhs_v === rhs_v - case (.split(let lhs_v), .split(let rhs_v)): - return lhs_v === rhs_v - default: - return false - } - } - - class Leaf: ObservableObject, Equatable, Hashable, Codable { - let app: ghostty_app_t - @Published var surface: SurfaceView - - weak var parent: SplitNode.Container? - - /// Initialize a new leaf which creates a new terminal surface. - init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { - self.app = app - self.surface = SurfaceView(app, baseConfig: baseConfig, uuid: uuid) - } - - // MARK: - Hashable - - func hash(into hasher: inout Hasher) { - hasher.combine(app) - hasher.combine(surface) - } - - // MARK: - Equatable - - static func == (lhs: Leaf, rhs: Leaf) -> Bool { - return lhs.app == rhs.app && lhs.surface === rhs.surface - } - - // MARK: - Codable - - enum CodingKeys: String, CodingKey { - case pwd - case uuid - } - - required convenience init(from decoder: Decoder) throws { - // Decoding uses the global Ghostty app - guard let del = NSApplication.shared.delegate, - let appDel = del as? AppDelegate, - let app = appDel.ghostty.app else { - throw TerminalRestoreError.delegateInvalid - } - - let container = try decoder.container(keyedBy: CodingKeys.self) - let uuid = UUID(uuidString: try container.decode(String.self, forKey: .uuid)) - var config = SurfaceConfiguration() - config.workingDirectory = try container.decode(String?.self, forKey: .pwd) - - self.init(app, baseConfig: config, uuid: uuid) - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(surface.pwd, forKey: .pwd) - try container.encode(surface.uuid.uuidString, forKey: .uuid) - } - } - - class Container: ObservableObject, Equatable, Hashable, Codable { - let app: ghostty_app_t - let direction: SplitViewDirection - - @Published var topLeft: SplitNode - @Published var bottomRight: SplitNode - @Published var split: CGFloat = 0.5 - - var resizeEvent: PassthroughSubject = .init() - - weak var parent: SplitNode.Container? - - /// A container is always initialized from some prior leaf because a split has to originate - /// from a non-split value. When initializing, we inherit the leaf's surface and then - /// initialize a new surface for the new pane. - init(from: Leaf, direction: SplitViewDirection, baseConfig: SurfaceConfiguration? = nil) { - self.app = from.app - self.direction = direction - self.parent = from.parent - - // Initially, both topLeft and bottomRight are in the "nosplit" - // state since this is a new split. - self.topLeft = .leaf(from) - - let bottomRight: Leaf = .init(app, baseConfig: baseConfig) - self.bottomRight = .leaf(bottomRight) - - from.parent = self - bottomRight.parent = self - } - - // Move the top left node to the bottom right and vice versa, - // preserving the size. - func swap() { - let topLeft: SplitNode = self.topLeft - self.topLeft = bottomRight - self.bottomRight = topLeft - self.split = 1 - self.split - } - - /// Resize the split by moving the split divider in the given - /// direction by the given amount. If this container is not split - /// in the given direction, navigate up the tree until we find a - /// container that is - func resize(direction: SplitResizeDirection, amount: UInt16) { - // We send a resize event to our publisher which will be - // received by the SplitView. - switch (self.direction) { - case .horizontal: - switch (direction) { - case .left: resizeEvent.send(-Double(amount)) - case .right: resizeEvent.send(Double(amount)) - default: parent?.resize(direction: direction, amount: amount) - } - case .vertical: - switch (direction) { - case .up: resizeEvent.send(-Double(amount)) - case .down: resizeEvent.send(Double(amount)) - default: parent?.resize(direction: direction, amount: amount) - } - } - } - - /// Equalize the splits in this container. Each split is equalized - /// based on its weight, i.e. the number of leaves it contains. - /// This function returns the weight of this container. - func equalize() -> UInt { - let topLeftWeight: UInt - switch (topLeft) { - case .leaf: - topLeftWeight = 1 - case .split(let c): - topLeftWeight = c.equalize() - } - - let bottomRightWeight: UInt - switch (bottomRight) { - case .leaf: - bottomRightWeight = 1 - case .split(let c): - bottomRightWeight = c.equalize() - } - - let weight = topLeftWeight + bottomRightWeight - split = Double(topLeftWeight) / Double(weight) - return weight - } - - /// Returns the top most parent, or this container. Because this - /// would fall back to use to self, the return value is guaranteed. - func rootContainer() -> Container { - guard let parent = self.parent else { return self } - return parent.rootContainer() - } - - /// Returns the first leaf from the given container. This is most - /// useful for root container, so that we can find the top-left-most - /// leaf. - func firstLeaf() -> Leaf { - switch (self.topLeft) { - case .leaf(let leaf): - return leaf - case .split(let s): - return s.firstLeaf() - } - } - - /// Returns the last leaf from the given container. This is most - /// useful for root container, so that we can find the bottom-right- - /// most leaf. - func lastLeaf() -> Leaf { - switch (self.bottomRight) { - case .leaf(let leaf): - return leaf - case .split(let s): - return s.lastLeaf() - } - } - - // MARK: - Hashable - - func hash(into hasher: inout Hasher) { - hasher.combine(app) - hasher.combine(direction) - hasher.combine(topLeft) - hasher.combine(bottomRight) - } - - // MARK: - Equatable - - static func == (lhs: Container, rhs: Container) -> Bool { - return lhs.app == rhs.app && - lhs.direction == rhs.direction && - lhs.topLeft == rhs.topLeft && - lhs.bottomRight == rhs.bottomRight - } - - // MARK: - Codable - - enum CodingKeys: String, CodingKey { - case direction - case split - case topLeft - case bottomRight - } - - required init(from decoder: Decoder) throws { - // Decoding uses the global Ghostty app - guard let del = NSApplication.shared.delegate, - let appDel = del as? AppDelegate, - let app = appDel.ghostty.app else { - throw TerminalRestoreError.delegateInvalid - } - - let container = try decoder.container(keyedBy: CodingKeys.self) - self.app = app - self.direction = try container.decode(SplitViewDirection.self, forKey: .direction) - self.split = try container.decode(CGFloat.self, forKey: .split) - self.topLeft = try container.decode(SplitNode.self, forKey: .topLeft) - self.bottomRight = try container.decode(SplitNode.self, forKey: .bottomRight) - - // Fix up the parent references - self.topLeft.parent = self - self.bottomRight.parent = self - } - - func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(direction, forKey: .direction) - try container.encode(split, forKey: .split) - try container.encode(topLeft, forKey: .topLeft) - try container.encode(bottomRight, forKey: .bottomRight) - } - } - - /// This keeps track of the "neighbors" of a split: the immediately above/below/left/right - /// nodes. This is purposely weak so we don't have to worry about memory management - /// with this (although, it should always be correct). - struct Neighbors { - var left: SplitNode? - var right: SplitNode? - var up: SplitNode? - var down: SplitNode? - - /// These are the previous/next nodes. It will certainly be one of the above as well - /// but we keep track of these separately because depending on the split direction - /// of the containing node, previous may be left OR up (same for next). - var previous: SplitNode? - var next: SplitNode? - - /// No neighbors, used by the root node. - static let empty: Self = .init() - - /// Get the node for a given direction. - func get(direction: SplitFocusDirection) -> SplitNode? { - let map: [SplitFocusDirection : KeyPath] = [ - .previous: \.previous, - .next: \.next, - .up: \.up, - .down: \.down, - .left: \.left, - .right: \.right, - ] - - guard let path = map[direction] else { return nil } - return self[keyPath: path] - } - - /// Update multiple keys and return a new copy. - func update(_ attrs: [WritableKeyPath: SplitNode?]) -> Self { - var clone = self - attrs.forEach { (key, value) in - clone[keyPath: key] = value - } - return clone - } - - /// True if there are no neighbors - func isEmpty() -> Bool { - return self.previous == nil && self.next == nil - } - } - } -} diff --git a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift b/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift deleted file mode 100644 index ccb7cca38..000000000 --- a/macos/Sources/Ghostty/Ghostty.TerminalSplit.swift +++ /dev/null @@ -1,468 +0,0 @@ -import SwiftUI -import GhosttyKit - -extension Ghostty { - /// A spittable terminal view is one where the terminal allows for "splits" (vertical and horizontal) within the - /// view. The terminal starts in the unsplit state (a plain ol' TerminalView) but responds to changes to the - /// split direction by splitting the terminal. - /// - /// This also allows one split to be "zoomed" at any time. - struct TerminalSplit: View { - /// The current state of the root node. This can be set to nil when all surfaces are closed. - @Binding var node: SplitNode? - - /// Non-nil if one of the surfaces in the split tree is currently "zoomed." A zoomed surface - /// becomes "full screen" on the split tree. - @State private var zoomedSurface: SurfaceView? = nil - - var body: some View { - ZStack { - TerminalSplitRoot( - node: $node, - zoomedSurface: $zoomedSurface - ) - - // If we have a zoomed surface, we overlay that on top of our split - // root. Our split root will become clear when there is a zoomed - // surface. We need to keep the split root around so that we don't - // lose all of the surface state so this must be a ZStack. - if let surfaceView = zoomedSurface { - InspectableSurface(surfaceView: surfaceView) - } - } - } - } - - /// The root of a split tree. This sets up the initial SplitNode state and renders. There is only ever - /// one of these in a split tree. - private struct TerminalSplitRoot: View { - /// The root node that we're rendering. This will be set to nil if all the surfaces in this tree close. - @Binding var node: SplitNode? - - /// Keeps track of whether we're in a zoomed split state or not. If one of the splits we own - /// is in the zoomed state, we clear our body since we expect a zoomed split to overlay - /// this one. - @Binding var zoomedSurface: SurfaceView? - - var body: some View { - let center = NotificationCenter.default - let pubZoom = center.publisher(for: Notification.didToggleSplitZoom) - - // If we're zoomed, we don't render anything, we are transparent. This - // ensures that the View stays around so we don't lose our state, but - // also that the zoomed view on top can see through if background transparency - // is enabled. - if (zoomedSurface == nil) { - ZStack { - switch (node) { - case nil: - Color(.clear) - - case .leaf(let leaf): - TerminalSplitLeaf( - leaf: leaf, - neighbors: .empty, - node: $node - ) - - case .split(let container): - TerminalSplitContainer( - neighbors: .empty, - node: $node, - container: container - ) - .onReceive(pubZoom) { onZoom(notification: $0) } - } - } - } else { - // On these events we want to reset the split state and call it. - let pubSplit = center.publisher(for: Notification.ghosttyNewSplit, object: zoomedSurface!) - let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: zoomedSurface!) - let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: zoomedSurface!) - - ZStack {} - .onReceive(pubZoom) { onZoomReset(notification: $0) } - .onReceive(pubSplit) { onZoomReset(notification: $0) } - .onReceive(pubClose) { onZoomReset(notification: $0) } - .onReceive(pubFocus) { onZoomReset(notification: $0) } - } - } - - func onZoom(notification: SwiftUI.Notification) { - // Our node must be split to receive zooms. You can't zoom an unsplit terminal. - if case .leaf = node { - preconditionFailure("TerminalSplitRoom must not be zoom-able if no splits exist") - } - - // Make sure the notification has a surface and that this window owns the surface. - guard let surfaceView = notification.object as? SurfaceView else { return } - guard node?.contains(view: surfaceView) ?? false else { return } - - // We are in the zoomed state. - zoomedSurface = surfaceView - - // See onZoomReset, same logic. - DispatchQueue.main.async { Ghostty.moveFocus(to: surfaceView) } - } - - func onZoomReset(notification: SwiftUI.Notification) { - // Make sure the notification has a surface and that this window owns the surface. - guard let surfaceView = notification.object as? SurfaceView else { return } - guard zoomedSurface == surfaceView else { return } - - // We are now unzoomed - zoomedSurface = nil - - // We need to stay focused on this view, but the view is going to change - // superviews. We need to do this async so it happens on the next event loop - // tick. - DispatchQueue.main.async { - Ghostty.moveFocus(to: surfaceView) - - // If the notification is not a toggle zoom notification, we want to re-publish - // it after a short delay so that the split tree has a chance to re-establish - // so the proper view gets this notification. - if (notification.name != Notification.didToggleSplitZoom) { - // We have to wait ANOTHER tick since we just established. - DispatchQueue.main.async { - NotificationCenter.default.post(notification) - } - } - } - } - } - - /// A noSplit leaf node of a split tree. - private struct TerminalSplitLeaf: View { - /// The leaf to draw the surface for. - let leaf: SplitNode.Leaf - - /// The neighbors, used for navigation. - let neighbors: SplitNode.Neighbors - - /// The SplitNode that the leaf belongs to. This will be set to nil when leaf is closed. - @Binding var node: SplitNode? - - var body: some View { - let center = NotificationCenter.default - let pub = center.publisher(for: Notification.ghosttyNewSplit, object: leaf.surface) - let pubClose = center.publisher(for: Notification.ghosttyCloseSurface, object: leaf.surface) - let pubFocus = center.publisher(for: Notification.ghosttyFocusSplit, object: leaf.surface) - let pubResize = center.publisher(for: Notification.didResizeSplit, object: leaf.surface) - - InspectableSurface(surfaceView: leaf.surface, isSplit: !neighbors.isEmpty()) - .onReceive(pub) { onNewSplit(notification: $0) } - .onReceive(pubClose) { onClose(notification: $0) } - .onReceive(pubFocus) { onMoveFocus(notification: $0) } - .onReceive(pubResize) { onResize(notification: $0) } - } - - private func onClose(notification: SwiftUI.Notification) { - var processAlive = false - if let valueAny = notification.userInfo?["process_alive"] { - if let value = valueAny as? Bool { - processAlive = value - } - } - - // If the child process is not alive, then we exit immediately - guard processAlive else { - node = nil - return - } - - // If we don't have a window to attach our modal to, we also exit immediately. - // This should NOT happen. - guard let window = leaf.surface.window else { - node = nil - return - } - - // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog - // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that - // confirmationDialog allows the user to Cmd-W close the alert, but when doing - // so SwiftUI does not update any of the bindings to note that window is no longer - // being shown, and provides no callback to detect this. - let alert = NSAlert() - alert.messageText = "Close Terminal?" - alert.informativeText = "The terminal still has a running process. If you close the " + - "terminal the process will be killed." - alert.addButton(withTitle: "Close the Terminal") - alert.addButton(withTitle: "Cancel") - alert.alertStyle = .warning - alert.beginSheetModal(for: window, completionHandler: { response in - switch (response) { - case .alertFirstButtonReturn: - alert.window.orderOut(nil) - node = nil - - default: - break - } - }) - } - - private func onNewSplit(notification: SwiftUI.Notification) { - let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] - let config = configAny as? SurfaceConfiguration - - // Determine our desired direction - guard let directionAny = notification.userInfo?["direction"] else { return } - guard let direction = directionAny as? ghostty_action_split_direction_e else { return } - let splitDirection: SplitViewDirection - let swap: Bool - switch (direction) { - case GHOSTTY_SPLIT_DIRECTION_RIGHT: - splitDirection = .horizontal - swap = false - case GHOSTTY_SPLIT_DIRECTION_LEFT: - splitDirection = .horizontal - swap = true - case GHOSTTY_SPLIT_DIRECTION_DOWN: - splitDirection = .vertical - swap = false - case GHOSTTY_SPLIT_DIRECTION_UP: - splitDirection = .vertical - swap = true - - default: - return - } - - // Setup our new container since we are now split - let container = SplitNode.Container(from: leaf, direction: splitDirection, baseConfig: config) - - // Change the parent node. This will trigger the parent to relayout our views. - node = .split(container) - - // See moveFocus comment, we have to run this whenever split changes. - Ghostty.moveFocus(to: container.bottomRight.preferredFocus(), from: node!.preferredFocus()) - - // If we are swapping, swap now. We do this after our focus event - // so that focus is in the right place. - if swap { - container.swap() - } - } - - /// This handles the event to move the split focus (i.e. previous/next) from a keyboard event. - private func onMoveFocus(notification: SwiftUI.Notification) { - // Determine our desired direction - guard let directionAny = notification.userInfo?[Notification.SplitDirectionKey] else { return } - guard let direction = directionAny as? SplitFocusDirection else { return } - - // Find the next surface to move to. In most cases this should be - // finding the neighbor in provided direction, and focus it. When - // the neighbor cannot be found based on next or previous direction, - // this would instead search for first or last leaf and focus it - // instead, giving the wrap around effect. - // When other directions are provided, this can be nil, and early - // returned. - guard let nextSurface = neighbors.get(direction: direction)?.preferredFocus(direction) - ?? node?.firstOrLast(direction)?.surface else { return } - - Ghostty.moveFocus( - to: nextSurface - ) - } - - /// Handle a resize event. - private func onResize(notification: SwiftUI.Notification) { - // If this leaf is not part of a split then there is nothing to do - guard let parent = leaf.parent else { return } - - guard let directionAny = notification.userInfo?[Ghostty.Notification.ResizeSplitDirectionKey] else { return } - guard let direction = directionAny as? Ghostty.SplitResizeDirection else { return } - - guard let amountAny = notification.userInfo?[Ghostty.Notification.ResizeSplitAmountKey] else { return } - guard let amount = amountAny as? UInt16 else { return } - - parent.resize(direction: direction, amount: amount) - } - } - - /// This represents a split view that is in the horizontal or vertical split state. - private struct TerminalSplitContainer: View { - @EnvironmentObject var ghostty: Ghostty.App - - let neighbors: SplitNode.Neighbors - @Binding var node: SplitNode? - @ObservedObject var container: SplitNode.Container - - var body: some View { - SplitView( - container.direction, - $container.split, - dividerColor: ghostty.config.splitDividerColor, - resizeIncrements: .init(width: 1, height: 1), - resizePublisher: container.resizeEvent, - left: { - let neighborKey: WritableKeyPath = container.direction == .horizontal ? \.right : \.down - - TerminalSplitNested( - node: closeableTopLeft(), - neighbors: neighbors.update([ - neighborKey: container.bottomRight, - \.next: container.bottomRight, - ]) - ) - }, right: { - let neighborKey: WritableKeyPath = container.direction == .horizontal ? \.left : \.up - - TerminalSplitNested( - node: closeableBottomRight(), - neighbors: neighbors.update([ - neighborKey: container.topLeft, - \.previous: container.topLeft, - ]) - ) - }) - } - - private func closeableTopLeft() -> Binding { - return .init(get: { - container.topLeft - }, set: { newValue in - if let newValue { - container.topLeft = newValue - return - } - - // Closing - node = container.bottomRight - - switch (node) { - case .leaf(let l): - l.parent = container.parent - case .split(let c): - c.parent = container.parent - case .none: - break - } - - DispatchQueue.main.async { - Ghostty.moveFocus( - to: container.bottomRight.preferredFocus(), - from: container.topLeft.preferredFocus() - ) - } - }) - } - - private func closeableBottomRight() -> Binding { - return .init(get: { - container.bottomRight - }, set: { newValue in - if let newValue { - container.bottomRight = newValue - return - } - - // Closing - node = container.topLeft - - switch (node) { - case .leaf(let l): - l.parent = container.parent - case .split(let c): - c.parent = container.parent - case .none: - break - } - - DispatchQueue.main.async { - Ghostty.moveFocus( - to: container.topLeft.preferredFocus(), - from: container.bottomRight.preferredFocus() - ) - } - }) - } - } - - - /// This is like TerminalSplitRoot, but... not the root. This renders a SplitNode in any state but - /// requires there be a binding to the parent node. - private struct TerminalSplitNested: View { - @Binding var node: SplitNode? - let neighbors: SplitNode.Neighbors - - var body: some View { - Group { - switch (node) { - case nil: - Color(.clear) - - case .leaf(let leaf): - TerminalSplitLeaf( - leaf: leaf, - neighbors: neighbors, - node: $node - ) - - case .split(let container): - TerminalSplitContainer( - neighbors: neighbors, - node: $node, - container: container - ) - } - } - .id(node) - } - } - - /// When changing the split state, or going full screen (native or non), the terminal view - /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't - /// figure it out so we're going to do this hacky thing to bring focus back to the terminal - /// that should have it. - static func moveFocus( - to: SurfaceView, - from: SurfaceView? = nil, - delay: TimeInterval? = nil - ) { - // The whole delay machinery is a bit of a hack to work around a - // situation where the window is destroyed and the surface view - // will never be attached to a window. Realistically, we should - // handle this upstream but we also don't want this function to be - // a source of infinite loops. - - // Our max delay before we give up - let maxDelay: TimeInterval = 0.5 - guard (delay ?? 0) < maxDelay else { return } - - // We start at a 50 millisecond delay and do a doubling backoff - let nextDelay: TimeInterval = if let delay { - delay * 2 - } else { - // 100 milliseconds - 0.05 - } - - let work: DispatchWorkItem = .init { - // If the callback runs before the surface is attached to a view - // then the window will be nil. We just reschedule in that case. - guard let window = to.window else { - moveFocus(to: to, from: from, delay: nextDelay) - return - } - - // If we had a previously focused node and its not where we're sending - // focus, make sure that we explicitly tell it to lose focus. In theory - // we should NOT have to do this but the focus callback isn't getting - // called for some reason. - if let from = from { - _ = from.resignFirstResponder() - } - - window.makeFirstResponder(to) - } - - let queue = DispatchQueue.main - if let delay { - queue.asyncAfter(deadline: .now() + delay, execute: work) - } else { - queue.async(execute: work) - } - } -} diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 513e5af46..a282c7a88 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -460,6 +460,60 @@ extension Ghostty { return config } } + + /// When changing the split state, or going full screen (native or non), the terminal view + /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't + /// figure it out so we're going to do this hacky thing to bring focus back to the terminal + /// that should have it. + static func moveFocus( + to: SurfaceView, + from: SurfaceView? = nil, + delay: TimeInterval? = nil + ) { + // The whole delay machinery is a bit of a hack to work around a + // situation where the window is destroyed and the surface view + // will never be attached to a window. Realistically, we should + // handle this upstream but we also don't want this function to be + // a source of infinite loops. + + // Our max delay before we give up + let maxDelay: TimeInterval = 0.5 + guard (delay ?? 0) < maxDelay else { return } + + // We start at a 50 millisecond delay and do a doubling backoff + let nextDelay: TimeInterval = if let delay { + delay * 2 + } else { + // 100 milliseconds + 0.05 + } + + let work: DispatchWorkItem = .init { + // If the callback runs before the surface is attached to a view + // then the window will be nil. We just reschedule in that case. + guard let window = to.window else { + moveFocus(to: to, from: from, delay: nextDelay) + return + } + + // If we had a previously focused node and its not where we're sending + // focus, make sure that we explicitly tell it to lose focus. In theory + // we should NOT have to do this but the focus callback isn't getting + // called for some reason. + if let from = from { + _ = from.resignFirstResponder() + } + + window.makeFirstResponder(to) + } + + let queue = DispatchQueue.main + if let delay { + queue.asyncAfter(deadline: .now() + delay, execute: work) + } else { + queue.async(execute: work) + } + } } // MARK: Surface Environment Keys From 01fa87f2aba0bfaa122b018ef08ca6baa8740d7c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 13:37:42 -0700 Subject: [PATCH 372/642] macos: fix iOS builds --- macos/Sources/Ghostty/SurfaceView.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index a282c7a88..18a8d2f1c 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -461,6 +461,7 @@ extension Ghostty { } } + #if canImport(AppKit) /// When changing the split state, or going full screen (native or non), the terminal view /// will lose focus. There has to be some nice SwiftUI-native way to fix this but I can't /// figure it out so we're going to do this hacky thing to bring focus back to the terminal @@ -514,6 +515,7 @@ extension Ghostty { queue.async(execute: work) } } + #endif } // MARK: Surface Environment Keys From f8e3539b7db2d6bfe60bb744e67891ea22cc02dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 19:43:47 -0700 Subject: [PATCH 373/642] macos: remove the unused resizeEvent code from SplitView --- .../Splits/TerminalSplitTreeView.swift | 1 - .../Sources/Helpers/SplitView/SplitView.swift | 44 +------------------ 2 files changed, 2 insertions(+), 43 deletions(-) diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 4a41afc42..b219e0b31 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -43,7 +43,6 @@ struct TerminalSplitSubtreeView: View { }), dividerColor: ghostty.config.splitDividerColor, resizeIncrements: .init(width: 1, height: 1), - resizePublisher: .init(), left: { TerminalSplitSubtreeView(node: split.left, onResize: onResize) }, diff --git a/macos/Sources/Helpers/SplitView/SplitView.swift b/macos/Sources/Helpers/SplitView/SplitView.swift index 8ac2bc33f..9747ac99f 100644 --- a/macos/Sources/Helpers/SplitView/SplitView.swift +++ b/macos/Sources/Helpers/SplitView/SplitView.swift @@ -1,5 +1,4 @@ import SwiftUI -import Combine /// A split view shows a left and right (or top and bottom) view with a divider in the middle to do resizing. /// The terminlogy "left" and "right" is always used but for vertical splits "left" is "top" and "right" is "bottom". @@ -13,12 +12,10 @@ struct SplitView: View { /// Divider color let dividerColor: Color - /// If set, the split view supports programmatic resizing via events sent via the publisher. /// Minimum increment (in points) that this split can be resized by, in /// each direction. Both `height` and `width` should be whole numbers /// greater than or equal to 1.0 let resizeIncrements: NSSize - let resizePublisher: PassthroughSubject /// The left and right views to render. let left: L @@ -55,37 +52,15 @@ struct SplitView: View { .position(splitterPoint) .gesture(dragGesture(geo.size, splitterPoint: splitterPoint)) } - .onReceive(resizePublisher) { value in - resize(for: geo.size, amount: value) - } } } - /// Initialize a split view. This view isn't programmatically resizable; it can only be resized - /// by manually dragging the divider. - init(_ direction: SplitViewDirection, - _ split: Binding, - dividerColor: Color, - @ViewBuilder left: (() -> L), - @ViewBuilder right: (() -> R)) { - self.init( - direction, - split, - dividerColor: dividerColor, - resizeIncrements: .init(width: 1, height: 1), - resizePublisher: .init(), - left: left, - right: right - ) - } - - /// Initialize a split view that supports programmatic resizing. + /// Initialize a split view that can be resized by manually dragging the divider. init( _ direction: SplitViewDirection, _ split: Binding, dividerColor: Color, - resizeIncrements: NSSize, - resizePublisher: PassthroughSubject, + resizeIncrements: NSSize = .init(width: 1, height: 1), @ViewBuilder left: (() -> L), @ViewBuilder right: (() -> R) ) { @@ -93,25 +68,10 @@ struct SplitView: View { self._split = split self.dividerColor = dividerColor self.resizeIncrements = resizeIncrements - self.resizePublisher = resizePublisher self.left = left() self.right = right() } - private func resize(for size: CGSize, amount: Double) { - let dim: CGFloat - switch (direction) { - case .horizontal: - dim = size.width - case .vertical: - dim = size.height - } - - let pos = split * dim - let new = min(max(minSize, pos + amount), dim - minSize) - split = new / dim - } - private func dragGesture(_ size: CGSize, splitterPoint: CGPoint) -> some Gesture { return DragGesture() .onChanged { gesture in From 1966dfdef7eb8a8d08bc486a5419f409260715fb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Jun 2025 19:44:30 -0700 Subject: [PATCH 374/642] macos: moving some files around --- macos/Ghostty.xcodeproj/project.pbxproj | 34 +++++++------------ .../Splits}/SplitView.Divider.swift | 0 .../Splits}/SplitView.swift | 0 .../EventModifiers+Extension.swift | 0 .../KeyboardShortcut+Extension.swift | 0 .../NSAppearance+Extension.swift | 0 .../NSApplication+Extension.swift | 0 .../{ => Extensions}/NSImage+Extension.swift | 0 .../NSPasteboard+Extension.swift | 0 .../{ => Extensions}/NSScreen+Extension.swift | 0 .../{ => Extensions}/NSWindow+Extension.swift | 0 .../{ => Extensions}/OSColor+Extension.swift | 0 .../{ => Extensions}/String+Extension.swift | 0 .../{ => Extensions}/View+Extension.swift | 0 14 files changed, 13 insertions(+), 21 deletions(-) rename macos/Sources/{Helpers/SplitView => Features/Splits}/SplitView.Divider.swift (100%) rename macos/Sources/{Helpers/SplitView => Features/Splits}/SplitView.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/EventModifiers+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/KeyboardShortcut+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/NSAppearance+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/NSApplication+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/NSImage+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/NSPasteboard+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/NSScreen+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/NSWindow+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/OSColor+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/String+Extension.swift (100%) rename macos/Sources/Helpers/{ => Extensions}/View+Extension.swift (100%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index bb9e860f3..62cb079bf 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -301,23 +301,11 @@ A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, - A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, - A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, - C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, - A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, - A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, - A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */, - A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, - AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, - A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, - A5985CD62C320C4500C57AD3 /* String+Extension.swift */, - A5CC36142C9CDA03004D6760 /* View+Extension.swift */, A5CA378D2D31D6C100931030 /* Weak.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, - A5CEAFDA29B8005900646FDA /* SplitView */, ); path = Helpers; sourceTree = ""; @@ -434,6 +422,8 @@ children = ( A586365E2DEE6C2100E04A10 /* SplitTree.swift */, A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */, + A5CEAFDB29B8009000646FDA /* SplitView.swift */, + A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */, ); path = Splits; sourceTree = ""; @@ -442,7 +432,18 @@ isa = PBXGroup; children = ( A586366A2DF0A98900E04A10 /* Array+Extension.swift */, + A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, + A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, + C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, + A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, + A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, + A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */, + A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, + AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, + A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, + A5985CD62C320C4500C57AD3 /* String+Extension.swift */, + A5CC36142C9CDA03004D6760 /* View+Extension.swift */, ); path = Extensions; sourceTree = ""; @@ -534,15 +535,6 @@ path = "Global Keybinds"; sourceTree = ""; }; - A5CEAFDA29B8005900646FDA /* SplitView */ = { - isa = PBXGroup; - children = ( - A5CEAFDB29B8009000646FDA /* SplitView.swift */, - A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */, - ); - path = SplitView; - sourceTree = ""; - }; A5D495A3299BECBA00DD1313 /* Frameworks */ = { isa = PBXGroup; children = ( diff --git a/macos/Sources/Helpers/SplitView/SplitView.Divider.swift b/macos/Sources/Features/Splits/SplitView.Divider.swift similarity index 100% rename from macos/Sources/Helpers/SplitView/SplitView.Divider.swift rename to macos/Sources/Features/Splits/SplitView.Divider.swift diff --git a/macos/Sources/Helpers/SplitView/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift similarity index 100% rename from macos/Sources/Helpers/SplitView/SplitView.swift rename to macos/Sources/Features/Splits/SplitView.swift diff --git a/macos/Sources/Helpers/EventModifiers+Extension.swift b/macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift similarity index 100% rename from macos/Sources/Helpers/EventModifiers+Extension.swift rename to macos/Sources/Helpers/Extensions/EventModifiers+Extension.swift diff --git a/macos/Sources/Helpers/KeyboardShortcut+Extension.swift b/macos/Sources/Helpers/Extensions/KeyboardShortcut+Extension.swift similarity index 100% rename from macos/Sources/Helpers/KeyboardShortcut+Extension.swift rename to macos/Sources/Helpers/Extensions/KeyboardShortcut+Extension.swift diff --git a/macos/Sources/Helpers/NSAppearance+Extension.swift b/macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift similarity index 100% rename from macos/Sources/Helpers/NSAppearance+Extension.swift rename to macos/Sources/Helpers/Extensions/NSAppearance+Extension.swift diff --git a/macos/Sources/Helpers/NSApplication+Extension.swift b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift similarity index 100% rename from macos/Sources/Helpers/NSApplication+Extension.swift rename to macos/Sources/Helpers/Extensions/NSApplication+Extension.swift diff --git a/macos/Sources/Helpers/NSImage+Extension.swift b/macos/Sources/Helpers/Extensions/NSImage+Extension.swift similarity index 100% rename from macos/Sources/Helpers/NSImage+Extension.swift rename to macos/Sources/Helpers/Extensions/NSImage+Extension.swift diff --git a/macos/Sources/Helpers/NSPasteboard+Extension.swift b/macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift similarity index 100% rename from macos/Sources/Helpers/NSPasteboard+Extension.swift rename to macos/Sources/Helpers/Extensions/NSPasteboard+Extension.swift diff --git a/macos/Sources/Helpers/NSScreen+Extension.swift b/macos/Sources/Helpers/Extensions/NSScreen+Extension.swift similarity index 100% rename from macos/Sources/Helpers/NSScreen+Extension.swift rename to macos/Sources/Helpers/Extensions/NSScreen+Extension.swift diff --git a/macos/Sources/Helpers/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift similarity index 100% rename from macos/Sources/Helpers/NSWindow+Extension.swift rename to macos/Sources/Helpers/Extensions/NSWindow+Extension.swift diff --git a/macos/Sources/Helpers/OSColor+Extension.swift b/macos/Sources/Helpers/Extensions/OSColor+Extension.swift similarity index 100% rename from macos/Sources/Helpers/OSColor+Extension.swift rename to macos/Sources/Helpers/Extensions/OSColor+Extension.swift diff --git a/macos/Sources/Helpers/String+Extension.swift b/macos/Sources/Helpers/Extensions/String+Extension.swift similarity index 100% rename from macos/Sources/Helpers/String+Extension.swift rename to macos/Sources/Helpers/Extensions/String+Extension.swift diff --git a/macos/Sources/Helpers/View+Extension.swift b/macos/Sources/Helpers/Extensions/View+Extension.swift similarity index 100% rename from macos/Sources/Helpers/View+Extension.swift rename to macos/Sources/Helpers/Extensions/View+Extension.swift From c40ac6b785cc7482d7556943b7d26f7fd4897617 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Jun 2025 07:09:46 -0700 Subject: [PATCH 375/642] input: add focus split directional commands to command palette --- .../Terminal/BaseTerminalController.swift | 6 ++-- src/input/command.zig | 34 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 5e2777195..ea849bb4a 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -447,7 +447,7 @@ class BaseTerminalController: NSWindowController, case .left: focusDirection = .spatial(.left) case .right: focusDirection = .spatial(.right) } - + // Find the node for the target surface guard let targetNode = surfaceTree.root?.node(view: target) else { return } @@ -462,7 +462,9 @@ class BaseTerminalController: NSWindowController, } // Move focus to the next surface - Ghostty.moveFocus(to: nextSurface, from: target) + DispatchQueue.main.async { + Ghostty.moveFocus(to: nextSurface, from: target) + } } @objc private func ghosttyDidToggleSplitZoom(_ notification: Notification) { diff --git a/src/input/command.zig b/src/input/command.zig index 1ce6aa7cb..4a918cff3 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -274,6 +274,39 @@ fn actionCommands(action: Action.Key) []const Command { }, }, + .goto_split => comptime &.{ + .{ + .action = .{ .goto_split = .previous }, + .title = "Focus Split: Previous", + .description = "Focus the previous split, if any.", + }, + .{ + .action = .{ .goto_split = .next }, + .title = "Focus Split: Next", + .description = "Focus the next split, if any.", + }, + .{ + .action = .{ .goto_split = .left }, + .title = "Focus Split: Left", + .description = "Focus the split to the left, if it exists.", + }, + .{ + .action = .{ .goto_split = .right }, + .title = "Focus Split: Right", + .description = "Focus the split to the right, if it exists.", + }, + .{ + .action = .{ .goto_split = .up }, + .title = "Focus Split: Up", + .description = "Focus the split above, if it exists.", + }, + .{ + .action = .{ .goto_split = .down }, + .title = "Focus Split: Down", + .description = "Focus the split below, if it exists.", + }, + }, + .toggle_split_zoom => comptime &.{.{ .action = .toggle_split_zoom, .title = "Toggle Split Zoom", @@ -396,7 +429,6 @@ fn actionCommands(action: Action.Key) []const Command { .jump_to_prompt, .write_scrollback_file, .goto_tab, - .goto_split, .resize_split, .crash, => comptime &.{}, From 9008e21637f504fe606da37fb30d3ebedb50a3d4 Mon Sep 17 00:00:00 2001 From: Francisco Giordano Date: Sun, 1 Jun 2025 12:06:47 -0300 Subject: [PATCH 376/642] fix: exit non-native fullscreen on close --- macos/Sources/Helpers/Fullscreen.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 6094bf844..6b10ceb40 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -150,6 +150,26 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { private var savedState: SavedState? + required init?(_ window: NSWindow) { + super.init(window) + + NotificationCenter.default.addObserver( + self, + selector: #selector(windowWillCloseNotification), + name: NSWindow.willCloseNotification, + object: window) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + @objc private func windowWillCloseNotification(_ notification: Notification) { + // When the window closes we need to explicitly exit non-native fullscreen + // otherwise some state like the menu bar can remain hidden. + exit() + } + func enter() { // If we are in fullscreen we don't do it again. guard !isFullscreen else { return } From 045c84acb71e99e384b63ca658621bce891f8841 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Jun 2025 13:41:33 -0700 Subject: [PATCH 377/642] macos: split directional navigation should use distance to leaf Fixes regression from #7523 I messed two things up around spatial navigation in the split tree that this commit fixes: 1. The distance in the spatial tree only used a single dimension that we were navigating. This commit now uses 2D euclidean distance from the top-left corners of nodes. This handles the case where the nodes are directly above or below each other better. 2. The spatial slots include split containers because they are layout elements. But we should only navigate to leaf nodes. This was causing the wrong navigatin to happen in some scenarios. --- macos/Sources/Features/Splits/SplitTree.swift | 52 +++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index ab4b387a4..cbd440124 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -190,17 +190,22 @@ extension SplitTree { return nil } - // Extract the view from the best candidate node - let bestNode = nodes[0].node - switch bestNode { + // Extract the view from the best candidate node. The best candidate + // node is the closest leaf node. If we have no leaves (impossible?) + // just use the first node. + let bestNode = nodes.first(where: { + if case .leaf = $0.node { return true } else { return false } + }) ?? nodes[0] + switch bestNode.node { case .leaf(let view): return view + case .split: // If the best candidate is a split node, use its the leaf/rightmost // depending on our spatial direction. return switch (spatialDirection) { - case .up, .left: bestNode.leftmostLeaf() - case .down, .right: bestNode.rightmostLeaf() + case .up, .left: bestNode.node.leftmostLeaf() + case .down, .right: bestNode.node.rightmostLeaf() } } } @@ -892,32 +897,47 @@ extension SplitTree.Spatial { /// - **Up**: Slots with bounds above the reference node (Y=0 is top) /// - **Down**: Slots with bounds below the reference node /// - /// Results are sorted by distance from the reference node, with closest slots first. - /// Distance is calculated as the gap between the reference node and the candidate slot - /// in the direction of movement. + /// Results are sorted by 2D euclidean distance from the reference node, with closest slots first. + /// Distance is calculated from the top-left corners of the bounds, prioritizing nodes that are + /// closer in both dimensions. + /// + /// **Important**: The returned array contains both split nodes and leaf nodes. When using this + /// for navigation or focus management, you typically want to filter for leaf nodes first, as they + /// represent the actual views that can receive focus. Split nodes are included in the results + /// because they have bounds and occupy space in the layout, but they are structural elements + /// that cannot themselves be focused. If no leaf nodes are found in the results, you may need + /// to traverse into a split node to find its appropriate leaf child. /// /// - Parameters: /// - direction: The direction to search for slots /// - referenceNode: The node to use as the reference point - /// - Returns: An array of slots in the specified direction, sorted by distance (closest first) + /// - Returns: An array of slots in the specified direction, sorted by 2D distance (closest first) func slots(in direction: Direction, from referenceNode: SplitTree.Node) -> [Slot] { guard let refSlot = slots.first(where: { $0.node == referenceNode }) else { return [] } + + // Helper function to calculate 2D euclidean distance between top-left corners of two rectangles + func distance(from rect1: CGRect, to rect2: CGRect) -> Double { + // Calculate distance between top-left corners + let dx = rect2.minX - rect1.minX + let dy = rect2.minY - rect1.minY + return sqrt(dx * dx + dy * dy) + } - return switch direction { + let result = switch direction { case .left: // Slots to the left: their right edge is at or left of reference's left edge slots.filter { $0.node != referenceNode && $0.bounds.maxX <= refSlot.bounds.minX }.sorted { - (refSlot.bounds.minX - $0.bounds.maxX) < (refSlot.bounds.minX - $1.bounds.maxX) + distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds) } - + case .right: // Slots to the right: their left edge is at or right of reference's right edge slots.filter { $0.node != referenceNode && $0.bounds.minX >= refSlot.bounds.maxX }.sorted { - ($0.bounds.minX - refSlot.bounds.maxX) < ($1.bounds.minX - refSlot.bounds.maxX) + distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds) } case .up: @@ -925,7 +945,7 @@ extension SplitTree.Spatial { slots.filter { $0.node != referenceNode && $0.bounds.maxY <= refSlot.bounds.minY }.sorted { - (refSlot.bounds.minY - $0.bounds.maxY) < (refSlot.bounds.minY - $1.bounds.maxY) + distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds) } case .down: @@ -933,9 +953,11 @@ extension SplitTree.Spatial { slots.filter { $0.node != referenceNode && $0.bounds.minY >= refSlot.bounds.maxY }.sorted { - ($0.bounds.minY - refSlot.bounds.maxY) < ($1.bounds.minY - refSlot.bounds.maxY) + distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds) } } + + return result } /// Returns whether the given node borders the specified side of the spatial bounds. From c2c267439be55304ed07181dd35d5a90a5dd7cde Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 5 Jun 2025 13:14:58 -0700 Subject: [PATCH 378/642] macos: fix hasWindowButtons logic --- .../Features/Terminal/TerminalWindow.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/TerminalWindow.swift index 830a73e86..0b43582f3 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/TerminalWindow.swift @@ -45,15 +45,14 @@ class TerminalWindow: NSWindow { }, ] + // false if all three traffic lights are missing/hidden, otherwise true private var hasWindowButtons: Bool { get { - if let close = standardWindowButton(.closeButton), - let miniaturize = standardWindowButton(.miniaturizeButton), - let zoom = standardWindowButton(.zoomButton) { - return !(close.isHidden && miniaturize.isHidden && zoom.isHidden) - } else { - return false - } + // if standardWindowButton(.theButton) == nil, the button isn't there, so coalesce to true + let closeIsHidden = standardWindowButton(.closeButton)?.isHiddenOrHasHiddenAncestor ?? true + let miniaturizeIsHidden = standardWindowButton(.miniaturizeButton)?.isHiddenOrHasHiddenAncestor ?? true + let zoomIsHidden = standardWindowButton(.zoomButton)?.isHiddenOrHasHiddenAncestor ?? true + return !(closeIsHidden && miniaturizeIsHidden && zoomIsHidden) } } @@ -78,7 +77,7 @@ class TerminalWindow: NSWindow { if titlebarTabs { generateToolbar() } - + level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } From 70f030e3c2f09ad7785846b94c40988dcc97266c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 07:22:37 -0700 Subject: [PATCH 379/642] macos: dismiss notifications on focus, application exit I've only recently been using programs that use user notifications heavily and this commit addresses a number of annoyances I've encountered. 1. Notifications dispatched while the source terminal surface is focused are now only shown for a short time (3 seconds hardcoded) and then automatically dismiss. 2. Notifications are dismissed when the target surface becomes focused from an unfocused state. This dismissal happens immediately (no delay). 3. Notifications are dismissed when the application exits. 4. This fixes a bug where notification callbacks were modifying view state, but the notification center doesn't guarantee that the callback is called on the main thread. We now ensure that the callback is always called on the main thread. --- macos/Sources/App/macOS/AppDelegate.swift | 7 +++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 26 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c6816d50c..54454e6bf 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -316,6 +316,13 @@ class AppDelegate: NSObject, } } + func applicationWillTerminate(_ notification: Notification) { + // We have no notifications we want to persist after death, + // so remove them all now. In the future we may want to be + // more selective and only remove surface-targeted notifications. + UNUserNotificationCenter.current().removeAllDeliveredNotifications() + } + /// This is called when the application is already open and someone double-clicks the icon /// or clicks the dock icon. func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 0aecef6ad..682efa947 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -306,6 +306,14 @@ extension Ghostty { // We unset our bell state if we gained focus bell = false + + // Remove any notifications for this surface once we gain focus. + if !notificationIdentifiers.isEmpty { + UNUserNotificationCenter.current() + .removeDeliveredNotifications( + withIdentifiers: Array(notificationIdentifiers)) + self.notificationIdentifiers = [] + } } } @@ -1388,13 +1396,29 @@ extension Ghostty { trigger: nil ) - UNUserNotificationCenter.current().add(request) { error in + // Note the callback may be executed on a background thread as documented + // so we need @MainActor since we're reading/writing view state. + UNUserNotificationCenter.current().add(request) { @MainActor error in if let error = error { AppDelegate.logger.error("Error scheduling user notification: \(error)") return } + // We need to keep track of this notification so we can remove it + // under certain circumstances self.notificationIdentifiers.insert(uuid) + + // If we're focused then we schedule to remove the notification + // after a few seconds. If we gain focus we automatically remove it + // in focusDidChange. + if (self.focused) { + Task { @MainActor [weak self] in + try await Task.sleep(for: .seconds(3)) + self?.notificationIdentifiers.remove(uuid) + UNUserNotificationCenter.current() + .removeDeliveredNotifications(withIdentifiers: [uuid]) + } + } } } From 5f6a15abef20f510d79314c5107f58ff3636682a Mon Sep 17 00:00:00 2001 From: Aaron Ruan Date: Fri, 6 Jun 2025 23:46:06 +0800 Subject: [PATCH 380/642] Add bell feature flags for audio, attention, and title actions on macOS Signed-off-by: Aaron Ruan --- macos/Sources/App/macOS/AppDelegate.swift | 10 ++++++---- macos/Sources/Ghostty/Ghostty.Config.swift | 3 +++ macos/Sources/Ghostty/SurfaceView.swift | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c6816d50c..bee5826ed 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -530,11 +530,13 @@ class AppDelegate: NSObject, } @objc private func ghosttyBellDidRing(_ notification: Notification) { - // Bounce the dock icon if we're not focused. - NSApp.requestUserAttention(.informationalRequest) + if (ghostty.config.bellFeatures.contains(.attention)) { + // Bounce the dock icon if we're not focused. + NSApp.requestUserAttention(.informationalRequest) - // Handle setting the dock badge based on permissions - ghosttyUpdateBadgeForBell() + // Handle setting the dock badge based on permissions + ghosttyUpdateBadgeForBell() + } } private func ghosttyUpdateBadgeForBell() { diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index cce14ca0f..3acb93c25 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -566,6 +566,9 @@ extension Ghostty.Config { let rawValue: CUnsignedInt static let system = BellFeatures(rawValue: 1 << 0) + static let audio = BellFeatures(rawValue: 1 << 1) + static let attention = BellFeatures(rawValue: 1 << 2) + static let title = BellFeatures(rawValue: 1 << 3) } enum MacHidden : String { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 18a8d2f1c..46d379b9c 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -59,7 +59,7 @@ extension Ghostty { var title: String { var result = surfaceView.title - if (surfaceView.bell) { + if (surfaceView.bell && ghostty.config.bellFeatures.contains(.title)) { result = "🔔 \(result)" } From aab00da24200d5eceb907d30339b889aec52a757 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 20:36:34 -0700 Subject: [PATCH 381/642] terminal: fix crash when reflowing grapheme with a spacer head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #7536 When we're reflowing a row and we need to insert a spacer head, we must move to the next row to insert it. Previously, we were setting a spacer head and then copying data into that spacer head, which could lead to corrupt data and an eventual crash. In debug builds this triggers assertion failures but in release builds this would lead to silent corruption and a crash later on. The unit test shows the issue clearly but effectively you need a multi-codepoint grapheme such as `👨‍👨‍👦‍👦` to wrap across a row by changing the columns. --- src/terminal/PageList.zig | 149 +++++++++++++++++++++++++++++++++++--- src/terminal/page.zig | 7 +- 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/src/terminal/PageList.zig b/src/terminal/PageList.zig index a0eb3edd1..9838bfb53 100644 --- a/src/terminal/PageList.zig +++ b/src/terminal/PageList.zig @@ -908,16 +908,6 @@ const ReflowCursor = struct { const cell = &cells[x]; x += 1; - // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={} wide={}", .{ - // src_y, - // x, - // self.y, - // self.x, - // self.page.size.cols, - // cell.content.codepoint, - // cell.wide, - // }); - // Copy cell contents. switch (cell.content_tag) { .codepoint, @@ -937,8 +927,15 @@ const ReflowCursor = struct { }; // Decrement the source position so that when we - // loop we'll process this source cell again. + // loop we'll process this source cell again, + // since we can't copy it into a spacer head. x -= 1; + + // Move to the next row (this sets pending wrap + // which will cause us to wrap on the next + // iteration). + self.cursorForward(); + continue; } else { self.page_cell.* = cell.*; } @@ -990,6 +987,17 @@ const ReflowCursor = struct { self.page_cell.hyperlink = false; self.page_cell.style_id = stylepkg.default_id; + // std.log.warn("\nsrc_y={} src_x={} dst_y={} dst_x={} dst_cols={} cp={X} wide={} page_cell_wide={}", .{ + // src_y, + // x, + // self.y, + // self.x, + // self.page.size.cols, + // cell.content.codepoint, + // cell.wide, + // self.page_cell.wide, + // }); + // Copy grapheme data. if (cell.content_tag == .codepoint_grapheme) { // Copy the graphemes @@ -8375,6 +8383,125 @@ test "PageList resize reflow less cols to wrap a wide char" { } } +test "PageList resize reflow less cols to wrap a multi-codepoint grapheme with a spacer head" { + const testing = std.testing; + const alloc = testing.allocator; + + var s = try init(alloc, 4, 2, 0); + defer s.deinit(); + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + // We want to make the screen look like this: + // + // 👨‍👨‍👦‍👦👨‍👨‍👦‍👦 + + // First family emoji at (0, 0) + { + const rac = page.getRowAndCell(0, 0); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0x1F468 }, // First codepoint of the grapheme + .wide = .wide, + }; + try page.setGraphemes(rac.row, rac.cell, &.{ + 0x200D, 0x1F468, + 0x200D, 0x1F466, + 0x200D, 0x1F466, + }); + } + { + const rac = page.getRowAndCell(1, 0); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0 }, + .wide = .spacer_tail, + }; + } + // Second family emoji at (2, 0) + { + const rac = page.getRowAndCell(2, 0); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0x1F468 }, // First codepoint of the grapheme + .wide = .wide, + }; + try page.setGraphemes(rac.row, rac.cell, &.{ + 0x200D, 0x1F468, + 0x200D, 0x1F466, + 0x200D, 0x1F466, + }); + } + { + const rac = page.getRowAndCell(3, 0); + rac.cell.* = .{ + .content_tag = .codepoint, + .content = .{ .codepoint = 0 }, + .wide = .spacer_tail, + }; + } + } + + // Resize + try s.resize(.{ .cols = 3, .reflow = true }); + try testing.expectEqual(@as(usize, 3), s.cols); + try testing.expectEqual(@as(usize, 2), s.totalRows()); + + { + try testing.expect(s.pages.first == s.pages.last); + const page = &s.pages.first.?.data; + + { + const rac = page.getRowAndCell(0, 0); + try testing.expectEqual(@as(u21, 0x1F468), rac.cell.content.codepoint); + try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide); + + const cps = page.lookupGrapheme(rac.cell).?; + try testing.expectEqual(@as(usize, 6), cps.len); + try testing.expectEqual(@as(u21, 0x200D), cps[0]); + try testing.expectEqual(@as(u21, 0x1F468), cps[1]); + try testing.expectEqual(@as(u21, 0x200D), cps[2]); + try testing.expectEqual(@as(u21, 0x1F466), cps[3]); + try testing.expectEqual(@as(u21, 0x200D), cps[4]); + try testing.expectEqual(@as(u21, 0x1F466), cps[5]); + + // Row should be wrapped + try testing.expect(rac.row.wrap); + } + { + const rac = page.getRowAndCell(1, 0); + try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint); + try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide); + } + { + const rac = page.getRowAndCell(2, 0); + try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint); + try testing.expectEqual(pagepkg.Cell.Wide.spacer_head, rac.cell.wide); + } + + { + const rac = page.getRowAndCell(0, 0); + try testing.expectEqual(@as(u21, 0x1F468), rac.cell.content.codepoint); + try testing.expectEqual(pagepkg.Cell.Wide.wide, rac.cell.wide); + + const cps = page.lookupGrapheme(rac.cell).?; + try testing.expectEqual(@as(usize, 6), cps.len); + try testing.expectEqual(@as(u21, 0x200D), cps[0]); + try testing.expectEqual(@as(u21, 0x1F468), cps[1]); + try testing.expectEqual(@as(u21, 0x200D), cps[2]); + try testing.expectEqual(@as(u21, 0x1F466), cps[3]); + try testing.expectEqual(@as(u21, 0x200D), cps[4]); + try testing.expectEqual(@as(u21, 0x1F466), cps[5]); + } + { + const rac = page.getRowAndCell(1, 1); + try testing.expectEqual(@as(u21, 0), rac.cell.content.codepoint); + try testing.expectEqual(pagepkg.Cell.Wide.spacer_tail, rac.cell.wide); + } + } +} + test "PageList resize reflow less cols copy kitty placeholder" { const testing = std.testing; const alloc = testing.allocator; diff --git a/src/terminal/page.zig b/src/terminal/page.zig index d7f252af1..fea16c28b 100644 --- a/src/terminal/page.zig +++ b/src/terminal/page.zig @@ -1316,7 +1316,12 @@ pub const Page = struct { /// Set the graphemes for the given cell. This asserts that the cell /// has no graphemes set, and only contains a single codepoint. - pub fn setGraphemes(self: *Page, row: *Row, cell: *Cell, cps: []u21) GraphemeError!void { + pub fn setGraphemes( + self: *Page, + row: *Row, + cell: *Cell, + cps: []const u21, + ) GraphemeError!void { defer self.assertIntegrity(); assert(cell.codepoint() > 0); From ea0766e62b4c63e7afacfb6d62cee554862061c6 Mon Sep 17 00:00:00 2001 From: Leorize Date: Sat, 7 Jun 2025 11:57:26 -0500 Subject: [PATCH 382/642] gtk/CommandPalette: prevent leaks on initialization * Deallocate the builder after use * Don't hold a reference to `Command` after appending to `GListStore` --- src/apprt/gtk/CommandPalette.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/apprt/gtk/CommandPalette.zig b/src/apprt/gtk/CommandPalette.zig index fda2c5ca8..a99db78d7 100644 --- a/src/apprt/gtk/CommandPalette.zig +++ b/src/apprt/gtk/CommandPalette.zig @@ -43,6 +43,7 @@ pub fn init(self: *CommandPalette, window: *Window) !void { _ = Command.getGObjectType(); var builder = Builder.init("command-palette", 1, 5); + defer builder.deinit(); self.* = .{ .window = window, @@ -120,7 +121,9 @@ pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !voi command, config.keybind.set, ); - self.source.append(cmd.as(gobject.Object)); + const cmd_ref = cmd.as(gobject.Object); + self.source.append(cmd_ref); + cmd_ref.unref(); } } From 42bafe9d599799e47cd11a421282bfb4a40a6af6 Mon Sep 17 00:00:00 2001 From: Leorize Date: Sat, 7 Jun 2025 12:44:21 -0500 Subject: [PATCH 383/642] flatpak: detach process tracking thread after spawn This makes sure the underlying thread implementation know to free resources the moment the thread is no longer necessary, preventing leaks from not manually collecting the thread. --- src/os/flatpak.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig index 7b92a8ba9..eaff529b0 100644 --- a/src/os/flatpak.zig +++ b/src/os/flatpak.zig @@ -112,6 +112,8 @@ pub const FlatpakHostCommand = struct { pub fn spawn(self: *FlatpakHostCommand, alloc: Allocator) !u32 { const thread = try std.Thread.spawn(.{}, threadMain, .{ self, alloc }); thread.setName("flatpak-host-command") catch {}; + // We don't track this thread, it will terminate on its own on command exit + thread.detach(); // Wait for the process to start or error. self.state_mutex.lock(); From 53c2874667f234e7dd054aeca81e8a469b050359 Mon Sep 17 00:00:00 2001 From: Leorize Date: Sat, 7 Jun 2025 13:26:21 -0500 Subject: [PATCH 384/642] flatpak: free GError after use --- src/os/flatpak.zig | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/os/flatpak.zig b/src/os/flatpak.zig index eaff529b0..7bd84bc27 100644 --- a/src/os/flatpak.zig +++ b/src/os/flatpak.zig @@ -234,9 +234,10 @@ pub const FlatpakHostCommand = struct { }; // Get our bus connection. - var g_err: [*c]c.GError = null; + var g_err: ?*c.GError = null; + defer if (g_err) |ptr| c.g_error_free(ptr); const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse { - log.warn("signal error getting bus: {s}", .{g_err.*.message}); + log.warn("signal error getting bus: {s}", .{g_err.?.*.message}); return Error.FlatpakSetupFail; }; defer c.g_object_unref(bus); @@ -260,7 +261,7 @@ pub const FlatpakHostCommand = struct { &g_err, ); if (g_err != null) { - log.warn("signal send error: {s}", .{g_err.*.message}); + log.warn("signal send error: {s}", .{g_err.?.*.message}); return; } defer c.g_variant_unref(reply); @@ -280,9 +281,10 @@ pub const FlatpakHostCommand = struct { // Get our bus connection. This has to remain active until we exit // the thread otherwise our signals won't be called. - var g_err: [*c]c.GError = null; + var g_err: ?*c.GError = null; + defer if (g_err) |ptr| c.g_error_free(ptr); const bus = c.g_bus_get_sync(c.G_BUS_TYPE_SESSION, null, &g_err) orelse { - log.warn("spawn error getting bus: {s}", .{g_err.*.message}); + log.warn("spawn error getting bus: {s}", .{g_err.?.*.message}); self.updateState(.{ .err = {} }); return; }; @@ -310,7 +312,8 @@ pub const FlatpakHostCommand = struct { bus: *c.GDBusConnection, loop: *c.GMainLoop, ) !void { - var err: [*c]c.GError = null; + var err: ?*c.GError = null; + defer if (err) |ptr| c.g_error_free(ptr); var arena_allocator = std.heap.ArenaAllocator.init(alloc); defer arena_allocator.deinit(); const arena = arena_allocator.allocator(); @@ -319,15 +322,15 @@ pub const FlatpakHostCommand = struct { const fd_list = c.g_unix_fd_list_new(); defer c.g_object_unref(fd_list); if (c.g_unix_fd_list_append(fd_list, self.stdin, &err) < 0) { - log.warn("error adding fd: {s}", .{err.*.message}); + log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } if (c.g_unix_fd_list_append(fd_list, self.stdout, &err) < 0) { - log.warn("error adding fd: {s}", .{err.*.message}); + log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } if (c.g_unix_fd_list_append(fd_list, self.stderr, &err) < 0) { - log.warn("error adding fd: {s}", .{err.*.message}); + log.warn("error adding fd: {s}", .{err.?.*.message}); return Error.FlatpakSetupFail; } @@ -407,7 +410,7 @@ pub const FlatpakHostCommand = struct { null, &err, ) orelse { - log.warn("Flatpak.HostCommand failed: {s}", .{err.*.message}); + log.warn("Flatpak.HostCommand failed: {s}", .{err.?.*.message}); return Error.FlatpakRPCFail; }; defer c.g_variant_unref(reply); From 41ee578b7a0ed7226a06d5d77fa672b30b502a46 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 12:36:12 -0700 Subject: [PATCH 385/642] macos: quick terminal restores previous size when exiting final surface This fixes a regression from the new split work last week, but it was also probably an issue before that in a slightly different way. With the new split work, the quick terminal was becoming unusable when the final surface explicitly `exit`-ed, because AppKit/SwiftUI would resize the window to a very small size and you couldn't see the new terminal on the next toggle. Prior to this, I think the quick terminal would've reverted to its original size but I'm not sure (even if the user resized it manually). This commit saves the size of the quick terminal at the point all surfaces are exited and restores it when the quick terminal is shown the next time with a new initial surface. --- .../QuickTerminalController.swift | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 0dcfce204..8c86c2531 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -21,6 +21,14 @@ class QuickTerminalController: BaseTerminalController { // The active space when the quick terminal was last shown. private var previousActiveSpace: CGSSpace? = nil + /// The window frame saved when the quick terminal's surface tree becomes empty. + /// + /// This preserves the user's window size and position when all terminal surfaces + /// are closed (e.g., via the `exit` command). When a new surface is created, + /// the window will be restored to this frame, preventing SwiftUI from resetting + /// the window to its default minimum size. + private var lastClosedFrame: NSRect? = nil + /// Non-nil if we have hidden dock state. private var hiddenDock: HiddenDock? = nil @@ -190,6 +198,12 @@ class QuickTerminalController: BaseTerminalController { // If our surface tree is nil then we animate the window out. if (to.isEmpty) { + // Save the current window frame before animating out. This preserves + // the user's preferred window size and position for when the quick + // terminal is reactivated with a new surface. Without this, SwiftUI + // would reset the window to its minimum content size. + lastClosedFrame = window?.frame + animateOut() } } @@ -230,9 +244,6 @@ class QuickTerminalController: BaseTerminalController { // Set previous active space self.previousActiveSpace = CGSSpace.active() - // Animate the window in - animateWindowIn(window: window, from: position) - // If our surface tree is empty then we initialize a new terminal. The surface // tree can be empty if for example we run "exit" in the terminal and force // animate out. @@ -241,7 +252,16 @@ class QuickTerminalController: BaseTerminalController { let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil) surfaceTree = SplitTree(view: view) focusedSurface = view + + // Restore our previous frame if we have one + if let lastClosedFrame { + window.setFrame(lastClosedFrame, display: false) + self.lastClosedFrame = nil + } } + + // Animate the window in + animateWindowIn(window: window, from: position) } func animateOut() { From 493b1f53506263c11d763ae19a0ca83f1b8bd0e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Jun 2025 15:04:11 -0700 Subject: [PATCH 386/642] wip: undo --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++ macos/Sources/App/macOS/AppDelegate.swift | 10 +++ macos/Sources/App/macOS/MainMenu.xib | 15 ++++ .../Terminal/BaseTerminalController.swift | 74 ++++++++++++++++--- macos/Sources/Helpers/ExpiringTarget.swift | 53 +++++++++++++ .../Extensions/Duration+Extension.swift | 8 ++ 6 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 macos/Sources/Helpers/ExpiringTarget.swift create mode 100644 macos/Sources/Helpers/Extensions/Duration+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 62cb079bf..153ec8e6f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -62,6 +62,8 @@ A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; }; A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */; }; A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; }; + A586366D2DF25C2500E04A10 /* ExpiringTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366C2DF25C1C00E04A10 /* ExpiringTarget.swift */; }; + A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366E2DF25D8300E04A10 /* Duration+Extension.swift */; }; A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; }; A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; @@ -168,6 +170,8 @@ A586365E2DEE6C2100E04A10 /* SplitTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitTree.swift; sourceTree = ""; }; A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSplitTreeView.swift; sourceTree = ""; }; A586366A2DF0A98900E04A10 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; + A586366C2DF25C1C00E04A10 /* ExpiringTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTarget.swift; sourceTree = ""; }; + A586366E2DF25D8300E04A10 /* Duration+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extension.swift"; sourceTree = ""; }; A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = ""; }; A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -290,6 +294,7 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( + A586366C2DF25C1C00E04A10 /* ExpiringTarget.swift */, A58636692DF0A98100E04A10 /* Extensions */, A5874D9B2DAD781100E83852 /* Private */, A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, @@ -432,6 +437,7 @@ isa = PBXGroup; children = ( A586366A2DF0A98900E04A10 /* Array+Extension.swift */, + A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, @@ -686,6 +692,7 @@ A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, + A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */, A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */, A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, @@ -713,6 +720,7 @@ A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CA378E2D31D6C300931030 /* Weak.swift in Sources */, + A586366D2DF25C2500E04A10 /* ExpiringTarget.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index fd25ef358..d12b2efd2 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -36,6 +36,8 @@ class AppDelegate: NSObject, @IBOutlet private var menuCloseWindow: NSMenuItem? @IBOutlet private var menuCloseAllWindows: NSMenuItem? + @IBOutlet private var menuUndo: NSMenuItem? + @IBOutlet private var menuRedo: NSMenuItem? @IBOutlet private var menuCopy: NSMenuItem? @IBOutlet private var menuPaste: NSMenuItem? @IBOutlet private var menuPasteSelection: NSMenuItem? @@ -88,6 +90,9 @@ class AppDelegate: NSObject, /// Manages our terminal windows. let terminalManager: TerminalManager + /// The global undo manager for app-level state such as window restoration. + lazy var undoManager = UndoManager() + /// Our quick terminal. This starts out uninitialized and only initializes if used. private var quickController: QuickTerminalController? = nil @@ -393,6 +398,11 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown) syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp) + // TODO: sync + menuUndo?.keyEquivalent = "z" + menuUndo?.keyEquivalentModifierMask = [.command] + menuRedo?.keyEquivalent = "z" + menuRedo?.keyEquivalentModifierMask = [.command, .shift] syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy) syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 828e82bd0..7130d544e 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -40,6 +40,7 @@ + @@ -57,6 +58,7 @@ + @@ -204,6 +206,19 @@ + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index ea849bb4a..cd7ceffbb 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -75,6 +75,13 @@ class BaseTerminalController: NSWindowController, /// The cancellables related to our focused surface. private var focusedSurfaceCancellables: Set = [] + /// The undo manager for this controller is the undo manager of the window, + /// which we set via the delegate method. + override var undoManager: UndoManager? { + // This should be set via the delegate method windowWillReturnUndoManager + window?.undoManager + } + struct SavedFrame { let window: NSRect let screen: NSRect @@ -261,6 +268,9 @@ class BaseTerminalController: NSWindowController, let oldFocused = focusedSurface let focused = node.contains { $0 == focusedSurface } + // Keep track of the old tree for undo management. + let oldTree = surfaceTree + // Remove the node from the tree surfaceTree = surfaceTree.remove(node) @@ -270,6 +280,32 @@ class BaseTerminalController: NSWindowController, Ghostty.moveFocus(to: nextTarget, from: oldFocused) } } + + // Setup our undo + if let undoManager { + undoManager.setActionName("Close Terminal") + undoManager.registerUndo(withTarget: ExpiringTarget( + with: .seconds(5), + in: undoManager, + )) { [weak self] v in + guard let self else { return } + self.surfaceTree = oldTree + if let oldFocused { + DispatchQueue.main.async { + Ghostty.moveFocus(to: oldFocused, from: self.focusedSurface) + } + } + + undoManager.registerUndo(withTarget: NSObject()) { [weak self] _ in + self?.closeSurface( + node.leftmostLeaf(), + withConfirmation: node.contains { + $0.needsConfirmQuit + } + ) + } + } + } } // MARK: Notifications @@ -346,19 +382,25 @@ class BaseTerminalController: NSWindowController, } @objc private func ghosttyDidCloseSurface(_ notification: Notification) { - // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } + closeSurface( + target, + withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false, + ) + } + + /// Close a surface view, requesting confirmation if necessary. + /// + /// This will also insert the proper undo stack information in. + private func closeSurface( + _ target: Ghostty.SurfaceView, + withConfirmation: Bool = true, + ) { + // The target must be within our tree guard let node = surfaceTree.root?.node(view: target) else { return } - var processAlive = false - if let valueAny = notification.userInfo?["process_alive"] { - if let value = valueAny as? Bool { - processAlive = value - } - } - // If the child process is not alive, then we exit immediately - guard processAlive else { + guard withConfirmation else { removeSurfaceAndMoveFocus(node) return } @@ -405,7 +447,8 @@ class BaseTerminalController: NSWindowController, // Do the split do { - surfaceTree = try surfaceTree.insert(view: newView, at: oldView, direction: splitDirection) + let newTree = try surfaceTree.insert(view: newView, at: oldView, direction: splitDirection) + surfaceTree = newTree } catch { // If splitting fails for any reason (it should not), then we just log // and return. The new view we created will be deinitialized and its @@ -414,6 +457,7 @@ class BaseTerminalController: NSWindowController, return } + // Once we've split, we need to move focus to the new split Ghostty.moveFocus(to: newView, from: oldView) } @@ -732,6 +776,11 @@ class BaseTerminalController: NSWindowController, // MARK: NSWindowController override func windowDidLoad() { + super.windowDidLoad() + + // Setup our undo manager. + + // Everything beyond here is setting up the window guard let window else { return } // If there is a hardcoded title in the configuration, we set that @@ -818,6 +867,11 @@ class BaseTerminalController: NSWindowController, windowFrameDidChange() } + func windowWillReturnUndoManager(_ window: NSWindow) -> UndoManager? { + guard let appDelegate = NSApplication.shared.delegate as? AppDelegate else { return nil } + return appDelegate.undoManager + } + // MARK: First Responder @IBAction func close(_ sender: Any) { diff --git a/macos/Sources/Helpers/ExpiringTarget.swift b/macos/Sources/Helpers/ExpiringTarget.swift new file mode 100644 index 000000000..d24021495 --- /dev/null +++ b/macos/Sources/Helpers/ExpiringTarget.swift @@ -0,0 +1,53 @@ +import AppKit + +/// A target object for UndoManager that automatically expires after a specified duration. +/// +/// ExpiringTarget holds a reference to a target object and removes all undo actions +/// associated with itself from the UndoManager when the timer expires. This is useful +/// for creating temporary undo operations that should not persist beyond a certain time. +/// +/// The parameter T can be used to retain a reference to some target value +/// that can be used in the undo operation. The target is released when the timer expires. +/// +/// - Parameter T: The type of the target object, constrained to AnyObject +class ExpiringTarget { + private(set) var target: T? + private var timer: Timer? + private weak var undoManager: UndoManager? + + /// Creates an expiring target that will automatically remove undo actions after the specified duration. + /// + /// - Parameters: + /// - target: The target object to hold weakly. Defaults to nil. + /// - duration: The time after which the target should expire + /// - undoManager: The UndoManager from which to remove actions when expired + init(_ target: T? = nil, with duration: Duration, in undoManager: UndoManager) { + self.target = target + self.undoManager = undoManager + self.timer = Timer.scheduledTimer( + withTimeInterval: duration.timeInterval, + repeats: false) { _ in + self.expire() + } + } + + /// Manually expires the target, removing all associated undo actions and invalidating the timer. + /// + /// This method is called automatically when the timer fires, but can also be called manually + /// to expire the target before the timer duration has elapsed. + func expire() { + target = nil + undoManager?.removeAllActions(withTarget: self) + timer?.invalidate() + } + + deinit { + expire() + } +} + +extension ExpiringTarget where T == NSObject { + convenience init(with duration: Duration, in undoManager: UndoManager) { + self.init(nil, with: duration, in: undoManager) + } +} diff --git a/macos/Sources/Helpers/Extensions/Duration+Extension.swift b/macos/Sources/Helpers/Extensions/Duration+Extension.swift new file mode 100644 index 000000000..43eca6b79 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Duration+Extension.swift @@ -0,0 +1,8 @@ +import Foundation + +extension Duration { + var timeInterval: TimeInterval { + return TimeInterval(self.components.seconds) + + TimeInterval(self.components.attoseconds) / 1_000_000_000_000_000_000 + } +} From 6d32b01c6498a4fab34293cd673dc4437798ede8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Jun 2025 21:28:49 -0700 Subject: [PATCH 387/642] macos: implement a custom ExpiringUndoManager, setup undo for new/close --- macos/Ghostty.xcodeproj/project.pbxproj | 8 +- macos/Sources/App/macOS/AppDelegate.swift | 2 +- .../Terminal/BaseTerminalController.swift | 79 ++++++++-- .../Sources/Ghostty/SurfaceView_AppKit.swift | 2 + macos/Sources/Helpers/ExpiringTarget.swift | 53 ------- .../Sources/Helpers/ExpiringUndoManager.swift | 137 ++++++++++++++++++ 6 files changed, 207 insertions(+), 74 deletions(-) delete mode 100644 macos/Sources/Helpers/ExpiringTarget.swift create mode 100644 macos/Sources/Helpers/ExpiringUndoManager.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 153ec8e6f..67f1784ac 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -62,8 +62,8 @@ A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586365E2DEE6C2100E04A10 /* SplitTree.swift */; }; A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */; }; A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; }; - A586366D2DF25C2500E04A10 /* ExpiringTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366C2DF25C1C00E04A10 /* ExpiringTarget.swift */; }; A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366E2DF25D8300E04A10 /* Duration+Extension.swift */; }; + A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */; }; A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; }; A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; @@ -170,8 +170,8 @@ A586365E2DEE6C2100E04A10 /* SplitTree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitTree.swift; sourceTree = ""; }; A58636652DEF963F00E04A10 /* TerminalSplitTreeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSplitTreeView.swift; sourceTree = ""; }; A586366A2DF0A98900E04A10 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; - A586366C2DF25C1C00E04A10 /* ExpiringTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringTarget.swift; sourceTree = ""; }; A586366E2DF25D8300E04A10 /* Duration+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extension.swift"; sourceTree = ""; }; + A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringUndoManager.swift; sourceTree = ""; }; A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = ""; }; A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -294,7 +294,6 @@ A534263D2A7DCBB000EBB7A2 /* Helpers */ = { isa = PBXGroup; children = ( - A586366C2DF25C1C00E04A10 /* ExpiringTarget.swift */, A58636692DF0A98100E04A10 /* Extensions */, A5874D9B2DAD781100E83852 /* Private */, A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, @@ -303,6 +302,7 @@ A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, A5D0AF3C2B37804400D21823 /* CodableBridge.swift */, + A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */, A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, @@ -708,6 +708,7 @@ A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, + A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */, A59630972AEE163600D64628 /* HostingWindow.swift in Sources */, A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */, A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */, @@ -720,7 +721,6 @@ A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CA378E2D31D6C300931030 /* Weak.swift in Sources */, - A586366D2DF25C2500E04A10 /* ExpiringTarget.swift in Sources */, A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index d12b2efd2..eae8dd121 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -91,7 +91,7 @@ class AppDelegate: NSObject, let terminalManager: TerminalManager /// The global undo manager for app-level state such as window restoration. - lazy var undoManager = UndoManager() + lazy var undoManager = ExpiringUndoManager() /// Our quick terminal. This starts out uninitialized and only initializes if used. private var quickController: QuickTerminalController? = nil diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index cd7ceffbb..6cc6b2ec8 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -75,11 +75,25 @@ class BaseTerminalController: NSWindowController, /// The cancellables related to our focused surface. private var focusedSurfaceCancellables: Set = [] + /// The time that undo/redo operations that contain running ptys are valid for. + private var undoExpiration: Duration { + .seconds(5) + } + /// The undo manager for this controller is the undo manager of the window, /// which we set via the delegate method. - override var undoManager: UndoManager? { + override var undoManager: ExpiringUndoManager? { // This should be set via the delegate method windowWillReturnUndoManager - window?.undoManager + if let result = window?.undoManager as? ExpiringUndoManager { + return result + } + + // If the window one isn't set, we fallback to our global one. + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + return appDelegate.undoManager + } + + return nil } struct SavedFrame { @@ -173,7 +187,7 @@ class BaseTerminalController: NSWindowController, deinit { NotificationCenter.default.removeObserver(self) - + undoManager?.removeAllActions(withTarget: self) if let eventMonitor { NSEvent.removeMonitor(eventMonitor) } @@ -284,20 +298,20 @@ class BaseTerminalController: NSWindowController, // Setup our undo if let undoManager { undoManager.setActionName("Close Terminal") - undoManager.registerUndo(withTarget: ExpiringTarget( - with: .seconds(5), - in: undoManager, - )) { [weak self] v in - guard let self else { return } - self.surfaceTree = oldTree + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration) { target in + target.surfaceTree = oldTree if let oldFocused { DispatchQueue.main.async { - Ghostty.moveFocus(to: oldFocused, from: self.focusedSurface) + Ghostty.moveFocus(to: oldFocused, from: target.focusedSurface) } } - undoManager.registerUndo(withTarget: NSObject()) { [weak self] _ in - self?.closeSurface( + undoManager.registerUndo( + withTarget: target, + expiresAfter: target.undoExpiration) { target in + target.closeSurface( node.leftmostLeaf(), withConfirmation: node.contains { $0.needsConfirmQuit @@ -446,9 +460,12 @@ class BaseTerminalController: NSWindowController, let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config) // Do the split + let newTree: SplitTree do { - let newTree = try surfaceTree.insert(view: newView, at: oldView, direction: splitDirection) - surfaceTree = newTree + newTree = try surfaceTree.insert( + view: newView, + at: oldView, + direction: splitDirection) } catch { // If splitting fails for any reason (it should not), then we just log // and return. The new view we created will be deinitialized and its @@ -457,9 +474,36 @@ class BaseTerminalController: NSWindowController, return } + // Keep track of the old tree for undo + let oldTree = surfaceTree - // Once we've split, we need to move focus to the new split - Ghostty.moveFocus(to: newView, from: oldView) + // Setup our new split tree + surfaceTree = newTree + DispatchQueue.main.async { + Ghostty.moveFocus(to: newView, from: oldView) + } + + // Setup our undo + if let undoManager { + undoManager.setActionName("New Split") + undoManager.registerUndo( + withTarget: self, + expiresAfter: undoExpiration) { target in + target.surfaceTree = oldTree + DispatchQueue.main.async { + Ghostty.moveFocus(to: oldView, from: target.focusedSurface) + } + + undoManager.registerUndo( + withTarget: target, + expiresAfter: target.undoExpiration) { target in + target.surfaceTree = newTree + DispatchQueue.main.async { + Ghostty.moveFocus(to: newView, from: target.focusedSurface) + } + } + } + } } @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { @@ -836,6 +880,9 @@ class BaseTerminalController: NSWindowController, // the view and the window so we had to nil this out to break it but I think this // may now be resolved. We should verify that no memory leaks and we can remove this. window.contentView = nil + + // Make sure we clean up all our undos + window.undoManager?.removeAllActions(withTarget: self) } func windowDidBecomeKey(_ notification: Notification) { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 682efa947..6e35f40d1 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -287,6 +287,8 @@ extension Ghostty { if let surface = self.surface { ghostty_surface_free(surface) } + + Ghostty.logger.warning("WOW close") } func focusDidChange(_ focused: Bool) { diff --git a/macos/Sources/Helpers/ExpiringTarget.swift b/macos/Sources/Helpers/ExpiringTarget.swift deleted file mode 100644 index d24021495..000000000 --- a/macos/Sources/Helpers/ExpiringTarget.swift +++ /dev/null @@ -1,53 +0,0 @@ -import AppKit - -/// A target object for UndoManager that automatically expires after a specified duration. -/// -/// ExpiringTarget holds a reference to a target object and removes all undo actions -/// associated with itself from the UndoManager when the timer expires. This is useful -/// for creating temporary undo operations that should not persist beyond a certain time. -/// -/// The parameter T can be used to retain a reference to some target value -/// that can be used in the undo operation. The target is released when the timer expires. -/// -/// - Parameter T: The type of the target object, constrained to AnyObject -class ExpiringTarget { - private(set) var target: T? - private var timer: Timer? - private weak var undoManager: UndoManager? - - /// Creates an expiring target that will automatically remove undo actions after the specified duration. - /// - /// - Parameters: - /// - target: The target object to hold weakly. Defaults to nil. - /// - duration: The time after which the target should expire - /// - undoManager: The UndoManager from which to remove actions when expired - init(_ target: T? = nil, with duration: Duration, in undoManager: UndoManager) { - self.target = target - self.undoManager = undoManager - self.timer = Timer.scheduledTimer( - withTimeInterval: duration.timeInterval, - repeats: false) { _ in - self.expire() - } - } - - /// Manually expires the target, removing all associated undo actions and invalidating the timer. - /// - /// This method is called automatically when the timer fires, but can also be called manually - /// to expire the target before the timer duration has elapsed. - func expire() { - target = nil - undoManager?.removeAllActions(withTarget: self) - timer?.invalidate() - } - - deinit { - expire() - } -} - -extension ExpiringTarget where T == NSObject { - convenience init(with duration: Duration, in undoManager: UndoManager) { - self.init(nil, with: duration, in: undoManager) - } -} diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift new file mode 100644 index 000000000..3eda56182 --- /dev/null +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -0,0 +1,137 @@ +/// An UndoManager subclass that supports registering undo operations that automatically expire after a specified duration. +/// +/// This class extends the standard UndoManager to add time-based expiration for undo operations. +/// When an undo operation expires, it is automatically removed from the undo stack and cannot be invoked. +/// +/// Example usage: +/// ```swift +/// let undoManager = ExpiringUndoManager() +/// undoManager.registerUndo(withTarget: myObject, expiresAfter: .seconds(30)) { target in +/// // Undo operation that expires after 30 seconds +/// target.restorePreviousState() +/// } +/// ``` +class ExpiringUndoManager: UndoManager { + /// The set of expiring targets so we can properly clean them up when removeAllActions + /// is called with the real target. + private lazy var expiringTargets: Set = [] + + /// Registers an undo operation that automatically expires after the specified duration. + /// + /// - Parameters: + /// - target: The target object for the undo operation. The undo operation will be removed + /// if this object is deallocated before the operation is invoked. + /// - duration: The duration after which the undo operation should expire and be removed from the undo stack. + /// - handler: The closure to execute when the undo operation is invoked. The closure receives + /// the target object as its parameter. + func registerUndo( + withTarget target: TargetType, + expiresAfter duration: Duration, + handler: @escaping (TargetType) -> Void + ) { + let expiringTarget = ExpiringTarget( + target, + expiresAfter: duration, + in: self) + expiringTargets.insert(expiringTarget) + + super.registerUndo(withTarget: expiringTarget) { [weak self] expiringTarget in + self?.expiringTargets.remove(expiringTarget) + guard let target = expiringTarget.target as? TargetType else { return } + handler(target) + } + } + + /// Removes all undo and redo operations from the undo manager. + /// + /// This override ensures that all expiring targets are also cleared when + /// the undo manager is reset. + override func removeAllActions() { + super.removeAllActions() + expiringTargets = [] + } + + /// Removes all undo and redo operations involving the specified target. + /// + /// This override ensures that when actions are removed for a target, any associated + /// expiring targets are also properly cleaned up. + /// + /// - Parameter target: The target object whose actions should be removed. + override func removeAllActions(withTarget target: Any) { + // Call super to handle standard removal + super.removeAllActions(withTarget: target) + + if !(target is ExpiringTarget) { + // Find and remove any ExpiringTarget instances that wrap this target. + expiringTargets + .filter { $0.target == nil || $0.target === (target as AnyObject) } + .forEach { + // Technically they'll always expire when they get deinitialized + // but we want to make sure it happens right now. + $0.expire() + expiringTargets.remove($0) + } + } + } +} + +/// A target object for ExpiringUndoManager that removes itself from the +/// undo manager after it expires. +/// +/// This class acts as a proxy for the real target object in undo operations. +/// It holds a weak reference to the actual target and automatically removes +/// all associated undo operations when either: +/// - The specified duration expires +/// - The ExpiringTarget instance is deallocated +/// - The expire() method is called manually +private class ExpiringTarget { + /// The actual target object for the undo operation, held weakly to avoid retain cycles. + private(set) weak var target: AnyObject? + + /// Timer that triggers expiration after the specified duration. + private var timer: Timer? + + /// The undo manager from which to remove actions when this target expires. + private weak var undoManager: UndoManager? + + /// Creates an expiring target that will automatically remove undo actions after the specified duration. + /// + /// - Parameters: + /// - target: The target object to hold weakly. + /// - duration: The time after which the target should expire. + /// - undoManager: The UndoManager from which to remove actions when expired. + init(_ target: AnyObject? = nil, expiresAfter duration: Duration, in undoManager: UndoManager) { + self.target = target + self.undoManager = undoManager + self.timer = Timer.scheduledTimer( + withTimeInterval: duration.timeInterval, + repeats: false) { [weak self] _ in + self?.expire() + } + } + + /// Manually expires the target, removing all associated undo actions and invalidating the timer. + /// + /// This method is called automatically when the timer fires, but can also be called manually + /// to expire the target before the timer duration has elapsed. + func expire() { + target = nil + undoManager?.removeAllActions(withTarget: self) + timer?.invalidate() + timer = nil + } + + deinit { + expire() + } +} + +extension ExpiringTarget: Hashable, Equatable { + static func == (lhs: ExpiringTarget, rhs: ExpiringTarget) -> Bool { + return lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} From f571519157bc2eaf3dcf7995989c7d8266a8ddc6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 5 Jun 2025 21:43:41 -0700 Subject: [PATCH 388/642] macos: setup undo responders at the AppDelegate level --- macos/Sources/App/macOS/AppDelegate.swift | 24 +++++++++++++++++++ .../Features/Terminal/TerminalManager.swift | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index eae8dd121..1fce7d665 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -892,6 +892,14 @@ class AppDelegate: NSObject, NSApplication.shared.arrangeInFront(sender) } + @IBAction func undo(_ sender: Any?) { + undoManager.undo() + } + + @IBAction func redo(_ sender: Any?) { + undoManager.redo() + } + private struct DerivedConfig { let initialWindow: Bool let shouldQuitAfterLastWindowClosed: Bool @@ -981,6 +989,22 @@ extension AppDelegate: NSMenuItemValidation { // terminal window (not quick terminal). return NSApp.keyWindow is TerminalWindow + case #selector(undo(_:)): + if undoManager.canUndo { + item.title = "Undo \(undoManager.undoActionName)" + } else { + item.title = "Undo" + } + return undoManager.canUndo + + case #selector(redo(_:)): + if undoManager.canRedo { + item.title = "Redo \(undoManager.redoActionName)" + } else { + item.title = "Redo" + } + return undoManager.canRedo + default: return true } diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 805ae6e93..050bc5563 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -228,7 +228,7 @@ class TerminalManager { // Ensure any publishers we have are cancelled w.closePublisher.cancel() - + // If we remove a window, we reset the cascade point to the key window so that // the next window cascade's from that one. if let focusedWindow = NSApplication.shared.keyWindow { From 104cc2adfee94062c735611b0dbacf7332dff58d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 07:52:31 -0700 Subject: [PATCH 389/642] macos: basic undo close window, not very robust yet --- macos/Sources/Features/Splits/SplitTree.swift | 38 ++++++++ .../Terminal/BaseTerminalController.swift | 31 +++--- .../Terminal/TerminalController.swift | 95 ++++++++++++++++++- 3 files changed, 147 insertions(+), 17 deletions(-) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index cbd440124..394cd1089 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -107,6 +107,18 @@ extension SplitTree { self.init(root: .leaf(view: view), zoomed: nil) } + /// Checks if the tree contains the specified node. + /// + /// Note that SplitTree implements Sequence on views so there's already a `contains` + /// for views too. + /// + /// - Parameter node: The node to search for in the tree + /// - Returns: True if the node exists in the tree, false otherwise + func contains(_ node: Node) -> Bool { + guard let root else { return false } + return root.path(to: node) != nil + } + /// Insert a new view at the given view point by creating a split in the given direction. /// This will always reset the zoomed state of the tree. func insert(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self { @@ -1078,3 +1090,29 @@ extension SplitTree.Node: Sequence { return leaves().makeIterator() } } + +// MARK: SplitTree Collection + +extension SplitTree: Collection { + typealias Index = Int + typealias Element = ViewType + + var startIndex: Int { + return 0 + } + + var endIndex: Int { + return root?.leaves().count ?? 0 + } + + subscript(position: Int) -> ViewType { + precondition(position >= 0 && position < endIndex, "Index out of bounds") + let leaves = root?.leaves() ?? [] + return leaves[position] + } + + func index(after i: Int) -> Int { + precondition(i < endIndex, "Cannot increment index beyond endIndex") + return i + 1 + } +} diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 6cc6b2ec8..e34a44941 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -76,7 +76,7 @@ class BaseTerminalController: NSWindowController, private var focusedSurfaceCancellables: Set = [] /// The time that undo/redo operations that contain running ptys are valid for. - private var undoExpiration: Duration { + var undoExpiration: Duration { .seconds(5) } @@ -277,7 +277,11 @@ class BaseTerminalController: NSWindowController, } /// Remove a node from the surface tree and move focus appropriately. - private func removeSurfaceAndMoveFocus(_ node: SplitTree.Node) { + /// + /// This also updates the undo manager to support restoring this node. + /// + /// This does no confirmation and assumes confirmation is already done. + private func removeSurfaceNode(_ node: SplitTree.Node) { let nextTarget = findNextFocusTargetAfterClosing(node: node) let oldFocused = focusedSurface let focused = node.contains { $0 == focusedSurface } @@ -311,8 +315,8 @@ class BaseTerminalController: NSWindowController, undoManager.registerUndo( withTarget: target, expiresAfter: target.undoExpiration) { target in - target.closeSurface( - node.leftmostLeaf(), + target.closeSurfaceNode( + node, withConfirmation: node.contains { $0.needsConfirmQuit } @@ -397,25 +401,26 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidCloseSurface(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } - closeSurface( - target, + guard let node = surfaceTree.root?.node(view: target) else { return } + closeSurfaceNode( + node, withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false, ) } - /// Close a surface view, requesting confirmation if necessary. + /// Close a surface node (which may contain splits), requesting confirmation if necessary. /// /// This will also insert the proper undo stack information in. - private func closeSurface( - _ target: Ghostty.SurfaceView, + func closeSurfaceNode( + _ node: SplitTree.Node, withConfirmation: Bool = true, ) { - // The target must be within our tree - guard let node = surfaceTree.root?.node(view: target) else { return } + // This node must be part of our tree + guard surfaceTree.contains(node) else { return } // If the child process is not alive, then we exit immediately guard withConfirmation else { - removeSurfaceAndMoveFocus(node) + removeSurfaceNode(node) return } @@ -429,7 +434,7 @@ class BaseTerminalController: NSWindowController, informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." ) { [weak self] in if let self { - self.removeSurfaceAndMoveFocus(node) + self.removeSurfaceNode(node) } } } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index baae90068..554f7699b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -386,6 +386,93 @@ class TerminalController: BaseTerminalController { return frame } + /// This is called anytime a node in the surface tree is being removed. + override func closeSurfaceNode( + _ node: SplitTree.Node, + withConfirmation: Bool = true + ) { + // If this isn't the root then we're dealing with a split closure. + if surfaceTree.root != node { + super.closeSurfaceNode(node, withConfirmation: withConfirmation) + return + } + + // More than 1 window means we have tabs and we're closing a tab + if window?.tabGroup?.windows.count ?? 0 > 1 { + closeTab(nil) + return + } + + // 1 window, closing the window + closeWindow(nil) + } + + /// Closes the current window (including any other tabs) immediately and without + /// confirmation. This will setup proper undo state so the action can be undone. + private func closeWindowImmediately(_ sender: Any?) { + guard let window = window else { return } + + // Regardless of tabs vs no tabs, what we want to do here is keep + // track of the window frame to restore, the surface tree, and the + // the focused surface. We want to restore that with undo even + // if we end up closing. + if let undoManager { + // Capture current state for undo + let currentFrame = window.frame + let currentSurfaceTree = surfaceTree + let currentFocusedSurface = focusedSurface + + // Register undo action to restore the window + undoManager.setActionName("Close Window") + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: undoExpiration) { ghostty in + + // Create a new window controller with the saved state + let newController = TerminalController( + ghostty, + withSurfaceTree: currentSurfaceTree + ) + + // Show the window and restore its frame + newController.showWindow(nil) + if let newWindow = newController.window { + newWindow.setFrame(currentFrame, display: true) + + // Restore focus to the previously focused surface + if let focusTarget = currentFocusedSurface { + DispatchQueue.main.async { + Ghostty.moveFocus(to: focusTarget, from: nil) + } + } + } + + // Register redo action + undoManager.registerUndo( + withTarget: newController, + expiresAfter: newController.undoExpiration) { target in + // For redo, we close the window again + target.closeWindowImmediately(sender) + } + } + } + + guard let tabGroup = window.tabGroup else { + // No tabs, no tab group, just perform a normal close. + window.close() + return + } + + // If have one window then we just do a normal close + if tabGroup.windows.count == 1 { + window.close() + return + } + + + tabGroup.windows.forEach { $0.close() } + } + //MARK: - NSWindowController override func windowWillLoad() { @@ -635,13 +722,13 @@ class TerminalController: BaseTerminalController { guard let window = window else { return } guard let tabGroup = window.tabGroup else { // No tabs, no tab group, just perform a normal close. - window.performClose(sender) + closeWindowImmediately(sender) return } // If have one window then we just do a normal close if tabGroup.windows.count == 1 { - window.performClose(sender) + closeWindowImmediately(sender) return } @@ -655,7 +742,7 @@ class TerminalController: BaseTerminalController { // If none need confirmation then we can just close all the windows. if !needsConfirm { - tabGroup.windows.forEach { $0.close() } + closeWindowImmediately(sender) return } @@ -663,7 +750,7 @@ class TerminalController: BaseTerminalController { messageText: "Close Window?", informativeText: "All terminal sessions in this window will be terminated." ) { - tabGroup.windows.forEach { $0.close() } + self.closeWindowImmediately(sender) } } From 5f74445b141a14d5a0d8705a38161f242edf1ec6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 11:05:06 -0700 Subject: [PATCH 390/642] macos: basic undo tab, not quite working --- .../Terminal/TerminalController.swift | 163 ++++++++++++------ 1 file changed, 115 insertions(+), 48 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 554f7699b..b7b2fcd89 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -407,52 +407,82 @@ class TerminalController: BaseTerminalController { closeWindow(nil) } - /// Closes the current window (including any other tabs) immediately and without - /// confirmation. This will setup proper undo state so the action can be undone. - private func closeWindowImmediately(_ sender: Any?) { + private func closeTabImmediately() { guard let window = window else { return } - - // Regardless of tabs vs no tabs, what we want to do here is keep - // track of the window frame to restore, the surface tree, and the - // the focused surface. We want to restore that with undo even - // if we end up closing. - if let undoManager { - // Capture current state for undo - let currentFrame = window.frame - let currentSurfaceTree = surfaceTree - let currentFocusedSurface = focusedSurface - - // Register undo action to restore the window - undoManager.setActionName("Close Window") + guard let tabGroup = window.tabGroup, + tabGroup.windows.count > 1 else { + closeWindowImmediately() + return + } + + // Undo + if let undoManager, let undoState { + // Get the current tab index before closing + let tabIndex = tabGroup.windows.firstIndex(of: window) ?? 0 + + // Register undo action to restore the tab + undoManager.setActionName("Close Tab") undoManager.registerUndo( withTarget: ghostty, expiresAfter: undoExpiration) { ghostty in - - // Create a new window controller with the saved state - let newController = TerminalController( - ghostty, - withSurfaceTree: currentSurfaceTree - ) - // Show the window and restore its frame - newController.showWindow(nil) + // Create a new window controller with the saved state + let newController = TerminalController(ghostty, with: undoState) + if let newWindow = newController.window { - newWindow.setFrame(currentFrame, display: true) - - // Restore focus to the previously focused surface - if let focusTarget = currentFocusedSurface { - DispatchQueue.main.async { - Ghostty.moveFocus(to: focusTarget, from: nil) - } + // Add the window back to the tab group at the correct position + if let targetWindow = tabGroup.windows.dropFirst(tabIndex).first { + // Insert after the target window + targetWindow.addTabbedWindow(newWindow, ordered: .above) + } else if let targetWindow = tabGroup.windows.last { + // Add at the end if the original position is beyond current tabs + targetWindow.addTabbedWindow(newWindow, ordered: .above) + } else if let firstWindow = tabGroup.windows.first { + // Fallback: add to the beginning if needed + firstWindow.addTabbedWindow(newWindow, ordered: .below) } + + // Make it the key window + newWindow.makeKeyAndOrderFront(nil) } + // Register redo action + undoManager.registerUndo( + withTarget: newController, + expiresAfter: newController.undoExpiration) { target in + // For redo, we close the tab again + target.closeTabImmediately() + } + } + } + + window.close() + } + + /// Closes the current window (including any other tabs) immediately and without + /// confirmation. This will setup proper undo state so the action can be undone. + private func closeWindowImmediately() { + guard let window = window else { return } + + // Regardless of tabs vs no tabs, what we want to do here is keep + // track of the window frame to restore, the surface tree, and the + // the focused surface. We want to restore that with undo even + // if we end up closing. + if let undoManager, let undoState { + // Register undo action to restore the window + undoManager.setActionName("Close Window") + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: undoExpiration) { ghostty in + // Restore the undo state + let newController = TerminalController(ghostty, with: undoState) + // Register redo action undoManager.registerUndo( withTarget: newController, expiresAfter: newController.undoExpiration) { target in // For redo, we close the window again - target.closeWindowImmediately(sender) + target.closeWindowImmediately() } } } @@ -473,6 +503,44 @@ class TerminalController: BaseTerminalController { tabGroup.windows.forEach { $0.close() } } + // MARK: Undo/Redo + + /// The state that we require to recreate a TerminalController from an undo. + struct UndoState { + let frame: NSRect + let surfaceTree: SplitTree + let focusedSurface: UUID? + } + + convenience init(_ ghostty: Ghostty.App, + with undoState: UndoState + ) { + self.init(ghostty, withSurfaceTree: undoState.surfaceTree) + + // Show the window and restore its frame + showWindow(nil) + if let window { + window.setFrame(undoState.frame, display: true) + + // Restore focus to the previously focused surface + if let focusedUUID = undoState.focusedSurface, + let focusTarget = surfaceTree.first(where: { $0.uuid == focusedUUID }) { + DispatchQueue.main.async { + Ghostty.moveFocus(to: focusTarget, from: nil) + } + } + } + } + + /// The current undo state for this controller + var undoState: UndoState? { + guard let window else { return nil } + return .init( + frame: window.frame, + surfaceTree: surfaceTree, + focusedSurface: focusedSurface?.uuid) + } + //MARK: - NSWindowController override func windowWillLoad() { @@ -694,23 +762,22 @@ class TerminalController: BaseTerminalController { @IBAction func closeTab(_ sender: Any?) { guard let window = window else { return } - guard window.tabGroup != nil else { - // No tabs, no tab group, just perform a normal close. - window.performClose(sender) + guard window.tabGroup?.windows.count ?? 0 > 1 else { + closeWindow(sender) return } - if surfaceTree.contains(where: { $0.needsConfirmQuit }) { - confirmClose( - messageText: "Close Tab?", - informativeText: "The terminal still has a running process. If you close the tab the process will be killed." - ) { - window.close() - } + guard surfaceTree.contains(where: { $0.needsConfirmQuit }) else { + closeTabImmediately() return } - window.close() + confirmClose( + messageText: "Close Tab?", + informativeText: "The terminal still has a running process. If you close the tab the process will be killed." + ) { + self.closeTabImmediately() + } } @IBAction func returnToDefaultSize(_ sender: Any?) { @@ -722,13 +789,13 @@ class TerminalController: BaseTerminalController { guard let window = window else { return } guard let tabGroup = window.tabGroup else { // No tabs, no tab group, just perform a normal close. - closeWindowImmediately(sender) + closeWindowImmediately() return } // If have one window then we just do a normal close if tabGroup.windows.count == 1 { - closeWindowImmediately(sender) + closeWindowImmediately() return } @@ -742,7 +809,7 @@ class TerminalController: BaseTerminalController { // If none need confirmation then we can just close all the windows. if !needsConfirm { - closeWindowImmediately(sender) + closeWindowImmediately() return } @@ -750,7 +817,7 @@ class TerminalController: BaseTerminalController { messageText: "Close Window?", informativeText: "All terminal sessions in this window will be terminated." ) { - self.closeWindowImmediately(sender) + self.closeWindowImmediately() } } @@ -948,7 +1015,6 @@ class TerminalController: BaseTerminalController { toggleFullscreen(mode: fullscreenMode) } - struct DerivedConfig { let backgroundColor: Color let macosWindowButtons: Ghostty.MacOSWindowButtons @@ -971,6 +1037,7 @@ class TerminalController: BaseTerminalController { } } +// MARK: NSMenuItemValidation extension TerminalController: NSMenuItemValidation { func validateMenuItem(_ item: NSMenuItem) -> Bool { From e1847da1391b3bc49e3c79a0afcec338a7441c0a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 11:13:47 -0700 Subject: [PATCH 391/642] macos: more robust undo tab that goes back to the same position --- .../Terminal/TerminalController.swift | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index b7b2fcd89..162141d11 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -417,35 +417,13 @@ class TerminalController: BaseTerminalController { // Undo if let undoManager, let undoState { - // Get the current tab index before closing - let tabIndex = tabGroup.windows.firstIndex(of: window) ?? 0 - // Register undo action to restore the tab undoManager.setActionName("Close Tab") undoManager.registerUndo( withTarget: ghostty, expiresAfter: undoExpiration) { ghostty in - - // Create a new window controller with the saved state let newController = TerminalController(ghostty, with: undoState) - if let newWindow = newController.window { - // Add the window back to the tab group at the correct position - if let targetWindow = tabGroup.windows.dropFirst(tabIndex).first { - // Insert after the target window - targetWindow.addTabbedWindow(newWindow, ordered: .above) - } else if let targetWindow = tabGroup.windows.last { - // Add at the end if the original position is beyond current tabs - targetWindow.addTabbedWindow(newWindow, ordered: .above) - } else if let firstWindow = tabGroup.windows.first { - // Fallback: add to the beginning if needed - firstWindow.addTabbedWindow(newWindow, ordered: .below) - } - - // Make it the key window - newWindow.makeKeyAndOrderFront(nil) - } - // Register redo action undoManager.registerUndo( withTarget: newController, @@ -510,6 +488,8 @@ class TerminalController: BaseTerminalController { let frame: NSRect let surfaceTree: SplitTree let focusedSurface: UUID? + let tabIndex: Int? + private(set) weak var tabGroup: NSWindowTabGroup? } convenience init(_ ghostty: Ghostty.App, @@ -522,6 +502,21 @@ class TerminalController: BaseTerminalController { if let window { window.setFrame(undoState.frame, display: true) + // If we have a tab group and index, restore the tab to its original position + if let tabGroup = undoState.tabGroup, + let tabIndex = undoState.tabIndex { + if tabIndex < tabGroup.windows.count { + // Find the window that is currently at that index + let currentWindow = tabGroup.windows[tabIndex] + currentWindow.addTabbedWindow(window, ordered: .below) + } else { + tabGroup.windows.last?.addTabbedWindow(window, ordered: .above) + } + + // Make it the key window + window.makeKeyAndOrderFront(nil) + } + // Restore focus to the previously focused surface if let focusedUUID = undoState.focusedSurface, let focusTarget = surfaceTree.first(where: { $0.uuid == focusedUUID }) { @@ -538,7 +533,9 @@ class TerminalController: BaseTerminalController { return .init( frame: window.frame, surfaceTree: surfaceTree, - focusedSurface: focusedSurface?.uuid) + focusedSurface: focusedSurface?.uuid, + tabIndex: window.tabGroup?.windows.firstIndex(of: window), + tabGroup: window.tabGroup) } //MARK: - NSWindowController From b044f4864ae4ad8ab06e3e631a23eb2e5748c0ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 11:34:33 -0700 Subject: [PATCH 392/642] add undo/redo keybindings, default them on macOS --- include/ghostty.h | 2 + macos/Sources/App/macOS/AppDelegate.swift | 7 +-- .../Terminal/TerminalController.swift | 6 +-- macos/Sources/Ghostty/Ghostty.App.swift | 48 +++++++++++++++++++ src/Surface.zig | 12 +++++ src/apprt/action.zig | 9 ++++ src/build/mdgen/mdgen.zig | 3 +- src/config/Config.zig | 12 +++++ src/input/Binding.zig | 31 ++++++++++++ src/input/command.zig | 12 +++++ 10 files changed, 132 insertions(+), 10 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 6b1625a30..95bd58cd7 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -673,6 +673,8 @@ typedef enum { GHOSTTY_ACTION_CONFIG_CHANGE, GHOSTTY_ACTION_CLOSE_WINDOW, GHOSTTY_ACTION_RING_BELL, + GHOSTTY_ACTION_UNDO, + GHOSTTY_ACTION_REDO, GHOSTTY_ACTION_CHECK_FOR_UPDATES } ghostty_action_tag_e; diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 1fce7d665..db332813f 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -398,11 +398,8 @@ class AppDelegate: NSObject, syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown) syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp) - // TODO: sync - menuUndo?.keyEquivalent = "z" - menuUndo?.keyEquivalentModifierMask = [.command] - menuRedo?.keyEquivalent = "z" - menuRedo?.keyEquivalentModifierMask = [.command, .shift] + syncMenuShortcut(config, action: "undo", menuItem: self.menuUndo) + syncMenuShortcut(config, action: "redo", menuItem: self.menuRedo) syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy) syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste) syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 162141d11..ddeb3dada 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -428,8 +428,7 @@ class TerminalController: BaseTerminalController { undoManager.registerUndo( withTarget: newController, expiresAfter: newController.undoExpiration) { target in - // For redo, we close the tab again - target.closeTabImmediately() + target.closeTab(nil) } } } @@ -459,8 +458,7 @@ class TerminalController: BaseTerminalController { undoManager.registerUndo( withTarget: newController, expiresAfter: newController.undoExpiration) { target in - // For redo, we close the window again - target.closeWindowImmediately() + target.closeWindow(nil) } } } diff --git a/macos/Sources/Ghostty/Ghostty.App.swift b/macos/Sources/Ghostty/Ghostty.App.swift index 4a9dc0ea6..ba0b95212 100644 --- a/macos/Sources/Ghostty/Ghostty.App.swift +++ b/macos/Sources/Ghostty/Ghostty.App.swift @@ -553,6 +553,12 @@ extension Ghostty { case GHOSTTY_ACTION_CHECK_FOR_UPDATES: checkForUpdates(app) + case GHOSTTY_ACTION_UNDO: + return undo(app, target: target) + + case GHOSTTY_ACTION_REDO: + return redo(app, target: target) + case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS: fallthrough case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW: @@ -599,6 +605,48 @@ extension Ghostty { } } + private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { + let undoManager: UndoManager? + switch (target.tag) { + case GHOSTTY_TARGET_APP: + undoManager = (NSApp.delegate as? AppDelegate)?.undoManager + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + undoManager = surfaceView.undoManager + + default: + assertionFailure() + return false + } + + guard let undoManager, undoManager.canUndo else { return false } + undoManager.undo() + return true + } + + private static func redo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool { + let undoManager: UndoManager? + switch (target.tag) { + case GHOSTTY_TARGET_APP: + undoManager = (NSApp.delegate as? AppDelegate)?.undoManager + + case GHOSTTY_TARGET_SURFACE: + guard let surface = target.target.surface else { return false } + guard let surfaceView = self.surfaceView(from: surface) else { return false } + undoManager = surfaceView.undoManager + + default: + assertionFailure() + return false + } + + guard let undoManager, undoManager.canRedo else { return false } + undoManager.redo() + return true + } + private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) { switch (target.tag) { case GHOSTTY_TARGET_APP: diff --git a/src/Surface.zig b/src/Surface.zig index 62a0ce549..e53613ac0 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4337,6 +4337,18 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), + .undo => return try self.rt_app.performAction( + .{ .surface = self }, + .undo, + {}, + ), + + .redo => return try self.rt_app.performAction( + .{ .surface = self }, + .redo, + {}, + ), + .select_all => { const sel = self.io.terminal.screen.selectAll(); if (sel) |s| { diff --git a/src/apprt/action.zig b/src/apprt/action.zig index 7866db182..b4c5164c2 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -258,6 +258,13 @@ pub const Action = union(Key) { /// it needs to ring the bell. This is usually a sound or visual effect. ring_bell, + /// Undo the last action. See the "undo" keybinding for more + /// details on what can and cannot be undone. + undo, + + /// Redo the last undone action. + redo, + check_for_updates, /// Sync with: ghostty_action_tag_e @@ -307,6 +314,8 @@ pub const Action = union(Key) { config_change, close_window, ring_bell, + undo, + redo, check_for_updates, }; diff --git a/src/build/mdgen/mdgen.zig b/src/build/mdgen/mdgen.zig index aca230aa5..e7d966323 100644 --- a/src/build/mdgen/mdgen.zig +++ b/src/build/mdgen/mdgen.zig @@ -26,7 +26,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void { \\ ); - @setEvalBranchQuota(3000); + @setEvalBranchQuota(5000); inline for (@typeInfo(Config).@"struct".fields) |field| { if (field.name[0] == '_') continue; @@ -94,6 +94,7 @@ pub fn genKeybindActions(writer: anytype) !void { const info = @typeInfo(KeybindAction); std.debug.assert(info == .@"union"); + @setEvalBranchQuota(5000); inline for (info.@"union".fields) |field| { if (field.name[0] == '_') continue; diff --git a/src/config/Config.zig b/src/config/Config.zig index 14f394559..fdbde692d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4898,6 +4898,18 @@ pub const Keybinds = struct { .{ .key = .{ .unicode = 'q' }, .mods = .{ .super = true } }, .{ .quit = {} }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true } }, + .{ .undo = {} }, + .{ .performable = true }, + ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true, .shift = true } }, + .{ .redo = {} }, + .{ .performable = true }, + ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 7818fac1e..52d36c004 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -655,6 +655,35 @@ pub const Action = union(enum) { /// Only implemented on macOS. check_for_updates, + /// Undo the last undoable action for the focused surface or terminal, + /// if possible. This can undo actions such as closing tabs or + /// windows. + /// + /// Not every action in Ghostty can be undone or redone. The list + /// of actions support undo/redo is currently limited to: + /// + /// - New window, close window + /// - New tab, close tab + /// - New split, close split + /// + /// All actions are only undoable/redoable for a limited time. + /// For example, restoring a closed split can only be done for + /// some number of seconds since the split was closed. The exact + /// amount is configured with `TODO`. + /// + /// The undo/redo actions being limited ensures that there is + /// bounded memory usage over time, closed surfaces don't continue running + /// in the background indefinitely, and the keybinds become available + /// for terminal applications to use. + /// + /// Only implemented on macOS. + undo, + + /// Redo the last undoable action for the focused surface or terminal, + /// if possible. See "undo" for more details on what can and cannot + /// be undone or redone. + redo, + /// Quit Ghostty. quit, @@ -991,6 +1020,8 @@ pub const Action = union(enum) { .toggle_secure_input, .toggle_command_palette, .reset_window_size, + .undo, + .redo, .crash, => .surface, diff --git a/src/input/command.zig b/src/input/command.zig index 4a918cff3..94fbf56a5 100644 --- a/src/input/command.zig +++ b/src/input/command.zig @@ -409,6 +409,18 @@ fn actionCommands(action: Action.Key) []const Command { .description = "Check for updates to the application.", }}, + .undo => comptime &.{.{ + .action = .undo, + .title = "Undo", + .description = "Undo the last action.", + }}, + + .redo => comptime &.{.{ + .action = .redo, + .title = "Redo", + .description = "Redo the last undone action.", + }}, + .quit => comptime &.{.{ .action = .quit, .title = "Quit", From 3e02c0cbd5edc1fbbf842a5a603ecf907ad5a187 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 12:12:14 -0700 Subject: [PATCH 393/642] macos: fix an incorrect bindable write during view update --- macos/Sources/Ghostty/SurfaceView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 46d379b9c..f830da4ef 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -301,8 +301,12 @@ extension Ghostty { if let instant = focusInstant { let d = instant.duration(to: ContinuousClock.now) if (d < .milliseconds(500)) { - // Avoid this size completely. - lastSize = geoSize + // Avoid this size completely. We can't set values during + // view updates so we have to defer this to another tick. + DispatchQueue.main.async { + lastSize = geoSize + } + return true; } } From 49cc88f0d335e4f52c69bbf38e9099c000d26c7d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 12:21:05 -0700 Subject: [PATCH 394/642] macos: configurable undo timeout --- .../Terminal/BaseTerminalController.swift | 2 +- macos/Sources/Ghostty/Ghostty.Config.swift | 8 +++ .../Sources/Helpers/ExpiringUndoManager.swift | 3 + src/config/Config.zig | 66 ++++++++++++++++++- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e34a44941..6ea56f693 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -77,7 +77,7 @@ class BaseTerminalController: NSWindowController, /// The time that undo/redo operations that contain running ptys are valid for. var undoExpiration: Duration { - .seconds(5) + ghostty.config.undoTimeout } /// The undo manager for this controller is the undo manager of the window, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 3acb93c25..fcbea2a12 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -506,6 +506,14 @@ extension Ghostty { return v; } + var undoTimeout: Duration { + guard let config = self.config else { return .seconds(5) } + var v: UInt = 0 + let key = "undo-timeout" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return .milliseconds(v) + } + var autoUpdate: AutoUpdate? { guard let config = self.config else { return nil } var v: UnsafePointer? = nil diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift index 3eda56182..9a9349cf3 100644 --- a/macos/Sources/Helpers/ExpiringUndoManager.swift +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -29,6 +29,9 @@ class ExpiringUndoManager: UndoManager { expiresAfter duration: Duration, handler: @escaping (TargetType) -> Void ) { + // Ignore instantly expiring undos + guard duration.timeInterval > 0 else { return } + let expiringTarget = ExpiringTarget( target, expiresAfter: duration, diff --git a/src/config/Config.zig b/src/config/Config.zig index fdbde692d..e1d5b548e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1705,6 +1705,52 @@ keybind: Keybinds = .{}, /// window is ever created. Only implemented on Linux and macOS. @"initial-window": bool = true, +/// The duration that undo operations remain available. After this +/// time, the operation will be removed from the undo stack and +/// cannot be undone. +/// +/// The default value is 5 seconds. +/// +/// This timeout applies per operation, meaning that if you perform +/// multiple operations, each operation will have its own timeout. +/// New operations do not reset the timeout of previous operations. +/// +/// A timeout of zero will effectively disable undo operations. It is +/// not possible to set an infinite timeout, but you can set a very +/// large timeout to effectively disable the timeout (on the order of years). +/// This is highly discouraged, as it will cause the undo stack to grow +/// indefinitely, memory usage to grow unbounded, and terminal sessions +/// to never actually quit. +/// +/// The duration is specified as a series of numbers followed by time units. +/// Whitespace is allowed between numbers and units. Each number and unit will +/// be added together to form the total duration. +/// +/// The allowed time units are as follows: +/// +/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments +/// are made for leap years or leap seconds. +/// * `d` - one SI day, or 86400 seconds. +/// * `h` - one hour, or 3600 seconds. +/// * `m` - one minute, or 60 seconds. +/// * `s` - one second. +/// * `ms` - one millisecond, or 0.001 second. +/// * `us` or `µs` - one microsecond, or 0.000001 second. +/// * `ns` - one nanosecond, or 0.000000001 second. +/// +/// Examples: +/// * `1h30m` +/// * `45s` +/// +/// Units can be repeated and will be added together. This means that +/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided. +/// A future update may disallow this. +/// +/// This configuration is only supported on macOS. Linux doesn't +/// support undo operations at all so this configuration has no +/// effect. +@"undo-timeout": Duration = .{ .duration = 5 * std.time.ns_per_s }, + /// The position of the "quick" terminal window. To learn more about the /// quick terminal, see the documentation for the `toggle_quick_terminal` /// binding action. @@ -6583,7 +6629,7 @@ pub const Duration = struct { if (remaining.len == 0) break; // Find the longest number - const number = number: { + const number: u64 = number: { var prev_number: ?u64 = null; var prev_remaining: ?[]const u8 = null; for (1..remaining.len + 1) |index| { @@ -6597,8 +6643,17 @@ pub const Duration = struct { break :number prev_number; } orelse return error.InvalidValue; - // A number without a unit is invalid - if (remaining.len == 0) return error.InvalidValue; + // A number without a unit is invalid unless the number is + // exactly zero. In that case, the unit is unambiguous since + // its all the same. + if (remaining.len == 0) { + if (number == 0) { + value = 0; + break; + } + + return error.InvalidValue; + } // Find the longest matching unit. Needs to be the longest matching // to distinguish 'm' from 'ms'. @@ -6808,6 +6863,11 @@ test "parse duration" { try std.testing.expectEqual(unit.factor, d.duration); } + { + const d = try Duration.parseCLI("0"); + try std.testing.expectEqual(@as(u64, 0), d.duration); + } + { const d = try Duration.parseCLI("100ns"); try std.testing.expectEqual(@as(u64, 100), d.duration); From d2d38520261c1ba9dc51ec7a787b3518077f4890 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 12:28:48 -0700 Subject: [PATCH 395/642] macos: remove debug log --- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 6e35f40d1..682efa947 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -287,8 +287,6 @@ extension Ghostty { if let surface = self.surface { ghostty_surface_free(surface) } - - Ghostty.logger.warning("WOW close") } func focusDidChange(_ focused: Bool) { From 966c4f98c7da9bb286342c385d8e28796f376d4e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 12:39:02 -0700 Subject: [PATCH 396/642] apprt/glfw,gtk: noop undo/redo actions --- src/apprt/glfw.zig | 2 ++ src/apprt/gtk/App.zig | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/apprt/glfw.zig b/src/apprt/glfw.zig index d67567aee..924737074 100644 --- a/src/apprt/glfw.zig +++ b/src/apprt/glfw.zig @@ -250,6 +250,8 @@ pub const App = struct { .reset_window_size, .ring_bell, .check_for_updates, + .undo, + .redo, .show_gtk_inspector, => { log.info("unimplemented action={}", .{action}); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index d69102bda..099a051a4 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -515,6 +515,8 @@ pub fn performAction( .color_change, .reset_window_size, .check_for_updates, + .undo, + .redo, => { log.warn("unimplemented action={}", .{action}); return false; From 5507ec0fc0199d3442065a54e8c737759eb5d084 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 12:48:23 -0700 Subject: [PATCH 397/642] macos: compile errors in CI --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 5 ++--- .../Sources/Helpers/Extensions/NSApplication+Extension.swift | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 6ea56f693..129aeb1e2 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -404,8 +404,7 @@ class BaseTerminalController: NSWindowController, guard let node = surfaceTree.root?.node(view: target) else { return } closeSurfaceNode( node, - withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false, - ) + withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false) } /// Close a surface node (which may contain splits), requesting confirmation if necessary. @@ -413,7 +412,7 @@ class BaseTerminalController: NSWindowController, /// This will also insert the proper undo stack information in. func closeSurfaceNode( _ node: SplitTree.Node, - withConfirmation: Bool = true, + withConfirmation: Bool = true ) { // This node must be part of our tree guard surfaceTree.contains(node) else { return } diff --git a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift index d8e41523a..0bc79fb6a 100644 --- a/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSApplication+Extension.swift @@ -1,3 +1,4 @@ +import AppKit import Cocoa // MARK: Presentation Options From 3b77a16b63448eb1a1764ba82c0e945969c2d819 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 13:35:31 -0700 Subject: [PATCH 398/642] Make undo/redo app-targeted so it works with no windows --- src/App.zig | 3 +++ src/Surface.zig | 27 +++++++++++++++------------ src/input/Binding.zig | 4 ++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/App.zig b/src/App.zig index 39db2e2f9..3bbeff2c8 100644 --- a/src/App.zig +++ b/src/App.zig @@ -446,6 +446,9 @@ pub fn performAction( .toggle_visibility => _ = try rt_app.performAction(.app, .toggle_visibility, {}), .check_for_updates => _ = try rt_app.performAction(.app, .check_for_updates, {}), .show_gtk_inspector => _ = try rt_app.performAction(.app, .show_gtk_inspector, {}), + .undo => _ = try rt_app.performAction(.app, .undo, {}), + + .redo => _ = try rt_app.performAction(.app, .redo, {}), } } diff --git a/src/Surface.zig b/src/Surface.zig index e53613ac0..9ab7234d6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -3923,6 +3923,21 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool .{ .parent = self }, ), + // Undo and redo both support both surface and app targeting. + // If we are triggering on a surface then we perform the + // action with the surface target. + .undo => return try self.rt_app.performAction( + .{ .surface = self }, + .undo, + {}, + ), + + .redo => return try self.rt_app.performAction( + .{ .surface = self }, + .redo, + {}, + ), + else => try self.app.performAction( self.rt_app, action.scoped(.app).?, @@ -4337,18 +4352,6 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool {}, ), - .undo => return try self.rt_app.performAction( - .{ .surface = self }, - .undo, - {}, - ), - - .redo => return try self.rt_app.performAction( - .{ .surface = self }, - .redo, - {}, - ), - .select_all => { const sel = self.io.terminal.screen.selectAll(); if (sel) |s| { diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 52d36c004..ca3fd9790 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -980,6 +980,8 @@ pub const Action = union(enum) { // These are app but can be special-cased in a surface context. .new_window, + .undo, + .redo, => .app, // Obviously surface actions. @@ -1020,8 +1022,6 @@ pub const Action = union(enum) { .toggle_secure_input, .toggle_command_palette, .reset_window_size, - .undo, - .redo, .crash, => .surface, From 33d128bcff2ee529359a844bde50c3aa9dfed460 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 15:19:05 -0700 Subject: [PATCH 399/642] macos: remove TerminalManager All logic related to TerminalController is now in TerminalController. --- macos/Ghostty.xcodeproj/project.pbxproj | 4 - macos/Sources/App/macOS/AppDelegate.swift | 61 ++- .../Features/Services/ServiceProvider.swift | 5 +- .../Terminal/BaseTerminalController.swift | 2 +- .../Terminal/TerminalController.swift | 223 ++++++++++- .../Features/Terminal/TerminalManager.swift | 372 ------------------ .../Terminal/TerminalRestorable.swift | 6 +- 7 files changed, 268 insertions(+), 405 deletions(-) delete mode 100644 macos/Sources/Features/Terminal/TerminalManager.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 67f1784ac..7da727fbb 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -71,7 +71,6 @@ A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */ = {isa = PBXBuildFile; fileRef = A59630992AEE1C6400D64628 /* Terminal.xib */; }; A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309B2AEE1C9E00D64628 /* TerminalController.swift */; }; A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309D2AEE1D6C00D64628 /* TerminalView.swift */; }; - A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A596309F2AEF6AEB00D64628 /* TerminalManager.swift */; }; A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5985CD62C320C4500C57AD3 /* String+Extension.swift */; }; A5985CE62C33060F00C57AD3 /* man in Resources */ = {isa = PBXBuildFile; fileRef = A5985CE52C33060F00C57AD3 /* man */; }; @@ -179,7 +178,6 @@ A59630992AEE1C6400D64628 /* Terminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Terminal.xib; sourceTree = ""; }; A596309B2AEE1C9E00D64628 /* TerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalController.swift; sourceTree = ""; }; A596309D2AEE1D6C00D64628 /* TerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalView.swift; sourceTree = ""; }; - A596309F2AEF6AEB00D64628 /* TerminalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalManager.swift; sourceTree = ""; }; A5985CD62C320C4500C57AD3 /* String+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Extension.swift"; sourceTree = ""; }; A5985CE52C33060F00C57AD3 /* man */ = {isa = PBXFileReference; lastKnownFileType = folder; name = man; path = "../zig-out/share/man"; sourceTree = ""; }; A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAppearance+Extension.swift"; sourceTree = ""; }; @@ -467,7 +465,6 @@ isa = PBXGroup; children = ( A59630992AEE1C6400D64628 /* Terminal.xib */, - A596309F2AEF6AEB00D64628 /* TerminalManager.swift */, A596309B2AEE1C9E00D64628 /* TerminalController.swift */, A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, @@ -710,7 +707,6 @@ C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */, A59630972AEE163600D64628 /* HostingWindow.swift in Sources */, - A59630A02AEF6AEB00D64628 /* TerminalManager.swift in Sources */, A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */, A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */, AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index db332813f..aacf8f651 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -87,9 +87,6 @@ class AppDelegate: NSObject, /// The ghostty global state. Only one per process. let ghostty: Ghostty.App = Ghostty.App() - /// Manages our terminal windows. - let terminalManager: TerminalManager - /// The global undo manager for app-level state such as window restoration. lazy var undoManager = ExpiringUndoManager() @@ -119,7 +116,6 @@ class AppDelegate: NSObject, } override init() { - terminalManager = TerminalManager(ghostty) updaterController = SPUStandardUpdaterController( // Important: we must not start the updater here because we need to read our configuration // first to determine whether we're automatically checking, downloading, etc. The updater @@ -202,6 +198,16 @@ class AppDelegate: NSObject, name: .ghosttyBellDidRing, object: nil ) + NotificationCenter.default.addObserver( + self, + selector: #selector(ghosttyNewWindow(_:)), + name: Ghostty.Notification.ghosttyNewWindow, + object: nil) + NotificationCenter.default.addObserver( + self, + selector: #selector(ghosttyNewTab(_:)), + name: Ghostty.Notification.ghosttyNewTab, + object: nil) // Configure user notifications let actions = [ @@ -253,8 +259,8 @@ class AppDelegate: NSObject, // is possible to have other windows in a few scenarios: // - if we're opening a URL since `application(_:openFile:)` is called before this. // - if we're restoring from persisted state - if terminalManager.windows.count == 0 && derivedConfig.initialWindow { - terminalManager.newWindow() + if TerminalController.all.isEmpty && derivedConfig.initialWindow { + _ = TerminalController.newWindow(ghostty) } } } @@ -339,10 +345,10 @@ class AppDelegate: NSObject, // This is possible with flag set to false if there a race where the // window is still initializing and is not visible but the user clicked // the dock icon. - guard terminalManager.windows.count == 0 else { return true } + guard TerminalController.all.isEmpty else { return true } // No visible windows, open a new one. - terminalManager.newWindow() + _ = TerminalController.newWindow(ghostty) return false } @@ -358,16 +364,17 @@ class AppDelegate: NSObject, var config = Ghostty.SurfaceConfiguration() if (isDirectory.boolValue) { - // When opening a directory, create a new tab in the main window with that as the working directory. + // When opening a directory, create a new tab in the main + // window with that as the working directory. // If no windows exist, a new one will be created. config.workingDirectory = filename - terminalManager.newTab(withBaseConfig: config) + _ = TerminalController.newTab(ghostty, withBaseConfig: config) } else { // When opening a file, open a new window with that file as the command, // and its parent directory as the working directory. config.command = filename config.workingDirectory = (filename as NSString).deletingLastPathComponent - terminalManager.newWindow(withBaseConfig: config) + _ = TerminalController.newWindow(ghostty, withBaseConfig: config) } return true @@ -456,10 +463,6 @@ class AppDelegate: NSObject, menu.keyEquivalentModifierMask = .init(swiftUIFlags: shortcut.modifiers) } - private func focusedSurface() -> ghostty_surface_t? { - return terminalManager.focusedSurface?.surface - } - // MARK: Notifications and Events /// This handles events from the NSEvent.addLocalEventMonitor. We use this so we can get @@ -592,6 +595,22 @@ class AppDelegate: NSObject, } } + @objc private func ghosttyNewWindow(_ notification: Notification) { + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] + let config = configAny as? Ghostty.SurfaceConfiguration + _ = TerminalController.newWindow(ghostty, withBaseConfig: config) + } + + @objc private func ghosttyNewTab(_ notification: Notification) { + guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } + guard let window = surfaceView.window else { return } + + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] + let config = configAny as? Ghostty.SurfaceConfiguration + + _ = TerminalController.newTab(ghostty, from: window, withBaseConfig: config) + } + private func setDockBadge(_ label: String? = "•") { NSApp.dockTile.badgeLabel = label NSApp.dockTile.display() @@ -627,7 +646,7 @@ class AppDelegate: NSObject, // Config could change keybindings, so update everything that depends on that syncMenuShortcuts(config) - terminalManager.relabelAllTabs() + TerminalController.all.forEach { $0.relabelTabs() } // Config could change window appearance. We wrap this in an async queue because when // this is called as part of application launch it can deadlock with an internal @@ -756,8 +775,8 @@ class AppDelegate: NSObject, //MARK: - GhosttyAppDelegate func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? { - for c in terminalManager.windows { - for view in c.controller.surfaceTree { + for c in TerminalController.all { + for view in c.surfaceTree { if view.uuid == uuid { return view } @@ -811,7 +830,7 @@ class AppDelegate: NSObject, } @IBAction func newWindow(_ sender: Any?) { - terminalManager.newWindow() + _ = TerminalController.newWindow(ghostty) // We also activate our app so that it becomes front. This may be // necessary for the dock menu. @@ -819,7 +838,7 @@ class AppDelegate: NSObject, } @IBAction func newTab(_ sender: Any?) { - terminalManager.newTab() + _ = TerminalController.newTab(ghostty) // We also activate our app so that it becomes front. This may be // necessary for the dock menu. @@ -827,7 +846,7 @@ class AppDelegate: NSObject, } @IBAction func closeAllWindows(_ sender: Any?) { - terminalManager.closeAllWindows() + TerminalController.closeAllWindows() AboutController.shared.hide() } diff --git a/macos/Sources/Features/Services/ServiceProvider.swift b/macos/Sources/Features/Services/ServiceProvider.swift index 043f5d704..f60f94211 100644 --- a/macos/Sources/Features/Services/ServiceProvider.swift +++ b/macos/Sources/Features/Services/ServiceProvider.swift @@ -32,7 +32,6 @@ class ServiceProvider: NSObject { error: AutoreleasingUnsafeMutablePointer ) { guard let delegate = NSApp.delegate as? AppDelegate else { return } - let terminalManager = delegate.terminalManager guard let pathURLs = pasteboard.readObjects(forClasses: [NSURL.self]) as? [URL] else { error.pointee = Self.errorNoString @@ -53,10 +52,10 @@ class ServiceProvider: NSObject { switch (target) { case .window: - terminalManager.newWindow(withBaseConfig: config) + _ = TerminalController.newWindow(delegate.ghostty, withBaseConfig: config) case .tab: - terminalManager.newTab(withBaseConfig: config) + _ = TerminalController.newTab(delegate.ghostty, withBaseConfig: config) } } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 129aeb1e2..e4b42c3a1 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -412,7 +412,7 @@ class BaseTerminalController: NSWindowController, /// This will also insert the proper undo stack information in. func closeSurfaceNode( _ node: SplitTree.Node, - withConfirmation: Bool = true + withConfirmation: Bool = true, ) { // This node must be part of our tree guard surfaceTree.contains(node) else { return } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index ddeb3dada..3210eda0c 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -32,7 +32,8 @@ class TerminalController: BaseTerminalController { init(_ ghostty: Ghostty.App, withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree tree: SplitTree? = nil + withSurfaceTree tree: SplitTree? = nil, + parent: NSWindow? = nil ) { // The window we manage is not restorable if we've specified a command // to execute. We do this because the restored window is meaningless at the @@ -137,6 +138,159 @@ class TerminalController: BaseTerminalController { syncAppearance(focusedSurface.derivedConfig) } + // MARK: Terminal Creation + + /// Returns all the available terminal controllers present in the app currently. + static var all: [TerminalController] { + return NSApplication.shared.windows.compactMap { + $0.windowController as? TerminalController + } + } + + // Keep track of the last point that our window was launched at so that new + // windows "cascade" over each other and don't just launch directly on top + // of each other. + private static var lastCascadePoint = NSPoint(x: 0, y: 0) + + // The preferred parent terminal controller. + private static var preferredParent: TerminalController? { + all.first { + $0.window?.isMainWindow ?? false + } ?? all.last + } + + /// The "new window" action. + static func newWindow( + _ ghostty: Ghostty.App, + withBaseConfig baseConfig: Ghostty.SurfaceConfiguration? = nil, + withParent explicitParent: NSWindow? = nil + ) -> TerminalController { + let c = TerminalController.init(ghostty, withBaseConfig: baseConfig) + + // Get our parent. Our parent is the one explicitly given to us, + // otherwise the focused terminal, otherwise an arbitrary one. + let parent: NSWindow? = explicitParent ?? preferredParent?.window + + if let parent { + if parent.styleMask.contains(.fullScreen) { + parent.toggleFullScreen(nil) + } else if ghostty.config.windowFullscreen { + switch (ghostty.config.windowFullscreenMode) { + case .native: + // Native has to be done immediately so that our stylemask contains + // fullscreen for the logic later in this method. + c.toggleFullscreen(mode: .native) + + case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch: + // If we're non-native then we have to do it on a later loop + // so that the content view is setup. + DispatchQueue.main.async { + c.toggleFullscreen(mode: ghostty.config.windowFullscreenMode) + } + } + } + } + + // We're dispatching this async because otherwise the lastCascadePoint doesn't + // take effect. Our best theory is there is some next-event-loop-tick logic + // that Cocoa is doing that we need to be after. + DispatchQueue.main.async { + // Only cascade if we aren't fullscreen. + if let window = c.window { + if (!window.styleMask.contains(.fullScreen)) { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } + } + + c.showWindow(self) + } + + return c + } + + static func newTab( + _ ghostty: Ghostty.App, + from parent: NSWindow? = nil, + withBaseConfig baseConfig: Ghostty.SurfaceConfiguration? = nil + ) -> TerminalController? { + // Making sure that we're dealing with a TerminalController. If not, + // then we just create a new window. + guard let parent, + let parentController = parent.windowController as? TerminalController else { + return newWindow(ghostty, withBaseConfig: baseConfig, withParent: parent) + } + + // If our parent is in non-native fullscreen, then new tabs do not work. + // See: https://github.com/mitchellh/ghostty/issues/392 + if let fullscreenStyle = parentController.fullscreenStyle, + fullscreenStyle.isFullscreen && !fullscreenStyle.supportsTabs { + let alert = NSAlert() + alert.messageText = "Cannot Create New Tab" + alert.informativeText = "New tabs are unsupported while in non-native fullscreen. Exit fullscreen and try again." + alert.addButton(withTitle: "OK") + alert.alertStyle = .warning + alert.beginSheetModal(for: parent) + return nil + } + + // Create a new window and add it to the parent + let controller = TerminalController.init(ghostty, withBaseConfig: baseConfig) + guard let window = controller.window else { return controller } + + // If the parent is miniaturized, then macOS exhibits really strange behaviors + // so we have to bring it back out. + if (parent.isMiniaturized) { parent.deminiaturize(self) } + + // If our parent tab group already has this window, macOS added it and + // we need to remove it so we can set the correct order in the next line. + // If we don't do this, macOS gets really confused and the tabbedWindows + // state becomes incorrect. + // + // At the time of writing this code, the only known case this happens + // is when the "+" button is clicked in the tab bar. + if let tg = parent.tabGroup, + tg.windows.firstIndex(of: window) != nil { + tg.removeWindow(window) + } + + // Our windows start out invisible. We need to make it visible. If we + // don't do this then various features such as window blur won't work because + // the macOS APIs only work on a visible window. + controller.showWindow(self) + + // If we have the "hidden" titlebar style we want to create new + // tabs as windows instead, so just skip adding it to the parent. + if (ghostty.config.macosTitlebarStyle != "hidden") { + // Add the window to the tab group and show it. + switch ghostty.config.windowNewTabPosition { + case "end": + // If we already have a tab group and we want the new tab to open at the end, + // then we use the last window in the tab group as the parent. + if let last = parent.tabGroup?.windows.last { + last.addTabbedWindow(window, ordered: .above) + } else { + fallthrough + } + + case "current": fallthrough + default: + parent.addTabbedWindow(window, ordered: .above) + } + } + + window.makeKeyAndOrderFront(self) + + // It takes an event loop cycle until the macOS tabGroup state becomes + // consistent which causes our tab labeling to be off when the "+" button + // is used in the tab bar. This fixes that. If we can find a more robust + // solution we should do that. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + controller.relabelTabs() + } + + return controller + } + //MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { @@ -479,6 +633,44 @@ class TerminalController: BaseTerminalController { tabGroup.windows.forEach { $0.close() } } + /// Close all windows, asking for confirmation if necessary. + static func closeAllWindows() { + let needsConfirm: Bool = all.contains { + $0.surfaceTree.contains { $0.needsConfirmQuit } + } + + if (!needsConfirm) { + closeAllWindowsImmediately() + return + } + + // If we don't have a main window, we just close all windows because + // we have no window to show the modal on top of. I'm sure there's a way + // to do an app-level alert but I don't know how and this case should never + // really happen. + guard let alertWindow = preferredParent?.window else { + closeAllWindowsImmediately() + return + } + + // If we need confirmation by any, show one confirmation for all windows + let alert = NSAlert() + alert.messageText = "Close All Windows?" + alert.informativeText = "All terminal sessions will be terminated." + alert.addButton(withTitle: "Close All Windows") + alert.addButton(withTitle: "Cancel") + alert.alertStyle = .warning + alert.beginSheetModal(for: alertWindow, completionHandler: { response in + if (response == .alertFirstButtonReturn) { + closeAllWindowsImmediately() + } + }) + } + + static private func closeAllWindowsImmediately() { + all.forEach { $0.close() } + } + // MARK: Undo/Redo /// The state that we require to recreate a TerminalController from an undo. @@ -709,6 +901,35 @@ class TerminalController: BaseTerminalController { override func windowWillClose(_ notification: Notification) { super.windowWillClose(notification) self.relabelTabs() + + // If we remove a window, we reset the cascade point to the key window so that + // the next window cascade's from that one. + if let focusedWindow = NSApplication.shared.keyWindow { + // If we are NOT the focused window, then we are a tabbed window. If we + // are closing a tabbed window, we want to set the cascade point to be + // the next cascade point from this window. + if focusedWindow != window { + // The cascadeTopLeft call below should NOT move the window. Starting with + // macOS 15, we found that specifically when used with the new window snapping + // features of macOS 15, this WOULD move the frame. So we keep track of the + // old frame and restore it if necessary. Issue: + // https://github.com/ghostty-org/ghostty/issues/2565 + let oldFrame = focusedWindow.frame + + Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) + + if focusedWindow.frame != oldFrame { + focusedWindow.setFrame(oldFrame, display: true) + } + + return + } + + // If we are the focused window, then we set the last cascade point to + // our own frame so that it shows up in the same spot. + let frame = focusedWindow.frame + Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY) + } } override func windowDidBecomeKey(_ notification: Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift deleted file mode 100644 index 050bc5563..000000000 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ /dev/null @@ -1,372 +0,0 @@ -import Cocoa -import SwiftUI -import GhosttyKit -import Combine - -/// Manages a set of terminal windows. This is effectively an array of TerminalControllers. -/// This abstraction helps manage tabs and multi-window scenarios. -class TerminalManager { - struct Window { - let controller: TerminalController - let closePublisher: AnyCancellable - } - - let ghostty: Ghostty.App - - /// The currently focused surface of the main window. - var focusedSurface: Ghostty.SurfaceView? { mainWindow?.controller.focusedSurface } - - /// The set of windows we currently have. - var windows: [Window] = [] - - // Keep track of the last point that our window was launched at so that new - // windows "cascade" over each other and don't just launch directly on top - // of each other. - private static var lastCascadePoint = NSPoint(x: 0, y: 0) - - /// Returns the main window of the managed window stack. If there is no window - /// then an arbitrary window will be chosen. - private var mainWindow: Window? { - for window in windows { - if (window.controller.window?.isMainWindow ?? false) { - return window - } - } - - // If we have no main window, just use the last window. - return windows.last - } - - /// The configuration derived from the Ghostty config so we don't need to rely on references. - private var derivedConfig: DerivedConfig - - init(_ ghostty: Ghostty.App) { - self.ghostty = ghostty - self.derivedConfig = DerivedConfig(ghostty.config) - - let center = NotificationCenter.default - center.addObserver( - self, - selector: #selector(onNewTab), - name: Ghostty.Notification.ghosttyNewTab, - object: nil) - center.addObserver( - self, - selector: #selector(onNewWindow), - name: Ghostty.Notification.ghosttyNewWindow, - object: nil) - center.addObserver( - self, - selector: #selector(ghosttyConfigDidChange(_:)), - name: .ghosttyConfigDidChange, - object: nil) - } - - deinit { - let center = NotificationCenter.default - center.removeObserver(self) - } - - // MARK: - Window Management - - /// Create a new terminal window. - func newWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { - let c = createWindow(withBaseConfig: base) - let window = c.window! - - // If the previous focused window was native fullscreen, the new window also - // becomes native fullscreen. - if let parent = focusedSurface?.window, - parent.styleMask.contains(.fullScreen) { - window.toggleFullScreen(nil) - } else if derivedConfig.windowFullscreen { - switch (derivedConfig.windowFullscreenMode) { - case .native: - // Native has to be done immediately so that our stylemask contains - // fullscreen for the logic later in this method. - c.toggleFullscreen(mode: .native) - - case .nonNative, .nonNativeVisibleMenu, .nonNativePaddedNotch: - // If we're non-native then we have to do it on a later loop - // so that the content view is setup. - DispatchQueue.main.async { - c.toggleFullscreen(mode: self.derivedConfig.windowFullscreenMode) - } - } - } - - // All new_window actions force our app to be active. - NSApp.activate(ignoringOtherApps: true) - - // We're dispatching this async because otherwise the lastCascadePoint doesn't - // take effect. Our best theory is there is some next-event-loop-tick logic - // that Cocoa is doing that we need to be after. - DispatchQueue.main.async { - // Only cascade if we aren't fullscreen. - if (!window.styleMask.contains(.fullScreen)) { - Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) - } - - c.showWindow(self) - } - } - - /// Creates a new tab in the current main window. If there are no windows, a window - /// is created. - func newTab(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil) { - // If there is no main window, just create a new window - guard let parent = mainWindow?.controller.window else { - newWindow(withBaseConfig: base) - return - } - - // Create a new window and add it to the parent - newTab(to: parent, withBaseConfig: base) - } - - private func newTab(to parent: NSWindow, withBaseConfig base: Ghostty.SurfaceConfiguration?) { - // Making sure that we're dealing with a TerminalController - guard parent.windowController is TerminalController else { return } - - // If our parent is in non-native fullscreen, then new tabs do not work. - // See: https://github.com/mitchellh/ghostty/issues/392 - if let controller = parent.windowController as? TerminalController, - let fullscreenStyle = controller.fullscreenStyle, - fullscreenStyle.isFullscreen && !fullscreenStyle.supportsTabs { - let alert = NSAlert() - alert.messageText = "Cannot Create New Tab" - alert.informativeText = "New tabs are unsupported while in non-native fullscreen. Exit fullscreen and try again." - alert.addButton(withTitle: "OK") - alert.alertStyle = .warning - alert.beginSheetModal(for: parent) - return - } - - // Create a new window and add it to the parent - let controller = createWindow(withBaseConfig: base) - let window = controller.window! - - // If the parent is miniaturized, then macOS exhibits really strange behaviors - // so we have to bring it back out. - if (parent.isMiniaturized) { parent.deminiaturize(self) } - - // If our parent tab group already has this window, macOS added it and - // we need to remove it so we can set the correct order in the next line. - // If we don't do this, macOS gets really confused and the tabbedWindows - // state becomes incorrect. - // - // At the time of writing this code, the only known case this happens - // is when the "+" button is clicked in the tab bar. - if let tg = parent.tabGroup, tg.windows.firstIndex(of: window) != nil { - tg.removeWindow(window) - } - - // Our windows start out invisible. We need to make it visible. If we - // don't do this then various features such as window blur won't work because - // the macOS APIs only work on a visible window. - controller.showWindow(self) - - // If we have the "hidden" titlebar style we want to create new - // tabs as windows instead, so just skip adding it to the parent. - if (derivedConfig.macosTitlebarStyle != "hidden") { - // Add the window to the tab group and show it. - switch derivedConfig.windowNewTabPosition { - case "end": - // If we already have a tab group and we want the new tab to open at the end, - // then we use the last window in the tab group as the parent. - if let last = parent.tabGroup?.windows.last { - last.addTabbedWindow(window, ordered: .above) - } else { - fallthrough - } - case "current": fallthrough - default: - parent.addTabbedWindow(window, ordered: .above) - - } - } - - window.makeKeyAndOrderFront(self) - - // It takes an event loop cycle until the macOS tabGroup state becomes - // consistent which causes our tab labeling to be off when the "+" button - // is used in the tab bar. This fixes that. If we can find a more robust - // solution we should do that. - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { controller.relabelTabs() } - } - - /// Creates a window controller, adds it to our managed list, and returns it. - func createWindow(withBaseConfig base: Ghostty.SurfaceConfiguration? = nil, - withSurfaceTree tree: SplitTree? = nil) -> TerminalController { - // Initialize our controller to load the window - let c = TerminalController(ghostty, withBaseConfig: base, withSurfaceTree: tree) - - // Create a listener for when the window is closed so we can remove it. - let pubClose = NotificationCenter.default.publisher( - for: NSWindow.willCloseNotification, - object: c.window! - ).sink { notification in - guard let window = notification.object as? NSWindow else { return } - guard let c = window.windowController as? TerminalController else { return } - self.removeWindow(c) - } - - // Keep track of every window we manage - windows.append(Window( - controller: c, - closePublisher: pubClose - )) - - return c - } - - func removeWindow(_ controller: TerminalController) { - // Remove it from our managed set - guard let idx = self.windows.firstIndex(where: { $0.controller == controller }) else { return } - let w = self.windows[idx] - self.windows.remove(at: idx) - - // Ensure any publishers we have are cancelled - w.closePublisher.cancel() - - // If we remove a window, we reset the cascade point to the key window so that - // the next window cascade's from that one. - if let focusedWindow = NSApplication.shared.keyWindow { - // If we are NOT the focused window, then we are a tabbed window. If we - // are closing a tabbed window, we want to set the cascade point to be - // the next cascade point from this window. - if focusedWindow != controller.window { - // The cascadeTopLeft call below should NOT move the window. Starting with - // macOS 15, we found that specifically when used with the new window snapping - // features of macOS 15, this WOULD move the frame. So we keep track of the - // old frame and restore it if necessary. Issue: - // https://github.com/ghostty-org/ghostty/issues/2565 - let oldFrame = focusedWindow.frame - - Self.lastCascadePoint = focusedWindow.cascadeTopLeft(from: NSZeroPoint) - - if focusedWindow.frame != oldFrame { - focusedWindow.setFrame(oldFrame, display: true) - } - - return - } - - // If we are the focused window, then we set the last cascade point to - // our own frame so that it shows up in the same spot. - let frame = focusedWindow.frame - Self.lastCascadePoint = NSPoint(x: frame.minX, y: frame.maxY) - } - - // I don't think we strictly have to do this but if a window is - // closed I want to make sure that the app state is invalided so - // we don't reopen closed windows. - NSApplication.shared.invalidateRestorableState() - } - - /// Close all windows, asking for confirmation if necessary. - func closeAllWindows() { - var needsConfirm: Bool = false - for w in self.windows { - if w.controller.surfaceTree.contains(where: { $0.needsConfirmQuit }) { - needsConfirm = true - break - } - } - - if (!needsConfirm) { - for w in self.windows { - w.controller.close() - } - - return - } - - // If we don't have a main window, we just close all windows because - // we have no window to show the modal on top of. I'm sure there's a way - // to do an app-level alert but I don't know how and this case should never - // really happen. - guard let alertWindow = mainWindow?.controller.window else { - for w in self.windows { - w.controller.close() - } - - return - } - - // If we need confirmation by any, show one confirmation for all windows - let alert = NSAlert() - alert.messageText = "Close All Windows?" - alert.informativeText = "All terminal sessions will be terminated." - alert.addButton(withTitle: "Close All Windows") - alert.addButton(withTitle: "Cancel") - alert.alertStyle = .warning - alert.beginSheetModal(for: alertWindow, completionHandler: { response in - if (response == .alertFirstButtonReturn) { - for w in self.windows { - w.controller.close() - } - } - }) - } - - /// Relabels all the tabs with the proper keyboard shortcut. - func relabelAllTabs() { - for w in windows { - w.controller.relabelTabs() - } - } - - // MARK: - Notifications - - @objc private func onNewWindow(notification: SwiftUI.Notification) { - let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] - let config = configAny as? Ghostty.SurfaceConfiguration - self.newWindow(withBaseConfig: config) - } - - @objc private func onNewTab(notification: SwiftUI.Notification) { - guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } - guard let window = surfaceView.window else { return } - - let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] - let config = configAny as? Ghostty.SurfaceConfiguration - - self.newTab(to: window, withBaseConfig: config) - } - - @objc private func ghosttyConfigDidChange(_ notification: Notification) { - // We only care if the configuration is a global configuration, not a - // surface-specific one. - guard notification.object == nil else { return } - - // Get our managed configuration object out - guard let config = notification.userInfo?[ - Notification.Name.GhosttyConfigChangeKey - ] as? Ghostty.Config else { return } - - // Update our derived config - self.derivedConfig = DerivedConfig(config) - } - - private struct DerivedConfig { - let windowFullscreen: Bool - let windowFullscreenMode: FullscreenMode - let macosTitlebarStyle: String - let windowNewTabPosition: String - - init() { - self.windowFullscreen = false - self.windowFullscreenMode = .native - self.macosTitlebarStyle = "transparent" - self.windowNewTabPosition = "" - } - - init(_ config: Ghostty.Config) { - self.windowFullscreen = config.windowFullscreen - self.windowFullscreenMode = config.windowFullscreenMode - self.macosTitlebarStyle = config.macosTitlebarStyle - self.windowNewTabPosition = config.windowNewTabPosition - } - } -} diff --git a/macos/Sources/Features/Terminal/TerminalRestorable.swift b/macos/Sources/Features/Terminal/TerminalRestorable.swift index 5229dc46e..9d9b7ffb1 100644 --- a/macos/Sources/Features/Terminal/TerminalRestorable.swift +++ b/macos/Sources/Features/Terminal/TerminalRestorable.swift @@ -83,9 +83,9 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration { // can be found for events from libghostty. This uses the low-level // createWindow so that AppKit can place the window wherever it should // be. - let c = appDelegate.terminalManager.createWindow( - withSurfaceTree: state.surfaceTree - ) + let c = TerminalController.init( + appDelegate.ghostty, + withSurfaceTree: state.surfaceTree) guard let window = c.window else { completionHandler(nil, TerminalRestoreError.windowDidNotLoad) return From 797c10af37aa71c0e36c569d6d47faa761f1614b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 15:50:30 -0700 Subject: [PATCH 400/642] macos: undo new window --- .../Terminal/TerminalController.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 3210eda0c..f90490c3f 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -205,6 +205,27 @@ class TerminalController: BaseTerminalController { c.showWindow(self) } + // Setup our undo + if let undoManager = c.undoManager { + undoManager.setActionName("New Window") + undoManager.registerUndo( + withTarget: c, + expiresAfter: c.undoExpiration) { target in + // Close the window when undoing + target.closeWindow(nil) + + // Register redo action + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: target.undoExpiration) { ghostty in + _ = TerminalController.newWindow( + ghostty, + withBaseConfig: baseConfig, + withParent: explicitParent) + } + } + } + return c } From 636b1fff8a4d0ca43ef1794791ff582dc6a6e111 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 15:56:17 -0700 Subject: [PATCH 401/642] macos: initial window shouldn't support undo --- macos/Sources/App/macOS/AppDelegate.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index aacf8f651..013e89f58 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -260,7 +260,9 @@ class AppDelegate: NSObject, // - if we're opening a URL since `application(_:openFile:)` is called before this. // - if we're restoring from persisted state if TerminalController.all.isEmpty && derivedConfig.initialWindow { + undoManager.disableUndoRegistration() _ = TerminalController.newWindow(ghostty) + undoManager.enableUndoRegistration() } } } From d92db73f25110bd5de145ce50cb99a9925cbc8f0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 15:59:22 -0700 Subject: [PATCH 402/642] macos: undo new tab --- .../Terminal/TerminalController.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index f90490c3f..7aa8d5285 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -309,6 +309,27 @@ class TerminalController: BaseTerminalController { controller.relabelTabs() } + // Setup our undo + if let undoManager = parentController.undoManager { + undoManager.setActionName("New Tab") + undoManager.registerUndo( + withTarget: controller, + expiresAfter: controller.undoExpiration) { target in + // Close the tab when undoing + target.closeTab(nil) + + // Register redo action + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: target.undoExpiration) { ghostty in + _ = TerminalController.newTab( + ghostty, + from: parent, + withBaseConfig: baseConfig) + } + } + } + return controller } From aeede903f50ce6224c94efe1cdc338b6b8ac9f56 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 6 Jun 2025 16:03:20 -0700 Subject: [PATCH 403/642] macos: undo close all windows --- macos/Sources/Features/Terminal/TerminalController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7aa8d5285..907109e1c 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -710,7 +710,11 @@ class TerminalController: BaseTerminalController { } static private func closeAllWindowsImmediately() { - all.forEach { $0.close() } + let undoManager = (NSApp.delegate as? AppDelegate)?.undoManager + undoManager?.beginUndoGrouping() + all.forEach { $0.closeWindowImmediately() } + undoManager?.setActionName("Close All Windows") + undoManager?.endUndoGrouping() } // MARK: Undo/Redo From 396e53244d998a7b6097f256a0af1daccf9e62d3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 06:57:11 -0700 Subject: [PATCH 404/642] config: add super+shift+t as a default undo too to mimic browsers --- src/config/Config.zig | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index e1d5b548e..2df66ba45 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -4944,6 +4944,25 @@ pub const Keybinds = struct { .{ .key = .{ .unicode = 'q' }, .mods = .{ .super = true } }, .{ .quit = {} }, ); + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } }, + .{ .clear_screen = {} }, + .{ .performable = true }, + ); + try self.set.put( + alloc, + .{ .key = .{ .unicode = 'a' }, .mods = .{ .super = true } }, + .{ .select_all = {} }, + ); + + // Undo/redo + try self.set.putFlags( + alloc, + .{ .key = .{ .unicode = 't' }, .mods = .{ .super = true, .shift = true } }, + .{ .undo = {} }, + .{ .performable = true }, + ); try self.set.putFlags( alloc, .{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true } }, @@ -4956,17 +4975,6 @@ pub const Keybinds = struct { .{ .redo = {} }, .{ .performable = true }, ); - try self.set.putFlags( - alloc, - .{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } }, - .{ .clear_screen = {} }, - .{ .performable = true }, - ); - try self.set.put( - alloc, - .{ .key = .{ .unicode = 'a' }, .mods = .{ .super = true } }, - .{ .select_all = {} }, - ); // Viewport scrolling try self.set.put( From b234cb20140fc2287799496fe7b4ad13d5deb94a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 07:01:08 -0700 Subject: [PATCH 405/642] macos: only process reopen if already activated --- macos/Sources/App/macOS/AppDelegate.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 013e89f58..e5b35037e 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -349,6 +349,11 @@ class AppDelegate: NSObject, // the dock icon. guard TerminalController.all.isEmpty else { return true } + // If the application isn't active yet then we don't want to process + // this because we're not ready. This happens sometimes in Xcode runs + // but I haven't seen it happen in releases. I'm unsure why. + guard applicationHasBecomeActive else { return true } + // No visible windows, open a new one. _ = TerminalController.newWindow(ghostty) return false From 973a2afdde103c302c690f2e41e16ab53a4a86fa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 07:11:30 -0700 Subject: [PATCH 406/642] macos: make sure we're not registering unnecessary undos --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Terminal/BaseTerminalController.swift | 103 ++++++++---------- .../Terminal/TerminalController.swift | 32 ++++-- .../Sources/Helpers/ExpiringUndoManager.swift | 10 +- .../Extensions/UndoManager+Extension.swift | 20 ++++ 5 files changed, 102 insertions(+), 67 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/UndoManager+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 7da727fbb..9686dcbd1 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -64,6 +64,7 @@ A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; }; A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366E2DF25D8300E04A10 /* Duration+Extension.swift */; }; A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */; }; + A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58636722DF4813000E04A10 /* UndoManager+Extension.swift */; }; A5874D992DAD751B00E83852 /* CGS.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D982DAD751A00E83852 /* CGS.swift */; }; A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */; }; A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59444F629A2ED5200725BBA /* SettingsView.swift */; }; @@ -171,6 +172,7 @@ A586366A2DF0A98900E04A10 /* Array+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extension.swift"; sourceTree = ""; }; A586366E2DF25D8300E04A10 /* Duration+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duration+Extension.swift"; sourceTree = ""; }; A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpiringUndoManager.swift; sourceTree = ""; }; + A58636722DF4813000E04A10 /* UndoManager+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UndoManager+Extension.swift"; sourceTree = ""; }; A5874D982DAD751A00E83852 /* CGS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGS.swift; sourceTree = ""; }; A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Extension.swift"; sourceTree = ""; }; A59444F629A2ED5200725BBA /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; @@ -447,6 +449,7 @@ C1F26EA62B738B9900404083 /* NSView+Extension.swift */, A5874D9C2DAD785F00E83852 /* NSWindow+Extension.swift */, A5985CD62C320C4500C57AD3 /* String+Extension.swift */, + A58636722DF4813000E04A10 /* UndoManager+Extension.swift */, A5CC36142C9CDA03004D6760 /* View+Extension.swift */, ); path = Extensions; @@ -683,6 +686,7 @@ A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */, + A58636732DF4813400E04A10 /* UndoManager+Extension.swift in Sources */, A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */, CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */, A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e4b42c3a1..06cecf651 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -260,8 +260,8 @@ class BaseTerminalController: NSWindowController, self.alert = alert } - // MARK: Focus Management - + // MARK: Split Tree Management + /// Find the next surface to focus when a node is being closed. /// Goes to previous split unless we're the leftmost leaf, then goes to next. private func findNextFocusTargetAfterClosing(node: SplitTree.Node) -> Ghostty.SurfaceView? { @@ -282,45 +282,63 @@ class BaseTerminalController: NSWindowController, /// /// This does no confirmation and assumes confirmation is already done. private func removeSurfaceNode(_ node: SplitTree.Node) { - let nextTarget = findNextFocusTargetAfterClosing(node: node) - let oldFocused = focusedSurface - let focused = node.contains { $0 == focusedSurface } - - // Keep track of the old tree for undo management. - let oldTree = surfaceTree - - // Remove the node from the tree - surfaceTree = surfaceTree.remove(node) - // Move focus if the closed surface was focused and we have a next target - if let nextTarget, focused { + let nextFocus: Ghostty.SurfaceView? = if node.contains( + where: { $0 == focusedSurface } + ) { + findNextFocusTargetAfterClosing(node: node) + } else { + nil + } + + replaceSurfaceTree( + surfaceTree.remove(node), + moveFocusTo: nextFocus, + moveFocusFrom: focusedSurface, + undoAction: "Close Terminal" + ) + } + + private func replaceSurfaceTree( + _ newTree: SplitTree, + moveFocusTo newView: Ghostty.SurfaceView? = nil, + moveFocusFrom oldView: Ghostty.SurfaceView? = nil, + undoAction: String? = nil + ) { + // Setup our new split tree + let oldTree = surfaceTree + surfaceTree = newTree + if let newView { DispatchQueue.main.async { - Ghostty.moveFocus(to: nextTarget, from: oldFocused) + Ghostty.moveFocus(to: newView, from: oldView) } } // Setup our undo if let undoManager { - undoManager.setActionName("Close Terminal") + if let undoAction { + undoManager.setActionName(undoAction) + } undoManager.registerUndo( withTarget: self, - expiresAfter: undoExpiration) { target in + expiresAfter: undoExpiration + ) { target in target.surfaceTree = oldTree - if let oldFocused { + if let oldView { DispatchQueue.main.async { - Ghostty.moveFocus(to: oldFocused, from: target.focusedSurface) + Ghostty.moveFocus(to: oldView, from: target.focusedSurface) } } undoManager.registerUndo( withTarget: target, - expiresAfter: target.undoExpiration) { target in - target.closeSurfaceNode( - node, - withConfirmation: node.contains { - $0.needsConfirmQuit - } - ) + expiresAfter: target.undoExpiration + ) { target in + target.replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: target.focusedSurface, + undoAction: undoAction) } } } @@ -478,36 +496,11 @@ class BaseTerminalController: NSWindowController, return } - // Keep track of the old tree for undo - let oldTree = surfaceTree - - // Setup our new split tree - surfaceTree = newTree - DispatchQueue.main.async { - Ghostty.moveFocus(to: newView, from: oldView) - } - - // Setup our undo - if let undoManager { - undoManager.setActionName("New Split") - undoManager.registerUndo( - withTarget: self, - expiresAfter: undoExpiration) { target in - target.surfaceTree = oldTree - DispatchQueue.main.async { - Ghostty.moveFocus(to: oldView, from: target.focusedSurface) - } - - undoManager.registerUndo( - withTarget: target, - expiresAfter: target.undoExpiration) { target in - target.surfaceTree = newTree - DispatchQueue.main.async { - Ghostty.moveFocus(to: newView, from: target.focusedSurface) - } - } - } - } + replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: oldView, + undoAction: "New Split") } @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 907109e1c..244f8720d 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -210,14 +210,18 @@ class TerminalController: BaseTerminalController { undoManager.setActionName("New Window") undoManager.registerUndo( withTarget: c, - expiresAfter: c.undoExpiration) { target in + expiresAfter: c.undoExpiration + ) { target in // Close the window when undoing - target.closeWindow(nil) + undoManager.disableUndoRegistration { + target.closeWindow(nil) + } // Register redo action undoManager.registerUndo( withTarget: ghostty, - expiresAfter: target.undoExpiration) { ghostty in + expiresAfter: target.undoExpiration + ) { ghostty in _ = TerminalController.newWindow( ghostty, withBaseConfig: baseConfig, @@ -314,14 +318,18 @@ class TerminalController: BaseTerminalController { undoManager.setActionName("New Tab") undoManager.registerUndo( withTarget: controller, - expiresAfter: controller.undoExpiration) { target in + expiresAfter: controller.undoExpiration + ) { target in // Close the tab when undoing - target.closeTab(nil) - + undoManager.disableUndoRegistration { + target.closeTab(nil) + } + // Register redo action undoManager.registerUndo( withTarget: ghostty, - expiresAfter: target.undoExpiration) { ghostty in + expiresAfter: target.undoExpiration + ) { ghostty in _ = TerminalController.newTab( ghostty, from: parent, @@ -617,14 +625,16 @@ class TerminalController: BaseTerminalController { undoManager.setActionName("Close Tab") undoManager.registerUndo( withTarget: ghostty, - expiresAfter: undoExpiration) { ghostty in + expiresAfter: undoExpiration + ) { ghostty in let newController = TerminalController(ghostty, with: undoState) // Register redo action undoManager.registerUndo( withTarget: newController, - expiresAfter: newController.undoExpiration) { target in - target.closeTab(nil) + expiresAfter: newController.undoExpiration + ) { target in + target.closeTabImmediately() } } } @@ -654,7 +664,7 @@ class TerminalController: BaseTerminalController { undoManager.registerUndo( withTarget: newController, expiresAfter: newController.undoExpiration) { target in - target.closeWindow(nil) + target.closeWindowImmediately() } } } diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift index 9a9349cf3..5fde0e870 100644 --- a/macos/Sources/Helpers/ExpiringUndoManager.swift +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -32,6 +32,11 @@ class ExpiringUndoManager: UndoManager { // Ignore instantly expiring undos guard duration.timeInterval > 0 else { return } + // Ignore when undo registration is disabled. UndoManager still lets + // registration happen then cancels later but I was seeing some + // weird behavior with this so let's just guard on it. + guard self.isUndoRegistrationEnabled else { return } + let expiringTarget = ExpiringTarget( target, expiresAfter: duration, @@ -64,7 +69,10 @@ class ExpiringUndoManager: UndoManager { // Call super to handle standard removal super.removeAllActions(withTarget: target) - if !(target is ExpiringTarget) { + // If the target is an expiring target, remove it. + if let expiring = target as? ExpiringTarget { + expiringTargets.remove(expiring) + } else { // Find and remove any ExpiringTarget instances that wrap this target. expiringTargets .filter { $0.target == nil || $0.target === (target as AnyObject) } diff --git a/macos/Sources/Helpers/Extensions/UndoManager+Extension.swift b/macos/Sources/Helpers/Extensions/UndoManager+Extension.swift new file mode 100644 index 000000000..6c7c1e9f1 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/UndoManager+Extension.swift @@ -0,0 +1,20 @@ +import Foundation + +extension UndoManager { + /// A Boolean value that indicates whether the undo manager is currently performing + /// either an undo or redo operation. + var isUndoingOrRedoing: Bool { + isUndoing || isRedoing + } + + /// Temporarily disables undo registration while executing the provided handler. + /// + /// This method provides a convenient way to perform operations without recording them + /// in the undo stack. It ensures that undo registration is properly re-enabled even + /// if the handler throws an error. + func disableUndoRegistration(handler: () -> Void) { + disableUndoRegistration() + handler() + enableUndoRegistration() + } +} From 20744f0482e1369039b3b565bd782a4bad786e8d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 12:22:37 -0700 Subject: [PATCH 407/642] macos: fix some CI build issues --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 06cecf651..594a58056 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -430,7 +430,7 @@ class BaseTerminalController: NSWindowController, /// This will also insert the proper undo stack information in. func closeSurfaceNode( _ node: SplitTree.Node, - withConfirmation: Bool = true, + withConfirmation: Bool = true ) { // This node must be part of our tree guard surfaceTree.contains(node) else { return } From 6e77a5a6ca05c1416a1c19c5c61b76566574ea71 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 13:07:05 -0700 Subject: [PATCH 408/642] macos: address quick terminal basic functionality with new API --- include/ghostty.h | 1 + macos/Sources/App/macOS/AppDelegate.swift | 4 ++ .../QuickTerminalController.swift | 58 +++++++++++++++---- .../Sources/Ghostty/SurfaceView_AppKit.swift | 6 ++ src/apprt/embedded.zig | 5 ++ 5 files changed, 62 insertions(+), 12 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 95bd58cd7..9f17d0b97 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -786,6 +786,7 @@ ghostty_app_t ghostty_surface_app(ghostty_surface_t); ghostty_surface_config_s ghostty_surface_inherited_config(ghostty_surface_t); void ghostty_surface_update_config(ghostty_surface_t, ghostty_config_t); bool ghostty_surface_needs_confirm_quit(ghostty_surface_t); +bool ghostty_surface_process_exited(ghostty_surface_t); void ghostty_surface_refresh(ghostty_surface_t); void ghostty_surface_draw(ghostty_surface_t); void ghostty_surface_set_content_scale(ghostty_surface_t, double, double); diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index e5b35037e..7fb52a025 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -612,6 +612,10 @@ class AppDelegate: NSObject, guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } guard let window = surfaceView.window else { return } + // We only want to listen to new tabs if the focused parent is + // a regular terminal controller. + guard window.windowController is TerminalController else { return } + let configAny = notification.userInfo?[Ghostty.Notification.NewSurfaceConfigKey] let config = configAny as? Ghostty.SurfaceConfiguration diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 8c86c2531..ce5f07616 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -61,6 +61,12 @@ class QuickTerminalController: BaseTerminalController { selector: #selector(ghosttyConfigDidChange(_:)), name: .ghosttyConfigDidChange, object: nil) + center.addObserver( + self, + selector: #selector(closeWindow(_:)), + name: .ghosttyCloseWindow, + object: nil + ) center.addObserver( self, selector: #selector(onNewTab), @@ -198,16 +204,38 @@ class QuickTerminalController: BaseTerminalController { // If our surface tree is nil then we animate the window out. if (to.isEmpty) { - // Save the current window frame before animating out. This preserves - // the user's preferred window size and position for when the quick - // terminal is reactivated with a new surface. Without this, SwiftUI - // would reset the window to its minimum content size. - lastClosedFrame = window?.frame - animateOut() } } + override func closeSurfaceNode( + _ node: SplitTree.Node, + withConfirmation: Bool = true + ) { + // If this isn't the root then we're dealing with a split closure. + if surfaceTree.root != node { + super.closeSurfaceNode(node, withConfirmation: withConfirmation) + return + } + + // If this isn't a final leaf then we're dealing with a split closure + guard case .leaf(let surface) = node else { + super.closeSurfaceNode(node, withConfirmation: withConfirmation) + return + } + + // If its the root, we check if the process exited. If it did, + // then we do empty the tree. + if surface.processExited { + surfaceTree = .init() + return + } + + // If its the root then we just animate out. We never actually allow + // the surface to fully close. + animateOut() + } + // MARK: Methods func toggle() { @@ -252,12 +280,6 @@ class QuickTerminalController: BaseTerminalController { let view = Ghostty.SurfaceView(ghostty_app, baseConfig: nil) surfaceTree = SplitTree(view: view) focusedSurface = view - - // Restore our previous frame if we have one - if let lastClosedFrame { - window.setFrame(lastClosedFrame, display: false) - self.lastClosedFrame = nil - } } // Animate the window in @@ -283,6 +305,12 @@ class QuickTerminalController: BaseTerminalController { private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) { guard let screen = derivedConfig.quickTerminalScreen.screen else { return } + // Restore our previous frame if we have one + if let lastClosedFrame { + window.setFrame(lastClosedFrame, display: false) + self.lastClosedFrame = nil + } + // Move our window off screen to the top position.setInitial(in: window, on: screen) @@ -393,6 +421,12 @@ class QuickTerminalController: BaseTerminalController { } private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) { + // Save the current window frame before animating out. This preserves + // the user's preferred window size and position for when the quick + // terminal is reactivated with a new surface. Without this, SwiftUI + // would reset the window to its minimum content size. + lastClosedFrame = window.frame + // If we hid the dock then we unhide it. hiddenDock = nil diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 682efa947..ea9a8c61b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -92,6 +92,12 @@ extension Ghostty { return ghostty_surface_needs_confirm_quit(surface) } + // Retruns true if the process in this surface has exited. + var processExited: Bool { + guard let surface = self.surface else { return true } + return ghostty_surface_process_exited(surface) + } + // Returns the inspector instance for this surface, or nil if the // surface has been closed. var inspector: ghostty_inspector_t? { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 67aeeaf7c..5334c8ecd 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1359,6 +1359,11 @@ pub const CAPI = struct { return surface.core_surface.needsConfirmQuit(); } + /// Returns true if the surface process has exited. + export fn ghostty_surface_process_exited(surface: *Surface) bool { + return surface.core_surface.child_exited; + } + /// Returns true if the surface has a selection. export fn ghostty_surface_has_selection(surface: *Surface) bool { return surface.core_surface.hasSelection(); From 6f6d493763f9fb77c9105578e6a748d014390b73 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 7 Jun 2025 13:13:57 -0700 Subject: [PATCH 409/642] macos: show quick terminal on undo/redo --- .../QuickTerminal/QuickTerminalController.swift | 14 ++++++++++++-- macos/Sources/Ghostty/SurfaceView_AppKit.swift | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index ce5f07616..28dea9579 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -202,9 +202,19 @@ class QuickTerminalController: BaseTerminalController { override func surfaceTreeDidChange(from: SplitTree, to: SplitTree) { super.surfaceTreeDidChange(from: from, to: to) - // If our surface tree is nil then we animate the window out. - if (to.isEmpty) { + // If our surface tree is nil then we animate the window out. We + // defer reinitializing the tree to save some memory here. + if to.isEmpty { animateOut() + return + } + + // If we're not empty (e.g. this isn't the first set) and we're + // not visible, then we animate in. This allows us to show the quick + // terminal when things such as undo/redo are done. + if !from.isEmpty && !visible { + animateIn() + return } } diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index ea9a8c61b..e4f6f507c 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -92,7 +92,7 @@ extension Ghostty { return ghostty_surface_needs_confirm_quit(surface) } - // Retruns true if the process in this surface has exited. + // Returns true if the process in this surface has exited. var processExited: Bool { guard let surface = self.surface else { return true } return ghostty_surface_process_exited(surface) From ba15da47229ff5859b1be939b40b28f4cd218ace Mon Sep 17 00:00:00 2001 From: Leah Amelia Chen Date: Sun, 8 Jun 2025 01:03:12 +0200 Subject: [PATCH 410/642] input: parse binds containing equal signs correctly Since the W3C rewrite we're able to specify codepoints like `+` directly in the config format who otherwise have special meanings. Turns out we forgot to do the same for `=`. --- src/input/Binding.zig | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index e5d434265..757c4ff24 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -63,9 +63,11 @@ pub const Parser = struct { const flags, const start_idx = try parseFlags(raw_input); const input = raw_input[start_idx..]; - // Find the first = which splits are mapping into the trigger + // Find the last = which splits are mapping into the trigger // and action, respectively. - const eql_idx = std.mem.indexOf(u8, input, "=") orelse return Error.InvalidFormat; + // We use the last = because the keybind itself could contain + // raw equal signs (for the = codepoint) + const eql_idx = std.mem.lastIndexOf(u8, input, "=") orelse return Error.InvalidFormat; // Sequence iterator goes up to the equal, action is after. We can // parse the action now. @@ -2050,6 +2052,32 @@ test "parse: plus sign" { try testing.expectError(Error.InvalidFormat, parseSingle("++=ignore")); } +test "parse: equals sign" { + const testing = std.testing; + + try testing.expectEqual( + Binding{ + .trigger = .{ .key = .{ .unicode = '=' } }, + .action = .ignore, + }, + try parseSingle("==ignore"), + ); + + // Modifier + try testing.expectEqual( + Binding{ + .trigger = .{ + .key = .{ .unicode = '=' }, + .mods = .{ .ctrl = true }, + }, + .action = .ignore, + }, + try parseSingle("ctrl+==ignore"), + ); + + try testing.expectError(Error.InvalidFormat, parseSingle("=ignore")); +} + // For Ghostty 1.2+ we changed our key names to match the W3C and removed // `physical:`. This tests the backwards compatibility with the old format. // Note that our backwards compatibility isn't 100% perfect since triggers From 3b33813071650a4a0ce0a7c9e5e3275d1767bfc5 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Sun, 8 Jun 2025 00:14:39 +0000 Subject: [PATCH 411/642] deps: Update iTerm2 color schemes --- build.zig.zon | 4 ++-- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 3c6ed95ed..fa071dbfe 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -103,8 +103,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz", - .hash = "N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz", + .hash = "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index b1d919f3a..ee2f14508 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -54,10 +54,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj": { + "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz", - "hash": "sha256-2AsOCV9RymfDbhFFRdNVE+GYCAmE713tM27TBPKxAW0=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz", + "hash": "sha256-TcrTZUCVetvAFGX0fBqg3zlG90flljScNr/OYR/MJ5Y=" }, "N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": { "name": "libpng", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index ce4a656c7..e28a2a0dd 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -170,11 +170,11 @@ in }; } { - name = "N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj"; + name = "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz"; - hash = "sha256-2AsOCV9RymfDbhFFRdNVE+GYCAmE713tM27TBPKxAW0="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz"; + hash = "sha256-TcrTZUCVetvAFGX0fBqg3zlG90flljScNr/OYR/MJ5Y="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index cb8195752..3335b9574 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -27,7 +27,7 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5. https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index d56e6d121..fb032fe82 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -67,9 +67,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/273a780bcd7e8a514d49ff42612b6856601cc052.tar.gz", - "dest": "vendor/p/N-V-__8AAIgLXgT76kRaZJzBE-1ZTXqaSx2jjIvPpIsnL2Gj", - "sha256": "d80b0e095f51ca67c36e114545d35513e198080984ef5ded336ed304f2b1016d" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz", + "dest": "vendor/p/N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj", + "sha256": "4dcad36540957adbc01465f47c1aa0df3946f747e596349c36bfce611fcc2796" }, { "type": "archive", From 0e74b8027aef60e9b8bf600ee1f88b03463661f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Mon, 7 Apr 2025 23:43:11 -0400 Subject: [PATCH 412/642] pwd: fix hostname resolution on macos When macOS's "Private WiFi address" feature is enabled it'll change the hostname to a mac address. Mac addresses look like URIs with a hostname and port component, e.g. 12:34:56:78:90:12 where `:12` looks like port 12. However, mac addresses can also contain letters a through f, so a valid mac address like ab:cd:ef:ab:cd:ef is valid, but will not be parsed as a URI, because `:ef` is not a valid port. This commit attempts to fix that by checking if the hostname is a valid mac address when `std.Uri.parse()` fails and constructing a new std.Uri struct using that information. It's not perfect, but is equally compliant with the URI spec as std.Uri currently is. --- src/termio/stream_handler.zig | 66 +++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b3aa82d20..5327d8b36 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1041,6 +1041,13 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } + fn isUriPathSeparator(c: u8) bool { + return switch (c) { + '?', '#' => true, + else => false, + }; + } + pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any @@ -1069,9 +1076,62 @@ pub const StreamHandler = struct { return; } - const uri = std.Uri.parse(url) catch |e| { - log.warn("invalid url in OSC 7: {}", .{e}); - return; + const uri: std.Uri = uri: { + const uri = std.Uri.parse(url) catch |e| { + // It's possible this is a mac address on macOS where the last 2 characters in the + // address are non-digits, e.g. 'ff', and thus an invalid port. + // + // Example: file://12:34:56:78:90:12/path/to/file + + // Insufficient length to have a mac address in the hostname. + if (url.len < 24) { + log.warn("invalid url in OSC 7: {}", .{e}); + return; + } + + // The first '/' after the scheme marks the end of the hostname. If the hostname is + // not 17 characters, it's not a mac address. + if (std.mem.indexOfScalarPos(u8, url, 7, '/') != 24) { + log.warn("invalid url in OSC 7: {}", .{e}); + return; + } + + // At this point we have a potential mac address as the hostname. + const mac_address = url[7..24]; + + for (0..mac_address.len) |i| { + const c = mac_address[i]; + + if (i + 1 % 3 == 0) { + if (c != ':') { + log.warn("invalid url in OSC 7: {}", .{e}); + return; + } + } else { + if (!std.mem.containsAtLeastScalar(u8, "0123456789abcdef", 1, mac_address[i])) { + log.warn("invalid url in OSC 7: {}", .{e}); + return; + } + } + } + + // At this point we have what looks like a valid mac address. + + var uri_path_end_idx: usize = 24; + while (uri_path_end_idx < url.len and !isUriPathSeparator(url[uri_path_end_idx])) { + uri_path_end_idx += 1; + } + + // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI + // spec. + break :uri .{ + .scheme = "file://", + .host = .{ .percent_encoded = mac_address }, + .path = .{ .percent_encoded = url[24..uri_path_end_idx] }, + }; + }; + + break :uri uri; }; if (!std.mem.eql(u8, "file", uri.scheme) and From 19ca1bfb1ca950bd7ddcb58718903bafc7777944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Wed, 30 Apr 2025 23:54:42 -0400 Subject: [PATCH 413/642] Fix modulo operation and custom Uri struct init --- src/termio/stream_handler.zig | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 5327d8b36..6668b17cd 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1082,17 +1082,18 @@ pub const StreamHandler = struct { // address are non-digits, e.g. 'ff', and thus an invalid port. // // Example: file://12:34:56:78:90:12/path/to/file + if (e != error.InvalidPort) return; // Insufficient length to have a mac address in the hostname. if (url.len < 24) { - log.warn("invalid url in OSC 7: {}", .{e}); + log.warn("invalid MAC address in OSC 7: insufficient length", .{}); return; } // The first '/' after the scheme marks the end of the hostname. If the hostname is // not 17 characters, it's not a mac address. if (std.mem.indexOfScalarPos(u8, url, 7, '/') != 24) { - log.warn("invalid url in OSC 7: {}", .{e}); + log.warn("invalid MAC address in OSC 7: invalid scheme", .{}); return; } @@ -1102,14 +1103,14 @@ pub const StreamHandler = struct { for (0..mac_address.len) |i| { const c = mac_address[i]; - if (i + 1 % 3 == 0) { + if ((i + 1) % 3 == 0) { if (c != ':') { - log.warn("invalid url in OSC 7: {}", .{e}); + log.warn("invalid MAC address in OSC 7: missing colon", .{}); return; } } else { - if (!std.mem.containsAtLeastScalar(u8, "0123456789abcdef", 1, mac_address[i])) { - log.warn("invalid url in OSC 7: {}", .{e}); + if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, mac_address[i])) { + log.warn("invalid MAC address in OSC 7: invalid character '{c}' at position '{d}'", .{ mac_address[i], i }); return; } } @@ -1125,7 +1126,7 @@ pub const StreamHandler = struct { // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI // spec. break :uri .{ - .scheme = "file://", + .scheme = "file", .host = .{ .percent_encoded = mac_address }, .path = .{ .percent_encoded = url[24..uri_path_end_idx] }, }; From b66368b4d6e5e7eb3cc26a97574b09d6bfdbb49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 1 May 2025 00:09:34 -0400 Subject: [PATCH 414/642] extract mac address validity check to function --- src/termio/stream_handler.zig | 57 +++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 6668b17cd..ba04ee6b1 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1048,6 +1048,29 @@ pub const StreamHandler = struct { }; } + fn isValidMacAddress(mac_address: []const u8) bool { + // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. + if (mac_address.len != 17) { + return false; + } + + for (0..mac_address.len) |i| { + const c = mac_address[i]; + + if ((i + 1) % 3 == 0) { + if (c != ':') { + return false; + } + } else { + if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { + return false; + } + } + } + + return true; + } + pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any @@ -1084,40 +1107,22 @@ pub const StreamHandler = struct { // Example: file://12:34:56:78:90:12/path/to/file if (e != error.InvalidPort) return; - // Insufficient length to have a mac address in the hostname. - if (url.len < 24) { - log.warn("invalid MAC address in OSC 7: insufficient length", .{}); - return; - } - - // The first '/' after the scheme marks the end of the hostname. If the hostname is - // not 17 characters, it's not a mac address. + // The first '/' after the scheme marks the end of the hostname. If the first '/' + // following the end of the `file://` scheme is not at position 24 this is not a + // valid mac address. if (std.mem.indexOfScalarPos(u8, url, 7, '/') != 24) { - log.warn("invalid MAC address in OSC 7: invalid scheme", .{}); + log.warn("invalid url in OSC 7: {}", .{e}); return; } - // At this point we have a potential mac address as the hostname. + // At this point we may have a mac address as the hostname. const mac_address = url[7..24]; - for (0..mac_address.len) |i| { - const c = mac_address[i]; - - if ((i + 1) % 3 == 0) { - if (c != ':') { - log.warn("invalid MAC address in OSC 7: missing colon", .{}); - return; - } - } else { - if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, mac_address[i])) { - log.warn("invalid MAC address in OSC 7: invalid character '{c}' at position '{d}'", .{ mac_address[i], i }); - return; - } - } + if (!isValidMacAddress(mac_address)) { + log.warn("ivalid url in OSC 7: {}", .{e}); + return; } - // At this point we have what looks like a valid mac address. - var uri_path_end_idx: usize = 24; while (uri_path_end_idx < url.len and !isUriPathSeparator(url[uri_path_end_idx])) { uri_path_end_idx += 1; From 64bfaf23f92fe92d1799088425d3e1c587f63231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 1 May 2025 00:18:42 -0400 Subject: [PATCH 415/642] take kitty-shell-cwd scheme into account --- src/termio/stream_handler.zig | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index ba04ee6b1..7bb604936 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1107,24 +1107,34 @@ pub const StreamHandler = struct { // Example: file://12:34:56:78:90:12/path/to/file if (e != error.InvalidPort) return; + const url_without_scheme = url: { + if (std.mem.startsWith(u8, url, "file://")) break :url url[7..]; + if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url url[18..]; + + log.warn("invalid url in OSC 7: invalid scheme", .{}); + return; + }; + // The first '/' after the scheme marks the end of the hostname. If the first '/' - // following the end of the `file://` scheme is not at position 24 this is not a + // following the end of the scheme is not at the right position this is not a // valid mac address. - if (std.mem.indexOfScalarPos(u8, url, 7, '/') != 24) { + if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) { log.warn("invalid url in OSC 7: {}", .{e}); return; } // At this point we may have a mac address as the hostname. - const mac_address = url[7..24]; + const mac_address = url_without_scheme[0..17]; if (!isValidMacAddress(mac_address)) { log.warn("ivalid url in OSC 7: {}", .{e}); return; } - var uri_path_end_idx: usize = 24; - while (uri_path_end_idx < url.len and !isUriPathSeparator(url[uri_path_end_idx])) { + var uri_path_end_idx: usize = 17; + while (uri_path_end_idx < url_without_scheme.len and + !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) + { uri_path_end_idx += 1; } @@ -1133,7 +1143,7 @@ pub const StreamHandler = struct { break :uri .{ .scheme = "file", .host = .{ .percent_encoded = mac_address }, - .path = .{ .percent_encoded = url[24..uri_path_end_idx] }, + .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, }; }; From ffe7f0d8bfc385fe9e1afed21e3973fdabb27283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 1 May 2025 00:24:37 -0400 Subject: [PATCH 416/642] extract url parsing into its own function --- src/termio/stream_handler.zig | 98 +++++++++++++++++------------------ 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 7bb604936..d57bdb1ac 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1071,6 +1071,52 @@ pub const StreamHandler = struct { return true; } + fn parseUrl(url: []const u8) !std.Uri { + return std.Uri.parse(url) catch |e| { + // It's possible this is a mac address on macOS where the last 2 characters in the + // address are non-digits, e.g. 'ff', and thus an invalid port. + // + // Example: file://12:34:56:78:90:12/path/to/file + if (e != error.InvalidPort) return e; + + const url_without_scheme = url: { + if (std.mem.startsWith(u8, url, "file://")) break :url url[7..]; + if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url url[18..]; + + return error.UnsupportedScheme; + }; + + // The first '/' after the scheme marks the end of the hostname. If the first '/' + // following the end of the scheme is not at the right position this is not a + // valid mac address. + if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) { + return error.HostnameIsNotMacAddress; + } + + // At this point we may have a mac address as the hostname. + const mac_address = url_without_scheme[0..17]; + + if (!isValidMacAddress(mac_address)) { + return error.HostnameIsNotMacAddress; + } + + var uri_path_end_idx: usize = 17; + while (uri_path_end_idx < url_without_scheme.len and + !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) + { + uri_path_end_idx += 1; + } + + // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI + // spec. + return .{ + .scheme = "file", + .host = .{ .percent_encoded = mac_address }, + .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, + }; + }; + } + pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any @@ -1099,55 +1145,9 @@ pub const StreamHandler = struct { return; } - const uri: std.Uri = uri: { - const uri = std.Uri.parse(url) catch |e| { - // It's possible this is a mac address on macOS where the last 2 characters in the - // address are non-digits, e.g. 'ff', and thus an invalid port. - // - // Example: file://12:34:56:78:90:12/path/to/file - if (e != error.InvalidPort) return; - - const url_without_scheme = url: { - if (std.mem.startsWith(u8, url, "file://")) break :url url[7..]; - if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url url[18..]; - - log.warn("invalid url in OSC 7: invalid scheme", .{}); - return; - }; - - // The first '/' after the scheme marks the end of the hostname. If the first '/' - // following the end of the scheme is not at the right position this is not a - // valid mac address. - if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) { - log.warn("invalid url in OSC 7: {}", .{e}); - return; - } - - // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..17]; - - if (!isValidMacAddress(mac_address)) { - log.warn("ivalid url in OSC 7: {}", .{e}); - return; - } - - var uri_path_end_idx: usize = 17; - while (uri_path_end_idx < url_without_scheme.len and - !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) - { - uri_path_end_idx += 1; - } - - // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI - // spec. - break :uri .{ - .scheme = "file", - .host = .{ .percent_encoded = mac_address }, - .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, - }; - }; - - break :uri uri; + const uri: std.Uri = parseUrl(url) catch |e| { + log.warn("invalid url in OSC 7: {}", .{e}); + return; }; if (!std.mem.eql(u8, "file", uri.scheme) and From e0655a7f75973dbc9013458587dac0c4e12ce66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Thu, 8 May 2025 22:36:37 -0400 Subject: [PATCH 417/642] Move url parsing helper to os/hostname Also adds a test to verify that the function is working as intended. --- src/os/hostname.zig | 154 ++++++++++++++++++++++++++++++++++ src/termio/stream_handler.zig | 78 +---------------- 2 files changed, 155 insertions(+), 77 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 22f29ceff..eb6c7052c 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -6,6 +6,91 @@ pub const HostnameParsingError = error{ NoSpaceLeft, }; +fn isUriPathSeparator(c: u8) bool { + return switch (c) { + '?', '#' => true, + else => false, + }; +} + +fn isValidMacAddress(mac_address: []const u8) bool { + // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. + if (mac_address.len != 17) { + return false; + } + + for (0..mac_address.len) |i| { + const c = mac_address[i]; + + if ((i + 1) % 3 == 0) { + if (c != ':') { + return false; + } + } else { + if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { + return false; + } + } + } + + return true; +} + +/// Parses the provided url to a `std.Uri` struct. This is very specific to getting hostname and +/// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS +/// the url passed to this function might have a mac address as its hostname and parses it +/// correctly. +pub fn parseUrl(url: []const u8) !std.Uri { + return std.Uri.parse(url) catch |e| { + // It's possible this is a mac address on macOS where the last 2 characters in the + // address are non-digits, e.g. 'ff', and thus an invalid port. + // + // Example: file://12:34:56:78:90:12/path/to/file + if (e != error.InvalidPort) return e; + + const scheme, const url_without_scheme = url: { + if (std.mem.startsWith(u8, url, "file://")) break :url .{ "file", url[7..] }; + if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url .{ + "kitty-shell-cwd", + url[18..], + }; + + return error.UnsupportedScheme; + }; + + // The first '/' after the scheme marks the end of the hostname. If the first '/' + // following the end of the scheme is not at the right position this is not a + // valid mac address. + if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17 and + url_without_scheme.len != 17) + { + return error.HostnameIsNotMacAddress; + } + + // At this point we may have a mac address as the hostname. + const mac_address = url_without_scheme[0..17]; + + if (!isValidMacAddress(mac_address)) { + return error.HostnameIsNotMacAddress; + } + + var uri_path_end_idx: usize = 17; + while (uri_path_end_idx < url_without_scheme.len and + !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) + { + uri_path_end_idx += 1; + } + + // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI + // spec. + return .{ + .scheme = scheme, + .host = .{ .percent_encoded = mac_address }, + .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, + }; + }; +} + /// Print the hostname from a file URI into a buffer. pub fn bufPrintHostnameFromFileUri( buf: []u8, @@ -70,6 +155,67 @@ pub fn isLocalHostname(hostname: []const u8) LocalHostnameValidationError!bool { return std.mem.eql(u8, hostname, ourHostname); } +test parseUrl { + // 1. Typical hostnames. + + var uri = try parseUrl("file://personal.computer/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://personal.computer/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("personal.computer", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + // 2. Hostnames that are mac addresses. + + // Numerical mac addresses. + + uri = try parseUrl("file://12:34:56:78:90:12/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + // Alphabetical mac addresses. + + uri = try parseUrl("file://ab:cd:ef:ab:cd:ef/home/test/"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef/home/test/"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); +} + +test "parseUrl succeeds even if path component is missing" { + const uri = try parseUrl("file://12:34:56:78:90:ab"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90:ab", uri.host.?.percent_encoded); + try std.testing.expect(uri.path.isEmpty()); + try std.testing.expect(uri.port == null); +} + test "bufPrintHostnameFromFileUri succeeds with ascii hostname" { const uri = try std.Uri.parse("file://localhost/"); @@ -86,6 +232,14 @@ test "bufPrintHostnameFromFileUri succeeds with hostname as mac address" { try std.testing.expectEqualStrings("12:34:56:78:90:12", actual); } +test "bufPrintHostnameFromFileUri succeeds with hostname as mac address with the last component as ascii" { + const uri = try parseUrl("file://12:34:56:78:90:ab"); + + var buf: [posix.HOST_NAME_MAX]u8 = undefined; + const actual = try bufPrintHostnameFromFileUri(&buf, uri); + try std.testing.expectEqualStrings("12:34:56:78:90:ab", actual); +} + test "bufPrintHostnameFromFileUri succeeds with hostname as a mac address and the last section is < 10" { const uri = try std.Uri.parse("file://12:34:56:78:90:05"); diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index d57bdb1ac..a238c2a59 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1041,82 +1041,6 @@ pub const StreamHandler = struct { self.terminal.markSemanticPrompt(.command); } - fn isUriPathSeparator(c: u8) bool { - return switch (c) { - '?', '#' => true, - else => false, - }; - } - - fn isValidMacAddress(mac_address: []const u8) bool { - // A valid mac address has 6 two-character components with 5 colons, e.g. 12:34:56:ab:cd:ef. - if (mac_address.len != 17) { - return false; - } - - for (0..mac_address.len) |i| { - const c = mac_address[i]; - - if ((i + 1) % 3 == 0) { - if (c != ':') { - return false; - } - } else { - if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { - return false; - } - } - } - - return true; - } - - fn parseUrl(url: []const u8) !std.Uri { - return std.Uri.parse(url) catch |e| { - // It's possible this is a mac address on macOS where the last 2 characters in the - // address are non-digits, e.g. 'ff', and thus an invalid port. - // - // Example: file://12:34:56:78:90:12/path/to/file - if (e != error.InvalidPort) return e; - - const url_without_scheme = url: { - if (std.mem.startsWith(u8, url, "file://")) break :url url[7..]; - if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url url[18..]; - - return error.UnsupportedScheme; - }; - - // The first '/' after the scheme marks the end of the hostname. If the first '/' - // following the end of the scheme is not at the right position this is not a - // valid mac address. - if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) { - return error.HostnameIsNotMacAddress; - } - - // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..17]; - - if (!isValidMacAddress(mac_address)) { - return error.HostnameIsNotMacAddress; - } - - var uri_path_end_idx: usize = 17; - while (uri_path_end_idx < url_without_scheme.len and - !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) - { - uri_path_end_idx += 1; - } - - // Same compliance factor as std.Uri.parse(), i.e. not at all compliant with the URI - // spec. - return .{ - .scheme = "file", - .host = .{ .percent_encoded = mac_address }, - .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, - }; - }; - } - pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { // Special handling for the empty URL. We treat the empty URL // as resetting the pwd as if we never saw a pwd. I can't find any @@ -1145,7 +1069,7 @@ pub const StreamHandler = struct { return; } - const uri: std.Uri = parseUrl(url) catch |e| { + const uri: std.Uri = internal_os.hostname.parseUrl(url) catch |e| { log.warn("invalid url in OSC 7: {}", .{e}); return; }; From 7a639a71197204254f869f50bde9cfb316acf40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:00:16 -0400 Subject: [PATCH 418/642] use iterator syntax in for loop --- src/os/hostname.zig | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index eb6c7052c..ef5ecbcf7 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -19,9 +19,7 @@ fn isValidMacAddress(mac_address: []const u8) bool { return false; } - for (0..mac_address.len) |i| { - const c = mac_address[i]; - + for (mac_address, 0..) |c, i| { if ((i + 1) % 3 == 0) { if (c != ':') { return false; From bb07e9c0261e229a99502871aa359e983bf81b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:06:53 -0400 Subject: [PATCH 419/642] don't rely on hard-coded schemes --- src/os/hostname.zig | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index ef5ecbcf7..747c145d4 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -46,15 +46,11 @@ pub fn parseUrl(url: []const u8) !std.Uri { // Example: file://12:34:56:78:90:12/path/to/file if (e != error.InvalidPort) return e; - const scheme, const url_without_scheme = url: { - if (std.mem.startsWith(u8, url, "file://")) break :url .{ "file", url[7..] }; - if (std.mem.startsWith(u8, url, "kitty-shell-cwd://")) break :url .{ - "kitty-shell-cwd", - url[18..], - }; - - return error.UnsupportedScheme; + const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse { + return error.InvalidScheme; }; + const scheme = url[0..url_without_scheme_start]; + const url_without_scheme = url[url_without_scheme_start + 3 ..]; // The first '/' after the scheme marks the end of the hostname. If the first '/' // following the end of the scheme is not at the right position this is not a From a24d0c9faf83ebe486fc4dfd660a9ee4bc78c52c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:15:23 -0400 Subject: [PATCH 420/642] re-order end-of-hostname validity check --- src/os/hostname.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 747c145d4..998b80fac 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -55,8 +55,8 @@ pub fn parseUrl(url: []const u8) !std.Uri { // The first '/' after the scheme marks the end of the hostname. If the first '/' // following the end of the scheme is not at the right position this is not a // valid mac address. - if (std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17 and - url_without_scheme.len != 17) + if (url_without_scheme.len != 17 and + std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) { return error.HostnameIsNotMacAddress; } From dfdb588f581d1fca3230ca1d2437c5e0cee53833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:15:53 -0400 Subject: [PATCH 421/642] add tests for hostnames without a path component --- src/os/hostname.zig | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 998b80fac..a04f9d4ab 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -199,6 +199,40 @@ test parseUrl { try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); try std.testing.expectEqualStrings("/home/test/", uri.path.percent_encoded); try std.testing.expect(uri.port == null); + + // 3. Hostnames that are mac addresses with no path. + + // Numerical mac addresses. + + uri = try parseUrl("file://12:34:56:78:90:12"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + uri = try parseUrl("kitty-shell-cwd://12:34:56:78:90:12"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("12:34:56:78:90", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == 12); + + // Alphabetical mac addresses. + + uri = try parseUrl("file://ab:cd:ef:ab:cd:ef"); + + try std.testing.expectEqualStrings("file", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); + + uri = try parseUrl("kitty-shell-cwd://ab:cd:ef:ab:cd:ef"); + + try std.testing.expectEqualStrings("kitty-shell-cwd", uri.scheme); + try std.testing.expectEqualStrings("ab:cd:ef:ab:cd:ef", uri.host.?.percent_encoded); + try std.testing.expectEqualStrings("", uri.path.percent_encoded); + try std.testing.expect(uri.port == null); } test "parseUrl succeeds even if path component is missing" { From 68f48b9911983cf60b258503e29900ffb928738f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:18:35 -0400 Subject: [PATCH 422/642] name the 17 magic constant `mac_address_length` --- src/os/hostname.zig | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index a04f9d4ab..4a2c5c841 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -55,20 +55,21 @@ pub fn parseUrl(url: []const u8) !std.Uri { // The first '/' after the scheme marks the end of the hostname. If the first '/' // following the end of the scheme is not at the right position this is not a // valid mac address. - if (url_without_scheme.len != 17 and - std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != 17) + const mac_address_length = 17; + if (url_without_scheme.len != mac_address_length and + std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length) { return error.HostnameIsNotMacAddress; } // At this point we may have a mac address as the hostname. - const mac_address = url_without_scheme[0..17]; + const mac_address = url_without_scheme[0..mac_address_length]; if (!isValidMacAddress(mac_address)) { return error.HostnameIsNotMacAddress; } - var uri_path_end_idx: usize = 17; + var uri_path_end_idx: usize = mac_address_length; while (uri_path_end_idx < url_without_scheme.len and !isUriPathSeparator(url_without_scheme[uri_path_end_idx])) { @@ -80,7 +81,9 @@ pub fn parseUrl(url: []const u8) !std.Uri { return .{ .scheme = scheme, .host = .{ .percent_encoded = mac_address }, - .path = .{ .percent_encoded = url_without_scheme[17..uri_path_end_idx] }, + .path = .{ + .percent_encoded = url_without_scheme[mac_address_length..uri_path_end_idx], + }, }; }; } From 7760389ab8933c7dce52c22658f988f383e00295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:29:46 -0400 Subject: [PATCH 423/642] add comptime check for platform we only need the mac-address-as-hostname workaround on macos, so we now have a comptime check to see if we're on macos. --- src/os/hostname.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 4a2c5c841..fdbe822e3 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const builtin = @import("builtin"); const posix = std.posix; pub const HostnameParsingError = error{ @@ -40,6 +41,10 @@ fn isValidMacAddress(mac_address: []const u8) bool { /// correctly. pub fn parseUrl(url: []const u8) !std.Uri { return std.Uri.parse(url) catch |e| { + // The mac-address-as-hostname issue is specific to macOS so we just return an error if we + // hit it on other platforms. + comptime if (builtin.os.tag != .macos) return e; + // It's possible this is a mac address on macOS where the last 2 characters in the // address are non-digits, e.g. 'ff', and thus an invalid port. // From e4a175d24a7ecb219ec20f92502bec2b86a29709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 13 May 2025 23:41:18 -0400 Subject: [PATCH 424/642] use explicit error set --- src/os/hostname.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index fdbe822e3..2cdba5763 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -7,6 +7,14 @@ pub const HostnameParsingError = error{ NoSpaceLeft, }; +pub const UrlParsingError = error{ + HostnameIsNotMacAddress, + InvalidFormat, + InvalidPort, + NoSchemeProvided, + UnexpectedCharacter, +}; + fn isUriPathSeparator(c: u8) bool { return switch (c) { '?', '#' => true, @@ -39,7 +47,7 @@ fn isValidMacAddress(mac_address: []const u8) bool { /// path information for Ghostty's PWD reporting functionality. Takes into account that on macOS /// the url passed to this function might have a mac address as its hostname and parses it /// correctly. -pub fn parseUrl(url: []const u8) !std.Uri { +pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri { return std.Uri.parse(url) catch |e| { // The mac-address-as-hostname issue is specific to macOS so we just return an error if we // hit it on other platforms. @@ -52,7 +60,7 @@ pub fn parseUrl(url: []const u8) !std.Uri { if (e != error.InvalidPort) return e; const url_without_scheme_start = std.mem.indexOf(u8, url, "://") orelse { - return error.InvalidScheme; + return error.NoSchemeProvided; }; const scheme = url[0..url_without_scheme_start]; const url_without_scheme = url[url_without_scheme_start + 3 ..]; From 73e5f7e5d6d0ce3bfe27a1d791585764cb17c536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Sat, 7 Jun 2025 22:12:26 -0400 Subject: [PATCH 425/642] merge std.Uri.ParseError and os/hostname error sets --- src/os/hostname.zig | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 2cdba5763..05f857b82 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -7,12 +7,9 @@ pub const HostnameParsingError = error{ NoSpaceLeft, }; -pub const UrlParsingError = error{ +pub const UrlParsingError = std.Uri.ParseError || error{ HostnameIsNotMacAddress, - InvalidFormat, - InvalidPort, NoSchemeProvided, - UnexpectedCharacter, }; fn isUriPathSeparator(c: u8) bool { From 6ed94b00346a402d85ac83a9585de17ced93f351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Sat, 7 Jun 2025 22:17:01 -0400 Subject: [PATCH 426/642] move mac address length constant to file-level scope --- src/os/hostname.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 05f857b82..3f2c53b50 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -12,6 +12,8 @@ pub const UrlParsingError = std.Uri.ParseError || error{ NoSchemeProvided, }; +const mac_address_length = 17; + fn isUriPathSeparator(c: u8) bool { return switch (c) { '?', '#' => true, @@ -65,7 +67,6 @@ pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri { // The first '/' after the scheme marks the end of the hostname. If the first '/' // following the end of the scheme is not at the right position this is not a // valid mac address. - const mac_address_length = 17; if (url_without_scheme.len != mac_address_length and std.mem.indexOfScalarPos(u8, url_without_scheme, 0, '/') != mac_address_length) { From ec043e13866cef34d1835930a0512e1a42de5736 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Jun 2025 07:00:49 -0700 Subject: [PATCH 427/642] macos: red traffic light should be undoable --- macos/Sources/Features/Terminal/TerminalController.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 244f8720d..7a241d866 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -681,7 +681,6 @@ class TerminalController: BaseTerminalController { return } - tabGroup.windows.forEach { $0.close() } } @@ -954,6 +953,13 @@ class TerminalController: BaseTerminalController { //MARK: - NSWindowDelegate + override func windowShouldClose(_ sender: NSWindow) -> Bool { + closeWindow(sender) + + // We will always explicitly close the window using the above + return false + } + override func windowWillClose(_ notification: Notification) { super.windowWillClose(notification) self.relabelTabs() From 3de3f48faf830fe1326f44b08fb9f27fa65cefcd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Jun 2025 07:29:19 -0700 Subject: [PATCH 428/642] macos: fix undo/redo for closing windows with multiple tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When closing a window that contains multiple tabs, the undo operation now properly restores all tabs as a single tabbed window rather than just restoring the active tab. The implementation: - Collects undo states from all windows in the tab group before closing - Sorts them by their original tab index to preserve order - Clears tab group references to avoid referencing garbage collected objects - Restores all windows and re-adds them as tabs to the first window - Tracks and restores which tab was focused (or focuses the last tab if none were) AI prompts that generated this commit are below. Each separate prompt is separated by a blank line, so this session was made up with many prompts in a back-and-forth conversation. > We need to update the undo/redo implementation in > @macos/Sources/Features/Terminal/TerminalController.swift `closeWindowImmediately` > to handle the case that multiple windows in a tab group are closed all at once, > and to restore them as a tabbed window. To do this, I think we should collect > all the `undoStates`, sort them by `tabIndex` (null at the end), and then on j > restore, restore them one at a time but add them back to the same tabGroup. We > can't use the tab group in the `undoState` because it will be garbage collected > by then. To be sure, we should just set it to nil. I should note at this point that the feature already worked, but the code quality and organization wasn't up to my standards. If someone using AI were just trying to make something work, they might be done at this point. I do think this is the biggest gap I worry about with AI-assisted development: bridging between the "it works" stage at a junior quality and the "it works and is maintainable" stage at a senior quality. I suspect this will be a balance of LLMs getting better but also senior code reviewers remaining highly involved in the process. > Let's extract all the work you just did into a dedicated private method > called `registerUndoForCloseWindow` Manual: made some tweaks to comments, moved some lines around, didn’t change any logic. > I think we can pull the tabIndex directly from the undoState instead of > storing it in a tuple. > Instead of `var undoStates`, I think we can create a `let undoStates` and > build and filter and sort them all in a chain of functional mappings. > Okay, looking at your logic for restoration, the `var firstController` and > conditionals are littly messy. Can you make your own pass at cleaning those > up and I'll review and provide more specific guidance after. > Excellent. Perfect. The last thing we're missing is restoring the proper > focused window of the tab group. We should store that and make sure the > proper window is made key. If no windows were key, then we should make the > last one key. > Excellent. Any more cleanups or comments you'd recommend in the places you > changed? Notes on the last one: it gave me a bunch of suggestions, I rejected most but did accept some. > Can you write me a commit message summarizing the changes? It wrote me a part of the commit message you're reading now, but I always manually tweak the commit message and add my own flair. --- .../Terminal/TerminalController.swift | 140 ++++++++++++++---- 1 file changed, 112 insertions(+), 28 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7a241d866..fc262686b 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -647,41 +647,125 @@ class TerminalController: BaseTerminalController { private func closeWindowImmediately() { guard let window = window else { return } - // Regardless of tabs vs no tabs, what we want to do here is keep - // track of the window frame to restore, the surface tree, and the - // the focused surface. We want to restore that with undo even - // if we end up closing. - if let undoManager, let undoState { - // Register undo action to restore the window - undoManager.setActionName("Close Window") - undoManager.registerUndo( - withTarget: ghostty, - expiresAfter: undoExpiration) { ghostty in - // Restore the undo state - let newController = TerminalController(ghostty, with: undoState) + // Register undo for this close operation + registerUndoForCloseWindow() - // Register redo action + // Close the window(s) + if let tabGroup = window.tabGroup, tabGroup.windows.count > 1 { + tabGroup.windows.forEach { $0.close() } + } else { + window.close() + } + } + + /// Registers undo for closing window(s), handling both single windows and tab groups. + private func registerUndoForCloseWindow() { + guard let undoManager else { return } + guard let window else { return } + + // If we don't have a tab group or we don't have multiple tabs, then + // do a normal single window close. + guard let tabGroup = window.tabGroup, + tabGroup.windows.count > 1 else { + // No tabs, just save this window's state + if let undoState { + // Register undo action to restore the window + undoManager.setActionName("Close Window") undoManager.registerUndo( - withTarget: newController, - expiresAfter: newController.undoExpiration) { target in - target.closeWindowImmediately() + withTarget: ghostty, + expiresAfter: undoExpiration) { ghostty in + // Restore the undo state + let newController = TerminalController(ghostty, with: undoState) + + // Register redo action + undoManager.registerUndo( + withTarget: newController, + expiresAfter: newController.undoExpiration) { target in + target.closeWindowImmediately() + } + } + } + + return + } + + // Multiple windows in tab group - collect all undo states in sorted order + // by tab ordering. Also track which window was key. + let undoStates = tabGroup.windows + .compactMap { tabWindow -> UndoState? in + guard let controller = tabWindow.windowController as? TerminalController, + var undoState = controller.undoState else { return nil } + // Clear the tab group reference since it is unneeded. It should be + // garbage collected but we want to be extra sure we don't try to + // restore into it because we're going to recreate it. + undoState.tabGroup = nil + return undoState + } + .sorted { (lhs, rhs) in + switch (lhs.tabIndex, rhs.tabIndex) { + case let (l?, r?): return l < r + case (_?, nil): return true + case (nil, _?): return false + case (nil, nil): return true } } + + // Find the index of the key window in our sorted states. This is a bit verbose + // but we only need this for this style of undo so we don't want to add it to + // UndoState. + let keyWindowIndex: Int? + if let keyWindow = tabGroup.windows.first(where: { $0.isKeyWindow }), + let keyController = keyWindow.windowController as? TerminalController, + let keyUndoState = keyController.undoState { + keyWindowIndex = undoStates.firstIndex { + $0.tabIndex == keyUndoState.tabIndex } + } else { + keyWindowIndex = nil } - guard let tabGroup = window.tabGroup else { - // No tabs, no tab group, just perform a normal close. - window.close() - return - } + // Register undo action to restore all windows + guard !undoStates.isEmpty else { return } - // If have one window then we just do a normal close - if tabGroup.windows.count == 1 { - window.close() - return - } + undoManager.setActionName("Close Window") + undoManager.registerUndo( + withTarget: ghostty, + expiresAfter: undoExpiration + ) { ghostty in + // Restore all windows in the tab group + let controllers = undoStates.map { undoState in + TerminalController(ghostty, with: undoState) + } + + // The first controller becomes the parent window for all tabs. + // If we don't have a first controller (shouldn't be possible?) + // then we can't restore tabs. + guard let firstController = controllers.first else { return } + + // Add all subsequent controllers as tabs to the first window + for controller in controllers.dropFirst() { + controller.showWindow(nil) + if let firstWindow = firstController.window, + let newWindow = controller.window { + firstWindow.addTabbedWindow(newWindow, ordered: .above) + } + } + + // Make the appropriate window key. If we had a key window, restore it. + // Otherwise, make the last window key. + if let keyWindowIndex, keyWindowIndex < controllers.count { + controllers[keyWindowIndex].window?.makeKeyAndOrderFront(nil) + } else { + controllers.last?.window?.makeKeyAndOrderFront(nil) + } - tabGroup.windows.forEach { $0.close() } + // Register redo action on the first controller + undoManager.registerUndo( + withTarget: firstController, + expiresAfter: firstController.undoExpiration + ) { target in + target.closeWindowImmediately() + } + } } /// Close all windows, asking for confirmation if necessary. @@ -734,7 +818,7 @@ class TerminalController: BaseTerminalController { let surfaceTree: SplitTree let focusedSurface: UUID? let tabIndex: Int? - private(set) weak var tabGroup: NSWindowTabGroup? + weak var tabGroup: NSWindowTabGroup? } convenience init(_ ghostty: Ghostty.App, From 26e1dd8f8e876bfc0b797c5968becf7fd565c319 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Jun 2025 12:23:08 -0700 Subject: [PATCH 429/642] macos: clear out the surface trees to prevent repeat undo see the comment --- .../Features/Terminal/TerminalController.swift | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index fc262686b..c9f8ef216 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -647,12 +647,19 @@ class TerminalController: BaseTerminalController { private func closeWindowImmediately() { guard let window = window else { return } - // Register undo for this close operation registerUndoForCloseWindow() - // Close the window(s) if let tabGroup = window.tabGroup, tabGroup.windows.count > 1 { - tabGroup.windows.forEach { $0.close() } + tabGroup.windows.forEach { window in + // Clear out the surfacetree to ensure there is no undo state. + // This prevents unnecessary undos registered since AppKit may + // process them on later ticks so we can't just disable undo registration. + if let controller = window.windowController as? TerminalController { + controller.surfaceTree = .init() + } + + window.close() + } } else { window.close() } @@ -660,7 +667,7 @@ class TerminalController: BaseTerminalController { /// Registers undo for closing window(s), handling both single windows and tab groups. private func registerUndoForCloseWindow() { - guard let undoManager else { return } + guard let undoManager, undoManager.isUndoRegistrationEnabled else { return } guard let window else { return } // If we don't have a tab group or we don't have multiple tabs, then @@ -859,6 +866,7 @@ class TerminalController: BaseTerminalController { /// The current undo state for this controller var undoState: UndoState? { guard let window else { return nil } + guard !surfaceTree.isEmpty else { return nil } return .init( frame: window.frame, surfaceTree: surfaceTree, From e4cd90b8a0cee2b704a8466e8f9b915c2ef30514 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 8 Jun 2025 19:57:38 -0700 Subject: [PATCH 430/642] macos: set explicit identity for split tree view based on structure Fixes #7546 SwiftUI uses type and structure to identify views, which can lead to issues with tree like structures where the shape and type is the same but the content changes. This was causing #7546. To fix this, we need to add explicit identity to the split tree view so that SwiftUI can differentiate when it needs to redraw the view. We don't want to blindly add Hashable to SplitTree because we don't want to take into account all the fields. Instead, we add an explicit "structural identity" to the SplitTreeView that can be used by SwiftUI. --- macos/Sources/Features/Splits/SplitTree.swift | 145 ++++++++++++++++++ .../Splits/TerminalSplitTreeView.swift | 5 + 2 files changed, 150 insertions(+) diff --git a/macos/Sources/Features/Splits/SplitTree.swift b/macos/Sources/Features/Splits/SplitTree.swift index 394cd1089..1c4be7dd6 100644 --- a/macos/Sources/Features/Splits/SplitTree.swift +++ b/macos/Sources/Features/Splits/SplitTree.swift @@ -1116,3 +1116,148 @@ extension SplitTree: Collection { return i + 1 } } + +// MARK: Structural Identity + +extension SplitTree.Node { + /// Returns a hashable representation that captures this node's structural identity. + var structuralIdentity: StructuralIdentity { + StructuralIdentity(self) + } + + /// A hashable representation of a node that captures its structural identity. + /// + /// This type provides a way to track changes to a node's structure in SwiftUI + /// by implementing `Hashable` based on: + /// - The node's hierarchical structure (splits and their directions) + /// - The identity of view instances in leaf nodes (using object identity) + /// - The split directions (but not ratios, as those may change slightly) + /// + /// This is useful for SwiftUI's `id()` modifier to detect when a node's structure + /// has changed, triggering appropriate view updates while preserving view identity + /// for unchanged portions of the tree. + struct StructuralIdentity: Hashable { + private let node: SplitTree.Node + + init(_ node: SplitTree.Node) { + self.node = node + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.node.isStructurallyEqual(to: rhs.node) + } + + func hash(into hasher: inout Hasher) { + node.hashStructure(into: &hasher) + } + } + + /// Checks if this node is structurally equal to another node. + /// Two nodes are structurally equal if they have the same tree structure + /// and the same views (by identity) in the same positions. + fileprivate func isStructurallyEqual(to other: Node) -> Bool { + switch (self, other) { + case let (.leaf(view1), .leaf(view2)): + // Views must be the same instance + return view1 === view2 + + case let (.split(split1), .split(split2)): + // Splits must have same direction and structurally equal children + // Note: We intentionally don't compare ratios as they may change slightly + return split1.direction == split2.direction && + split1.left.isStructurallyEqual(to: split2.left) && + split1.right.isStructurallyEqual(to: split2.right) + + default: + // Different node types + return false + } + } + + /// Hash keys for structural identity + private enum HashKey: UInt8 { + case leaf = 0 + case split = 1 + } + + /// Hashes the structural identity of this node. + /// Includes the tree structure and view identities in the hash. + fileprivate func hashStructure(into hasher: inout Hasher) { + switch self { + case .leaf(let view): + hasher.combine(HashKey.leaf) + hasher.combine(ObjectIdentifier(view)) + + case .split(let split): + hasher.combine(HashKey.split) + hasher.combine(split.direction) + // Note: We intentionally don't hash the ratio + split.left.hashStructure(into: &hasher) + split.right.hashStructure(into: &hasher) + } + } +} + +extension SplitTree { + /// Returns a hashable representation that captures this tree's structural identity. + var structuralIdentity: StructuralIdentity { + StructuralIdentity(self) + } + + /// A hashable representation of a SplitTree that captures its structural identity. + /// + /// This type provides a way to track changes to a SplitTree's structure in SwiftUI + /// by implementing `Hashable` based on: + /// - The tree's hierarchical structure (splits and their directions) + /// - The identity of view instances in leaf nodes (using object identity) + /// - The zoomed node state (if any) + /// + /// This is useful for SwiftUI's `id()` modifier to detect when a tree's structure + /// has changed, triggering appropriate view updates while preserving view identity + /// for unchanged portions of the tree. + /// + /// Example usage: + /// ```swift + /// var body: some View { + /// SplitTreeView(tree: splitTree) + /// .id(splitTree.structuralIdentity) + /// } + /// ``` + struct StructuralIdentity: Hashable { + private let root: Node? + private let zoomed: Node? + + init(_ tree: SplitTree) { + self.root = tree.root + self.zoomed = tree.zoomed + } + + static func == (lhs: Self, rhs: Self) -> Bool { + areNodesStructurallyEqual(lhs.root, rhs.root) && + areNodesStructurallyEqual(lhs.zoomed, rhs.zoomed) + } + + func hash(into hasher: inout Hasher) { + hasher.combine(0) // Tree marker + if let root = root { + root.hashStructure(into: &hasher) + } + hasher.combine(1) // Zoomed marker + if let zoomed = zoomed { + zoomed.hashStructure(into: &hasher) + } + } + + /// Helper to compare optional nodes for structural equality + private static func areNodesStructurallyEqual(_ lhs: Node?, _ rhs: Node?) -> Bool { + switch (lhs, rhs) { + case (nil, nil): + return true + case let (node1?, node2?): + return node1.isStructurallyEqual(to: node2) + default: + return false + } + } + } +} diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index b219e0b31..2810fc2b4 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -10,6 +10,11 @@ struct TerminalSplitTreeView: View { node: node, isRoot: node == tree.root, onResize: onResize) + // This is necessary because we can't rely on SwiftUI's implicit + // structural identity to detect changes to this view. Due to + // the tree structure of splits it could result in bad beaviors. + // See: https://github.com/ghostty-org/ghostty/issues/7546 + .id(node.structuralIdentity) } } } From a87c68d49aa1f3a08c8173dbd7744e68f8af4d30 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Jun 2025 06:51:14 -0700 Subject: [PATCH 431/642] termio: unconditionally show "process exited" message We previously only showed this message if the user had `wait-after-command` set to true, since if its false the surface would close anyways. With the latest undo feature on macOS, this is no longer the case; a exited process can be undone and reopened. I considered disallowing undoing an exited surface, but I think there is value in being able to go back and recapture output in scrollback if you wanted to. --- src/termio/Exec.zig | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 23c626879..317ad13b4 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -418,25 +418,27 @@ fn processExitCommon(td: *termio.Termio.ThreadData, exit_code: u32) void { return; } + // We output a message so that the user knows whats going on and + // doesn't think their terminal just froze. We show this unconditionally + // on close even if `wait_after_command` is false and the surface closes + // immediately because if a user does an `undo` to restore a closed + // surface then they will see this message and know the process has + // completed. + terminal: { + td.renderer_state.mutex.lock(); + defer td.renderer_state.mutex.unlock(); + const t = td.renderer_state.terminal; + t.carriageReturn(); + t.linefeed() catch break :terminal; + t.printString("Process exited. Press any key to close the terminal.") catch + break :terminal; + t.modes.set(.cursor_visible, false); + } + // If we're purposely waiting then we just return since the process // exited flag is set to true. This allows the terminal window to remain // open. - if (execdata.wait_after_command) { - // We output a message so that the user knows whats going on and - // doesn't think their terminal just froze. - terminal: { - td.renderer_state.mutex.lock(); - defer td.renderer_state.mutex.unlock(); - const t = td.renderer_state.terminal; - t.carriageReturn(); - t.linefeed() catch break :terminal; - t.printString("Process exited. Press any key to close the terminal.") catch - break :terminal; - t.modes.set(.cursor_visible, false); - } - - return; - } + if (execdata.wait_after_command) return; // Notify our surface we want to close _ = td.surface_mailbox.push(.{ From 59bc980250e7b448a8e8693484a9fb5d625fc198 Mon Sep 17 00:00:00 2001 From: Alex Straight Date: Sun, 8 Jun 2025 23:22:04 -0700 Subject: [PATCH 432/642] feat: implement mode 1048 for saving/restoring cursor position --- src/terminal/modes.zig | 1 + src/termio/stream_handler.zig | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/terminal/modes.zig b/src/terminal/modes.zig index b36266b32..9a74db73c 100644 --- a/src/terminal/modes.zig +++ b/src/terminal/modes.zig @@ -223,6 +223,7 @@ const entries: []const ModeEntry = &.{ .{ .name = "alt_sends_escape", .value = 1039 }, .{ .name = "reverse_wrap_extended", .value = 1045 }, .{ .name = "alt_screen", .value = 1047 }, + .{ .name = "save_cursor", .value = 1048 }, .{ .name = "alt_screen_save_cursor_clear_enter", .value = 1049 }, .{ .name = "bracketed_paste", .value = 2004 }, .{ .name = "synchronized_output", .value = 2026 }, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b3aa82d20..2069a8ff2 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -597,6 +597,18 @@ pub const StreamHandler = struct { try self.queueRender(); }, + // Mode 1048 is xterm's conditional save cursor depending + // on if alt screen is enabled or not (at the terminal emulator + // level). Alt screen is always enabled for us so this just + // does a save/restore cursor. + .save_cursor => { + if (enabled) { + self.terminal.saveCursor(); + } else { + try self.terminal.restoreCursor(); + } + }, + // Force resize back to the window size .enable_mode_3 => { const grid_size = self.size.grid(); From b0e0aadaf3583d59574e69a3f8199d48ad967591 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Jun 2025 20:44:23 -0700 Subject: [PATCH 433/642] build: Xcode 26, macOS Tahoe support (build tooling only) This updates our build script and CI to support Xcode 26 and macOS Tahoe. **This doesn't update the Ghostty app to resolve any Tahoe issues.** For CI, we've added a new build job that runs on macOS Tahoe with Xcode 26. I've stopped short of updating our tip release job, since I think I want to wait until I verify a bit more about Tahoe before we flip that bit. Also, ideally, we'd run Xcode 26 on Sequoia (macOS 15) for stability reasons and Namespace doesn't have Xcode 26 on 15 yet. For builds, this updates our build script to find Metal binaries using `xcodebuild -find-executable` instead of `xcrun`. The latter doesn't work with Xcode 26, but the former does and also still works with older Xcodes. I'm not sure if this is a bug but I did report it: FB17874042. --- .github/workflows/test.yml | 60 +++++++++++++++++++++++++++++++++++--- src/build/MetallibStep.zig | 35 +++++++++++++++++----- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0b0ded6b..8a98584a2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: - build-nix - build-snap - build-macos + - build-macos-tahoe - build-macos-matrix - build-windows - build-windows-cross @@ -284,7 +285,7 @@ jobs: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: XCode Select + - name: Xcode Select run: sudo xcode-select -s /Applications/Xcode_16.0.app - name: get the Zig deps @@ -296,7 +297,58 @@ jobs: - name: Build GhosttyKit run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} - # The native app is built with native XCode tooling. This also does + # The native app is built with native Xcode tooling. This also does + # codesigning. IMPORTANT: this must NOT run in a Nix environment. + # Nix breaks xcodebuild so this has to be run outside. + - name: Build Ghostty.app + run: cd macos && xcodebuild -target Ghostty + + # Build the iOS target without code signing just to verify it works. + - name: Build Ghostty iOS + run: | + cd macos + xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" + + build-macos-tahoe: + runs-on: namespace-profile-ghostty-macos-tahoe + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # TODO(tahoe): https://github.com/NixOS/nix/issues/13342 + - uses: DeterminateSystems/nix-installer-action@main + with: + determinate: true + - uses: cachix/cachix-action@v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.0.app + + # TODO(tahoe): + # https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes#Interface-Builder + # We allow this step to fail because if our image already has + # the workaround in place this will fail. + - name: Xcode 26 Beta 17A5241e Metal Workaround + continue-on-error: true + run: | + xcodebuild -downloadComponent metalToolchain -exportPath /tmp/MyMetalExport/ + sed -i '' -e 's/17A5241c/17A5241e/g' /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle/ExportMetadata.plist + xcodebuild -importComponent metalToolchain -importPath /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle + + - name: get the Zig deps + id: deps + run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT + + # GhosttyKit is the framework that is built from Zig for our native + # Mac app to access. + - name: Build GhosttyKit + run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} + + # The native app is built with native Xcode tooling. This also does # codesigning. IMPORTANT: this must NOT run in a Nix environment. # Nix breaks xcodebuild so this has to be run outside. - name: Build Ghostty.app @@ -324,7 +376,7 @@ jobs: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: XCode Select + - name: Xcode Select run: sudo xcode-select -s /Applications/Xcode_16.0.app - name: get the Zig deps @@ -642,7 +694,7 @@ jobs: name: ghostty authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - name: XCode Select + - name: Xcode Select run: sudo xcode-select -s /Applications/Xcode_16.0.app - name: get the Zig deps diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index 12adf3edb..bac3a72c5 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -22,13 +22,12 @@ step: *Step, output: LazyPath, pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { - const self = b.allocator.create(MetallibStep) catch @panic("OOM"); + switch (opts.target.result.os.tag) { + .macos, .ios => {}, + else => return null, // Only macOS and iOS are supported. + } - const sdk = switch (opts.target.result.os.tag) { - .macos => "macosx", - .ios => "iphoneos", - else => return null, - }; + const self = b.allocator.create(MetallibStep) catch @panic("OOM"); const min_version = if (opts.target.query.os_version_min) |v| b.fmt("{}", .{v.semver}) @@ -38,11 +37,31 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { else => unreachable, }; + // Find the metal and metallib executables. The Apple docs + // at the time of writing (June 2025) say to use + // `xcrun --sdk metal` but this doesn't work with Xcode 26. + // + // I don't know if this is a bug but the xcodebuild approach also + // works with Xcode 15 so it seems safe to use this instead. + // + // Reported bug: FB17874042. + var code: u8 = undefined; + const metal_exe = std.mem.trim(u8, b.runAllowFail( + &.{ "xcodebuild", "-find-executable", "metal" }, + &code, + .Ignore, + ) catch return null, "\r\n "); + const metallib_exe = std.mem.trim(u8, b.runAllowFail( + &.{ "xcodebuild", "-find-executable", "metallib" }, + &code, + .Ignore, + ) catch return null, "\r\n "); + const run_ir = RunStep.create( b, b.fmt("metal {s}", .{opts.name}), ); - run_ir.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metal", "-o" }); + run_ir.addArgs(&.{ metal_exe, "-o" }); const output_ir = run_ir.addOutputFileArg(b.fmt("{s}.ir", .{opts.name})); run_ir.addArgs(&.{"-c"}); for (opts.sources) |source| run_ir.addFileArg(source); @@ -62,7 +81,7 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { b, b.fmt("metallib {s}", .{opts.name}), ); - run_lib.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metallib", "-o" }); + run_lib.addArgs(&.{ metallib_exe, "-o" }); const output_lib = run_lib.addOutputFileArg(b.fmt("{s}.metallib", .{opts.name})); run_lib.addFileArg(output_ir); run_lib.step.dependOn(&run_ir.step); From 3d692e46f435ec5488e9570d1fc65b8778437480 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 10 Jun 2025 10:20:26 -0600 Subject: [PATCH 434/642] license: update copyright notices to include contributors Updates all copyright notices to include "Ghostty contributors" to reflect the fact that Mitchell is not the sole copyright owner. Also adds "Ghostty contributors" to the author section in the manpages, linking https://github.com/ghostty-org/ghostty/graphs/contributors for proper credit. --- LICENSE | 2 +- pkg/README.md | 2 +- pkg/glfw/LICENSE | 2 +- po/ca_ES.UTF-8.po | 2 +- po/com.mitchellh.ghostty.pot | 2 +- po/de_DE.UTF-8.po | 2 +- po/es_BO.UTF-8.po | 2 +- po/fr_FR.UTF-8.po | 2 +- po/id_ID.UTF-8.po | 2 +- po/ja_JP.UTF-8.po | 2 +- po/mk_MK.UTF-8.po | 2 +- po/nb_NO.UTF-8.po | 2 +- po/nl_NL.UTF-8.po | 2 +- po/pl_PL.UTF-8.po | 2 +- po/pt_BR.UTF-8.po | 2 +- po/ru_RU.UTF-8.po | 2 +- po/tr_TR.UTF-8.po | 2 +- po/uk_UA.UTF-8.po | 2 +- po/zh_CN.UTF-8.po | 2 +- src/build/GhosttyI18n.zig | 2 +- src/build/mdgen/ghostty_1_footer.md | 1 + src/build/mdgen/ghostty_5_footer.md | 1 + 22 files changed, 22 insertions(+), 20 deletions(-) diff --git a/LICENSE b/LICENSE index 14e132f55..0a07a66cd 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Mitchell Hashimoto +Copyright (c) 2024 Mitchell Hashimoto, Ghostty contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/pkg/README.md b/pkg/README.md index 1d6f9f6eb..fddc4b3db 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -12,7 +12,7 @@ paste them into your project. the Ghostty project. This license does not apply to the rest of the Ghostty project.** -Copyright © 2024 Mitchell Hashimoto +Copyright © 2024 Mitchell Hashimoto, Ghostty contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in diff --git a/pkg/glfw/LICENSE b/pkg/glfw/LICENSE index eeeb852fe..8c422bd23 100644 --- a/pkg/glfw/LICENSE +++ b/pkg/glfw/LICENSE @@ -1,5 +1,5 @@ Copyright (c) 2021 Hexops Contributors (given via the Git commit history). -Copyright (c) 2025 Mitchell Hashimoto +Copyright (c) 2025 Mitchell Hashimoto, Ghostty contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/po/ca_ES.UTF-8.po b/po/ca_ES.UTF-8.po index 712f0d5af..653439fa2 100644 --- a/po/ca_ES.UTF-8.po +++ b/po/ca_ES.UTF-8.po @@ -1,5 +1,5 @@ # Catalan translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Francesc Arpi , 2025. # diff --git a/po/com.mitchellh.ghostty.pot b/po/com.mitchellh.ghostty.pot index d6a99d01d..da0efbbee 100644 --- a/po/com.mitchellh.ghostty.pot +++ b/po/com.mitchellh.ghostty.pot @@ -1,5 +1,5 @@ # SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR Mitchell Hashimoto +# Copyright (C) YEAR Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # FIRST AUTHOR , YEAR. # diff --git a/po/de_DE.UTF-8.po b/po/de_DE.UTF-8.po index 44f3bae39..2d3b96d81 100644 --- a/po/de_DE.UTF-8.po +++ b/po/de_DE.UTF-8.po @@ -1,6 +1,6 @@ # German translations for com.mitchellh.ghostty package # German translation for com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Robin Pfäffle , 2025. # diff --git a/po/es_BO.UTF-8.po b/po/es_BO.UTF-8.po index f3a62748a..077b7dfa1 100644 --- a/po/es_BO.UTF-8.po +++ b/po/es_BO.UTF-8.po @@ -1,5 +1,5 @@ # Spanish translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Miguel Peredo , 2025. # diff --git a/po/fr_FR.UTF-8.po b/po/fr_FR.UTF-8.po index 4db72a23e..aef0d96ac 100644 --- a/po/fr_FR.UTF-8.po +++ b/po/fr_FR.UTF-8.po @@ -1,5 +1,5 @@ # French translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Kirwiisp , 2025. # diff --git a/po/id_ID.UTF-8.po b/po/id_ID.UTF-8.po index d5204d420..f82ec6197 100644 --- a/po/id_ID.UTF-8.po +++ b/po/id_ID.UTF-8.po @@ -1,5 +1,5 @@ # Indonesian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Satrio Bayu Aji , 2025. # diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index e6e015f8a..73ddd9f5a 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -1,6 +1,6 @@ # Japanese translations for com.mitchellh.ghostty package # com.mitchellh.ghostty パッケージに対する和訳. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Lon Sagisawa , 2025. # diff --git a/po/mk_MK.UTF-8.po b/po/mk_MK.UTF-8.po index 39bb72b91..20a43572e 100644 --- a/po/mk_MK.UTF-8.po +++ b/po/mk_MK.UTF-8.po @@ -1,5 +1,5 @@ # Macedonian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Andrej Daskalov , 2025. # diff --git a/po/nb_NO.UTF-8.po b/po/nb_NO.UTF-8.po index 2685d67bb..045d47a80 100644 --- a/po/nb_NO.UTF-8.po +++ b/po/nb_NO.UTF-8.po @@ -1,5 +1,5 @@ # Norwegian Bokmal translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Hanna Rose , 2025. # Uzair Aftab , 2025. diff --git a/po/nl_NL.UTF-8.po b/po/nl_NL.UTF-8.po index 466116352..355bc4a57 100644 --- a/po/nl_NL.UTF-8.po +++ b/po/nl_NL.UTF-8.po @@ -1,5 +1,5 @@ # Dutch translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Nico Geesink , 2025. # diff --git a/po/pl_PL.UTF-8.po b/po/pl_PL.UTF-8.po index 22d2cd975..a68d56818 100644 --- a/po/pl_PL.UTF-8.po +++ b/po/pl_PL.UTF-8.po @@ -1,6 +1,6 @@ # Polish translations for com.mitchellh.ghostty package # Polskie tłumaczenia dla pakietu com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Bartosz Sokorski , 2025. # diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index f6d2f26a2..d2ba0e693 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -1,6 +1,6 @@ # Portuguese translations for com.mitchellh.ghostty package # Traduções em português brasileiro para o pacote com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Gustavo Peres , 2025. # diff --git a/po/ru_RU.UTF-8.po b/po/ru_RU.UTF-8.po index 9e9cf8077..0cb533de7 100644 --- a/po/ru_RU.UTF-8.po +++ b/po/ru_RU.UTF-8.po @@ -1,6 +1,6 @@ # Russian translations for com.mitchellh.ghostty package # Русские переводы для пакета com.mitchellh.ghostty. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # blackzeshi , 2025. # diff --git a/po/tr_TR.UTF-8.po b/po/tr_TR.UTF-8.po index 3de70d61c..5d761f6a4 100644 --- a/po/tr_TR.UTF-8.po +++ b/po/tr_TR.UTF-8.po @@ -1,5 +1,5 @@ # Turkish translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Emir SARI , 2025. # diff --git a/po/uk_UA.UTF-8.po b/po/uk_UA.UTF-8.po index 5a264b537..bde975fc4 100644 --- a/po/uk_UA.UTF-8.po +++ b/po/uk_UA.UTF-8.po @@ -1,5 +1,5 @@ # Ukrainian translations for com.mitchellh.ghostty package. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Danylo Zalizchuk , 2025. # diff --git a/po/zh_CN.UTF-8.po b/po/zh_CN.UTF-8.po index ee2c51362..77be8a351 100644 --- a/po/zh_CN.UTF-8.po +++ b/po/zh_CN.UTF-8.po @@ -1,6 +1,6 @@ # Chinese translations for com.mitchellh.ghostty package # com.mitchellh.ghostty 软件包的简体中文翻译. -# Copyright (C) 2025 Mitchell Hashimoto +# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors # This file is distributed under the same license as the com.mitchellh.ghostty package. # Leah , 2025. # diff --git a/src/build/GhosttyI18n.zig b/src/build/GhosttyI18n.zig index daf523938..a1852bb96 100644 --- a/src/build/GhosttyI18n.zig +++ b/src/build/GhosttyI18n.zig @@ -54,7 +54,7 @@ fn createUpdateStep(b: *std.Build) !*std.Build.Step { "--keyword=C_:1c,2", "--package-name=" ++ domain, "--msgid-bugs-address=m@mitchellh.com", - "--copyright-holder=Mitchell Hashimoto", + "--copyright-holder=\"Mitchell Hashimoto, Ghostty contributors\"", "-o", "-", }); diff --git a/src/build/mdgen/ghostty_1_footer.md b/src/build/mdgen/ghostty_1_footer.md index 7ace64cd8..f8e502b45 100644 --- a/src/build/mdgen/ghostty_1_footer.md +++ b/src/build/mdgen/ghostty_1_footer.md @@ -44,6 +44,7 @@ See GitHub issues: # AUTHOR Mitchell Hashimoto +Ghostty contributors # SEE ALSO diff --git a/src/build/mdgen/ghostty_5_footer.md b/src/build/mdgen/ghostty_5_footer.md index c5077ab97..380d83a53 100644 --- a/src/build/mdgen/ghostty_5_footer.md +++ b/src/build/mdgen/ghostty_5_footer.md @@ -36,6 +36,7 @@ See GitHub issues: # AUTHOR Mitchell Hashimoto +Ghostty contributors # SEE ALSO From 12ad0fa4b68c0748fbbbbf3ba89ceecad3567d0c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 10 Jun 2025 12:11:59 -0600 Subject: [PATCH 435/642] font/sprite: add corner pieces from Geometric Shapes block MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ◢ ◣ ◤ ◥ ◸ ◹ ◺ ◿ --- src/font/sprite/Box.zig | 123 +++++++++++++++++++++++++++++++ src/font/sprite/Face.zig | 5 ++ src/font/sprite/testdata/Box.ppm | Bin 1048593 -> 1048593 bytes 3 files changed, 128 insertions(+) diff --git a/src/font/sprite/Box.zig b/src/font/sprite/Box.zig index dd02f701b..f5140091d 100644 --- a/src/font/sprite/Box.zig +++ b/src/font/sprite/Box.zig @@ -581,6 +581,120 @@ fn draw(self: Box, alloc: Allocator, canvas: *font.sprite.Canvas, cp: u32) !void // '▟' 0x259f => self.draw_quadrant(canvas, .{ .tr = true, .bl = true, .br = true }), + // '◢' + 0x25e2 => self.draw_corner_triangle_shade(canvas, .br, .on), + // '◣' + 0x25e3 => self.draw_corner_triangle_shade(canvas, .bl, .on), + // '◤' + 0x25e4 => self.draw_corner_triangle_shade(canvas, .tl, .on), + // '◥' + 0x25e5 => self.draw_corner_triangle_shade(canvas, .tr, .on), + + // '◸' + 0x25f8 => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // top edge + self.rect( + canvas, + 0, + 0, + self.metrics.cell_width, + thickness_px, + ); + // left edge + self.rect( + canvas, + 0, + 0, + thickness_px, + self.metrics.cell_height -| 1, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .lower_left, + .upper_right, + ); + }, + // '◹' + 0x25f9 => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // top edge + self.rect( + canvas, + 0, + 0, + self.metrics.cell_width, + thickness_px, + ); + // right edge + self.rect( + canvas, + self.metrics.cell_width -| thickness_px, + 0, + self.metrics.cell_width, + self.metrics.cell_height -| 1, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .upper_left, + .lower_right, + ); + }, + // '◺' + 0x25fa => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // bottom edge + self.rect( + canvas, + 0, + self.metrics.cell_height -| thickness_px, + self.metrics.cell_width, + self.metrics.cell_height, + ); + // left edge + self.rect( + canvas, + 0, + 1, + thickness_px, + self.metrics.cell_height, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .upper_left, + .lower_right, + ); + }, + // '◿' + 0x25ff => { + const thickness_px = Thickness.light.height(self.metrics.box_thickness); + // bottom edge + self.rect( + canvas, + 0, + self.metrics.cell_height -| thickness_px, + self.metrics.cell_width, + self.metrics.cell_height, + ); + // right edge + self.rect( + canvas, + self.metrics.cell_width -| thickness_px, + 1, + self.metrics.cell_width, + self.metrics.cell_height, + ); + // diagonal + self.draw_cell_diagonal( + canvas, + .lower_left, + .upper_right, + ); + }, + 0x2800...0x28ff => self.draw_braille(canvas, cp), 0x1fb00...0x1fb3b => self.draw_sextant(canvas, cp), @@ -3197,6 +3311,15 @@ fn testRenderAll(self: Box, alloc: Allocator, atlas: *font.Atlas) !void { else => {}, } } + + // Geometric Shapes: filled and outlined corners + for ([_]u21{ '◢', '◣', '◤', '◥', '◸', '◹', '◺', '◿' }) |char| { + _ = try self.renderGlyph( + alloc, + atlas, + char, + ); + } } test "render all sprites" { diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index f15423ada..af0c0af6a 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -190,6 +190,11 @@ const Kind = enum { // ▀ ▁ ▂ ▃ ▄ ▅ ▆ ▇ █ ▉ ▊ ▋ ▌ ▍ ▎ ▏ ▐ ░ ▒ ▓ ▔ ▕ ▖ ▗ ▘ ▙ ▚ ▛ ▜ ▝ ▞ ▟ 0x2580...0x259F, + // "Geometric Shapes" block + 0x25e2...0x25e5, // ◢◣◤◥ + 0x25f8...0x25fa, // ◸◹◺ + 0x25ff, // ◿ + // "Braille" block 0x2800...0x28FF, diff --git a/src/font/sprite/testdata/Box.ppm b/src/font/sprite/testdata/Box.ppm index d5a6cc72906318b235e33bc3ea53be8f88b67193..6082475af7d3e2264cf04061e9f63d7c6e6fdc9e 100644 GIT binary patch delta 12462 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 aaAk&8OZB|f2)~dj4w`aM6&XsT5I+Fj-I(40 From 2b9a6a482017b7bd3a1856e976ff8c2a3b13cecf Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 12:11:17 -0700 Subject: [PATCH 436/642] macos: unsplit window shouldn't allow split zooming This was always the case, and is a recent regression from the SplitTree rework. This brings it back to the previous behavior. --- macos/Sources/Features/Terminal/BaseTerminalController.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 594a58056..e91199358 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -556,12 +556,15 @@ class BaseTerminalController: NSWindowController, // The target must be within our tree guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let targetNode = surfaceTree.root?.node(view: target) else { return } - + // Toggle the zoomed state if surfaceTree.zoomed == targetNode { // Already zoomed, unzoom it surfaceTree = SplitTree(root: surfaceTree.root, zoomed: nil) } else { + // We require that the split tree have splits + guard surfaceTree.isSplit else { return } + // Not zoomed or different node zoomed, zoom this node surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode) } From 8b5cceed3ecb916f6a5be4d384dcd964402abc3c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 12:30:15 -0700 Subject: [PATCH 437/642] ci: pin gh-action-release to 2.2.2 to workaround issue https://github.com/softprops/action-gh-release/issues/628 --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 42626288c..b6a6c5f6c 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -132,7 +132,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -299,7 +299,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -507,7 +507,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -682,7 +682,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v2.2.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From 1f340b4b2dc8e2f9752bee8702a555e8bd367b51 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 12:39:09 -0700 Subject: [PATCH 438/642] macos: for windowShouldClose, only close the tab if we have multiple Fixes a regression from our undo/redo rework. We were accidentally closing the entire window when the "X" button in the tab bar was clicked. --- macos/Sources/Features/Terminal/TerminalController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index c9f8ef216..a984952f9 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1046,7 +1046,12 @@ class TerminalController: BaseTerminalController { //MARK: - NSWindowDelegate override func windowShouldClose(_ sender: NSWindow) -> Bool { - closeWindow(sender) + // If we have tabs, then this should only close the tab. + if window?.tabGroup?.windows.count ?? 0 > 1 { + closeTab(sender) + } else { + closeWindow(sender) + } // We will always explicitly close the window using the above return false From 3db5b3da752b07d3877ab9682f660c76320e82a6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 14:31:41 -0700 Subject: [PATCH 439/642] macos: hidden titlebar windows should cascade on new tab Windows with `macos-titlebar-style = hidden` create new windows when the new tab binding is pressed. This behavior has existed for a long time. However, these windows did not cascade, meaning they'd appear overlapped directly on top of the previous window, which is kind of nasty. This commit changes it so that new windows created via new tab from a hidden titlebar window will cascade. --- .../Terminal/TerminalController.swift | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a984952f9..5916c5921 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -278,11 +278,6 @@ class TerminalController: BaseTerminalController { tg.removeWindow(window) } - // Our windows start out invisible. We need to make it visible. If we - // don't do this then various features such as window blur won't work because - // the macOS APIs only work on a visible window. - controller.showWindow(self) - // If we have the "hidden" titlebar style we want to create new // tabs as windows instead, so just skip adding it to the parent. if (ghostty.config.macosTitlebarStyle != "hidden") { @@ -303,7 +298,19 @@ class TerminalController: BaseTerminalController { } } - window.makeKeyAndOrderFront(self) + // We're dispatching this async because otherwise the lastCascadePoint doesn't + // take effect. Our best theory is there is some next-event-loop-tick logic + // that Cocoa is doing that we need to be after. + DispatchQueue.main.async { + // Only cascade if we aren't fullscreen and are alone in the tab group. + if !window.styleMask.contains(.fullScreen) && + window.tabGroup?.windows.count ?? 1 == 1 { + Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) + } + + controller.showWindow(self) + window.makeKeyAndOrderFront(self) + } // It takes an event loop cycle until the macOS tabGroup state becomes // consistent which causes our tab labeling to be off when the "+" button From 990b6a6b0808f3eef1549bdeaf6b5413384141db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 11 Jun 2025 00:31:07 +0000 Subject: [PATCH 440/642] build(deps): bump softprops/action-gh-release from 2.2.2 to 2.3.2 Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2.2.2 to 2.3.2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v2.2.2...v2.3.2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-version: 2.3.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/release-tip.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index b6a6c5f6c..73a1ddeeb 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -132,7 +132,7 @@ jobs: nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password - name: Update Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -299,7 +299,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -507,7 +507,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true @@ -682,7 +682,7 @@ jobs: # Update Release - name: Release - uses: softprops/action-gh-release@v2.2.2 + uses: softprops/action-gh-release@v2.3.2 with: name: 'Ghostty Tip ("Nightly")' prerelease: true From 31e386afa6e4874be266ea083c773ffac9e6168e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Tue, 10 Jun 2025 22:03:33 -0400 Subject: [PATCH 441/642] use else if instead of else { if } --- src/os/hostname.zig | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index 3f2c53b50..ddcdeb59e 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -32,10 +32,8 @@ fn isValidMacAddress(mac_address: []const u8) bool { if (c != ':') { return false; } - } else { - if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { - return false; - } + } else if (!std.mem.containsAtLeastScalar(u8, "0123456789ABCDEFabcdef", 1, c)) { + return false; } } From 4d33a73fc4640b19cb6160942c5bdcd2a8102fb9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 10 Jun 2025 13:03:30 -0700 Subject: [PATCH 442/642] wip: redo terminal window styling --- macos/Ghostty.xcodeproj/project.pbxproj | 42 ++++- .../Terminal/TerminalController.swift | 170 +++++------------- .../HiddenTitlebarTerminalWindow.swift | 78 ++++++++ .../LegacyTerminalWindow.swift} | 44 +---- .../Terminal/{ => Window Styles}/Terminal.xib | 8 +- .../Window Styles/TerminalHiddenTitlebar.xib | 31 ++++ .../Terminal/Window Styles/TerminalLegacy.xib | 31 ++++ .../TerminalTransparentTitlebar.xib | 31 ++++ .../Window Styles/TerminalWindow.swift | 75 ++++++++ .../TransparentTitlebarTerminalWindow.swift | 72 ++++++++ .../Helpers/Extensions/NSView+Extension.swift | 27 +++ macos/Sources/Helpers/Fullscreen.swift | 11 +- 12 files changed, 448 insertions(+), 172 deletions(-) create mode 100644 macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift rename macos/Sources/Features/Terminal/{TerminalWindow.swift => Window Styles/LegacyTerminalWindow.swift} (95%) rename macos/Sources/Features/Terminal/{ => Window Styles}/Terminal.xib (86%) create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift create mode 100644 macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 9686dcbd1..594579744 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; - A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */; }; + A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC212B2FB6B400E92F16 /* AboutView.swift */; }; @@ -51,6 +51,12 @@ A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; + A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; }; + A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; }; + A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; }; + A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */; }; + A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */; }; + A5593FE92DF927DF00B47B10 /* TerminalTransparentTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */; }; A56B880B2A840447007A0E29 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A56B880A2A840447007A0E29 /* Carbon.framework */; }; @@ -129,7 +135,7 @@ 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; - A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; + A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; A51BFC212B2FB6B400E92F16 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; @@ -159,6 +165,12 @@ A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = ""; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = ""; }; A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; + A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; + A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; }; + A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = ""; }; + A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalLegacy.xib; sourceTree = ""; }; + A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentTitlebarTerminalWindow.swift; sourceTree = ""; }; + A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTransparentTitlebar.xib; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; A55B7BBB29B6FC330055DE60 /* SurfaceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceView.swift; sourceTree = ""; }; A56B880A2A840447007A0E29 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; }; @@ -384,6 +396,21 @@ path = Sources; sourceTree = ""; }; + A5593FDD2DF8D56000B47B10 /* Window Styles */ = { + isa = PBXGroup; + children = ( + A59630992AEE1C6400D64628 /* Terminal.xib */, + A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, + A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, + A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, + A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, + A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, + A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, + A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, + ); + path = "Window Styles"; + sourceTree = ""; + }; A55B7BB429B6F4410055DE60 /* Ghostty */ = { isa = PBXGroup; children = ( @@ -467,11 +494,10 @@ A59630982AEE1C4400D64628 /* Terminal */ = { isa = PBXGroup; children = ( - A59630992AEE1C6400D64628 /* Terminal.xib */, + A5593FDD2DF8D56000B47B10 /* Window Styles */, A596309B2AEE1C9E00D64628 /* TerminalController.swift */, A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, - A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */, AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */, @@ -647,9 +673,11 @@ buildActionMask = 2147483647; files = ( FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */, + A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */, 29C15B1D2CDC3B2900520DD4 /* bat in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */, + A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */, A546F1142D7B68D7003B11A0 /* locale in Resources */, A5985CE62C33060F00C57AD3 /* man in Resources */, 9351BE8E3D22937F003B3499 /* nvim in Resources */, @@ -658,6 +686,7 @@ FC5218FA2D10FFCE004C93E0 /* zsh in Resources */, A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */, A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */, + A5593FE92DF927DF00B47B10 /* TerminalTransparentTitlebar.xib in Resources */, A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */, A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, @@ -702,6 +731,7 @@ A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, + A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */, A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */, A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, @@ -709,6 +739,7 @@ A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, + A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */, A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */, A59630972AEE163600D64628 /* HostingWindow.swift in Sources */, A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */, @@ -734,9 +765,10 @@ A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, - A51B78472AF4B58B00F3EDB9 /* TerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, + A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 5916c5921..7fb8f9a07 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -6,7 +6,22 @@ import GhosttyKit /// A classic, tabbed terminal experience. class TerminalController: BaseTerminalController { - override var windowNibName: NSNib.Name? { "Terminal" } + override var windowNibName: NSNib.Name? { + //NOTE(mitchellh): switch to this when we've transitioned all legacy logic out + //let defaultValue = "Terminal" + let defaultValue = "TerminalLegacy" + + guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } + let config = appDelegate.ghostty.config + let nib = switch config.macosTitlebarStyle { + case "tabs": defaultValue + case "hidden": "TerminalHiddenTitlebar" + case "transparent": "TerminalTransparentTitlebar" + default: defaultValue + } + + return nib + } /// This is set to true when we care about frame changes. This is a small optimization since /// this controller registers a listener for ALL frame change notifications and this lets us bail @@ -114,7 +129,7 @@ class TerminalController: BaseTerminalController { invalidateRestorableState() // Update our zoom state - if let window = window as? TerminalWindow { + if let window = window as? LegacyTerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } @@ -129,11 +144,6 @@ class TerminalController: BaseTerminalController { // When our fullscreen state changes, we resync our appearance because some // properties change when fullscreen or not. guard let focusedSurface else { return } - if (!(fullscreenStyle?.isFullscreen ?? false) && - ghostty.config.macosTitlebarStyle == "hidden") - { - applyHiddenTitlebarStyle() - } syncAppearance(focusedSurface.derivedConfig) } @@ -278,9 +288,8 @@ class TerminalController: BaseTerminalController { tg.removeWindow(window) } - // If we have the "hidden" titlebar style we want to create new - // tabs as windows instead, so just skip adding it to the parent. - if (ghostty.config.macosTitlebarStyle != "hidden") { + // If we don't allow tabs then we create a new window instead. + if (window.tabbingMode != .disallowed) { // Add the window to the tab group and show it. switch ghostty.config.windowNewTabPosition { case "end": @@ -389,7 +398,7 @@ class TerminalController: BaseTerminalController { // Reset this to false. It'll be set back to true later. tabListenForFrame = false - guard let windows = self.window?.tabbedWindows as? [TerminalWindow] else { return } + guard let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] else { return } // We only listen for frame changes if we have more than 1 window, // otherwise the accessory view doesn't matter. @@ -440,7 +449,11 @@ class TerminalController: BaseTerminalController { } private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { - guard let window = self.window as? TerminalWindow else { return } + if let window = window as? TerminalWindow { + window.syncAppearance(surfaceConfig) + } + + guard let window = self.window as? LegacyTerminalWindow else { return } // Set our explicit appearance if we need to based on the configuration. window.appearance = surfaceConfig.windowAppearance @@ -523,31 +536,6 @@ class TerminalController: BaseTerminalController { } } - private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { - guard let window else { return } - - // If we don't have an X/Y then we try to use the previously saved window pos. - guard let x, let y else { - if (!LastWindowPosition.shared.restore(window)) { - window.center() - } - - return - } - - // Prefer the screen our window is being placed on otherwise our primary screen. - guard let screen = window.screen ?? NSScreen.screens.first else { - window.center() - return - } - - // Orient based on the top left of the primary monitor - let frame = screen.visibleFrame - window.setFrameOrigin(.init( - x: frame.minX + CGFloat(x), - y: frame.maxY - (CGFloat(y) + window.frame.height))) - } - /// Returns the default size of the window. This is contextual based on the focused surface because /// the focused surface may specify a different default size than others. private var defaultSize: NSRect? { @@ -889,52 +877,9 @@ class TerminalController: BaseTerminalController { shouldCascadeWindows = false } - fileprivate func hideWindowButtons() { - guard let window else { return } - - window.standardWindowButton(.closeButton)?.isHidden = true - window.standardWindowButton(.miniaturizeButton)?.isHidden = true - window.standardWindowButton(.zoomButton)?.isHidden = true - } - - fileprivate func applyHiddenTitlebarStyle() { - guard let window else { return } - - window.styleMask = [ - // We need `titled` in the mask to get the normal window frame - .titled, - - // Full size content view so we can extend - // content in to the hidden titlebar's area - .fullSizeContentView, - - .resizable, - .closable, - .miniaturizable, - ] - - // Hide the title - window.titleVisibility = .hidden - window.titlebarAppearsTransparent = true - - // Hide the traffic lights (window control buttons) - hideWindowButtons() - - // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. - window.tabbingMode = .disallowed - - // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are - // some operations that appear to bring back the titlebar visibility so this ensures - // it is gone forever. - if let themeFrame = window.contentView?.superview, - let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") { - titleBarContainer.isHidden = true - } - } - override func windowDidLoad() { super.windowDidLoad() - guard let window = window as? TerminalWindow else { return } + guard let window else { return } // Store our initial frame so we can know our default later. initialFrame = window.frame @@ -952,9 +897,6 @@ class TerminalController: BaseTerminalController { window.identifier = .init(String(describing: TerminalWindowRestoration.self)) } - // If window decorations are disabled, remove our title - if (!config.windowDecorations) { window.styleMask.remove(.titled) } - // If we have only a single surface (no splits) and there is a default size then // we should resize to that default size. if case let .leaf(view) = surfaceTree.root { @@ -967,42 +909,29 @@ class TerminalController: BaseTerminalController { } } - // Set our window positioning to coordinates if config value exists, otherwise - // fallback to original centering behavior - setInitialWindowPosition( - x: config.windowPositionX, - y: config.windowPositionY, - windowDecorations: config.windowDecorations) - - if config.macosWindowButtons == .hidden { - hideWindowButtons() - } - - // Make sure our theme is set on the window so styling is correct. - if let windowTheme = config.windowTheme { - window.windowTheme = .init(rawValue: windowTheme) - } - - // Handle titlebar tabs config option. Something about what we do while setting up the - // titlebar tabs interferes with the window restore process unless window.tabbingMode - // is set to .preferred, so we set it, and switch back to automatic as soon as we can. - if (config.macosTitlebarStyle == "tabs") { - window.tabbingMode = .preferred - window.titlebarTabs = true - DispatchQueue.main.async { - window.tabbingMode = .automatic + // TODO: remove + if let window = window as? LegacyTerminalWindow { + // Handle titlebar tabs config option. Something about what we do while setting up the + // titlebar tabs interferes with the window restore process unless window.tabbingMode + // is set to .preferred, so we set it, and switch back to automatic as soon as we can. + if (config.macosTitlebarStyle == "tabs") { + window.tabbingMode = .preferred + window.titlebarTabs = true + DispatchQueue.main.async { + window.tabbingMode = .automatic + } + } else if (config.macosTitlebarStyle == "transparent") { + window.transparentTabs = true } - } else if (config.macosTitlebarStyle == "transparent") { - window.transparentTabs = true - } - if window.hasStyledTabs { - // Set the background color of the window - let backgroundColor = NSColor(config.backgroundColor) - window.backgroundColor = backgroundColor + if window.hasStyledTabs { + // Set the background color of the window + let backgroundColor = NSColor(config.backgroundColor) + window.backgroundColor = backgroundColor - // This makes sure our titlebar renders correctly when there is a transparent background - window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + // This makes sure our titlebar renders correctly when there is a transparent background + window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + } } // Initialize our content view to the SwiftUI root @@ -1012,11 +941,6 @@ class TerminalController: BaseTerminalController { delegate: self )) - // If our titlebar style is "hidden" we adjust the style appropriately - if (config.macosTitlebarStyle == "hidden") { - applyHiddenTitlebarStyle() - } - // In various situations, macOS automatically tabs new windows. Ghostty handles // its own tabbing so we DONT want this behavior. This detects this scenario and undoes // it. @@ -1218,7 +1142,7 @@ class TerminalController: BaseTerminalController { override func titleDidChange(to: String) { super.titleDidChange(to: to) - guard let window = window as? TerminalWindow else { return } + guard let window = window as? LegacyTerminalWindow else { return } // Custom toolbar-based title used when titlebar tabs are enabled. if let toolbar = window.toolbar as? TerminalToolbar { diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift new file mode 100644 index 000000000..f2d3b9b85 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -0,0 +1,78 @@ +import AppKit + +class HiddenTitlebarTerminalWindow: TerminalWindow { + override func awakeFromNib() { + super.awakeFromNib() + + // Setup our initial style + reapplyHiddenStyle() + + // Notifications + NotificationCenter.default.addObserver( + self, + selector: #selector(fullscreenDidExit(_:)), + name: .fullscreenDidExit, + object: nil) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // We override this so that with the hidden titlebar style the titlebar + // area is not draggable. + override var contentLayoutRect: CGRect { + var rect = super.contentLayoutRect + rect.origin.y = 0 + rect.size.height = self.frame.height + return rect + } + + /// Apply the hidden titlebar style. + private func reapplyHiddenStyle() { + styleMask = [ + // We need `titled` in the mask to get the normal window frame + .titled, + + // Full size content view so we can extend + // content in to the hidden titlebar's area + .fullSizeContentView, + + .resizable, + .closable, + .miniaturizable, + ] + + // Hide the title + titleVisibility = .hidden + titlebarAppearsTransparent = true + + // Hide the traffic lights (window control buttons) + standardWindowButton(.closeButton)?.isHidden = true + standardWindowButton(.miniaturizeButton)?.isHidden = true + standardWindowButton(.zoomButton)?.isHidden = true + + // Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar. + tabbingMode = .disallowed + + // Nuke it from orbit -- hide the titlebar container entirely, just in case. There are + // some operations that appear to bring back the titlebar visibility so this ensures + // it is gone forever. + if let themeFrame = contentView?.superview, + let titleBarContainer = themeFrame.firstDescendant(withClassName: "NSTitlebarContainerView") { + titleBarContainer.isHidden = true + } + } + + // MARK: Notifications + + @objc private func fullscreenDidExit(_ notification: Notification) { + // Make sure they're talking about our window + guard let fullscreen = notification.object as? FullscreenBase else { return } + guard fullscreen.window == self else { return } + + // On exit we need to reapply the style because macOS breaks it usually. + // This is safe to call repeatedly so if its not broken its still safe. + reapplyHiddenStyle() + } +} diff --git a/macos/Sources/Features/Terminal/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift similarity index 95% rename from macos/Sources/Features/Terminal/TerminalWindow.swift rename to macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift index 0b43582f3..208e86343 100644 --- a/macos/Sources/Features/Terminal/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift @@ -1,9 +1,8 @@ import Cocoa -class TerminalWindow: NSWindow { - /// This is the key in UserDefaults to use for the default `level` value. - static let defaultLevelKey: String = "TerminalDefaultLevel" - +/// The terminal window that we originally had in Ghostty for a long time. Kind of a soupy mess +/// of styling. +class LegacyTerminalWindow: TerminalWindow { @objc dynamic var keyEquivalent: String = "" /// This is used to determine if certain elements should be drawn light or dark and should @@ -56,11 +55,6 @@ class TerminalWindow: NSWindow { } } - // Both of these must be true for windows without decorations to be able to - // still become key/main and receive events. - override var canBecomeKey: Bool { return true } - override var canBecomeMain: Bool { return true } - // MARK: - Lifecycle override func awakeFromNib() { @@ -77,8 +71,6 @@ class TerminalWindow: NSWindow { if titlebarTabs { generateToolbar() } - - level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } deinit { @@ -135,25 +127,6 @@ class TerminalWindow: NSWindow { } } - // We override this so that with the hidden titlebar style the titlebar - // area is not draggable. - override var contentLayoutRect: CGRect { - var rect = super.contentLayoutRect - - // If we are using a hidden titlebar style, the content layout is the - // full frame making it so that it is not draggable. - if let controller = windowController as? TerminalController, - controller.derivedConfig.macosTitlebarStyle == "hidden" { - rect.origin.y = 0 - rect.size.height = self.frame.height - } - return rect - } - - // The window theme configuration from Ghostty. This is used to control some - // behaviors that don't look quite right in certain situations. - var windowTheme: TerminalWindowTheme? - // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to // remove the effect view if the default tab bar is being used since the effect created in @@ -703,7 +676,7 @@ fileprivate class WindowDragView: NSView { fileprivate class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. - private weak var terminalWindow: TerminalWindow? + private weak var terminalWindow: LegacyTerminalWindow? private let isLightTheme: Bool private let overlayLayer = VibrantLayer() @@ -731,7 +704,7 @@ fileprivate class WindowButtonsBackdropView: NSView { fatalError("init(coder:) has not been implemented") } - init(window: TerminalWindow) { + init(window: LegacyTerminalWindow) { self.terminalWindow = window self.isLightTheme = window.isLightTheme @@ -746,10 +719,3 @@ fileprivate class WindowButtonsBackdropView: NSView { layer?.addSublayer(overlayLayer) } } - -enum TerminalWindowTheme: String { - case auto - case system - case light - case dark -} diff --git a/macos/Sources/Features/Terminal/Terminal.xib b/macos/Sources/Features/Terminal/Window Styles/Terminal.xib similarity index 86% rename from macos/Sources/Features/Terminal/Terminal.xib rename to macos/Sources/Features/Terminal/Window Styles/Terminal.xib index 65b03b6eb..cfbb2221c 100644 --- a/macos/Sources/Features/Terminal/Terminal.xib +++ b/macos/Sources/Features/Terminal/Window Styles/Terminal.xib @@ -1,8 +1,8 @@ - + - + @@ -17,10 +17,10 @@ - + - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib new file mode 100644 index 000000000..eb4675657 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib new file mode 100644 index 000000000..61ed6f782 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib new file mode 100644 index 000000000..ada6959b3 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift new file mode 100644 index 000000000..74744a962 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -0,0 +1,75 @@ +import AppKit + +/// The base class for all standalone, "normal" terminal windows. This sets the basic +/// style and configuration of the window based on the app configuration. +class TerminalWindow: NSWindow { + /// This is the key in UserDefaults to use for the default `level` value. This is + /// used by the manual float on top menu item feature. + static let defaultLevelKey: String = "TerminalDefaultLevel" + + // MARK: NSWindow Overrides + + override func awakeFromNib() { + guard let appDelegate = NSApp.delegate as? AppDelegate else { return } + + // All new windows are based on the app config at the time of creation. + let config = appDelegate.ghostty.config + + // If window decorations are disabled, remove our title + if (!config.windowDecorations) { styleMask.remove(.titled) } + + // Set our window positioning to coordinates if config value exists, otherwise + // fallback to original centering behavior + setInitialWindowPosition( + x: config.windowPositionX, + y: config.windowPositionY, + windowDecorations: config.windowDecorations) + + // If our traffic buttons should be hidden, then hide them + if config.macosWindowButtons == .hidden { + hideWindowButtons() + } + + // Get our saved level + level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal + } + + // Both of these must be true for windows without decorations to be able to + // still become key/main and receive events. + override var canBecomeKey: Bool { return true } + override var canBecomeMain: Bool { return true } + + // MARK: Positioning And Styling + + /// This is called by the controller when there is a need to reset the window apperance. + func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {} + + private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { + // If we don't have an X/Y then we try to use the previously saved window pos. + guard let x, let y else { + if (!LastWindowPosition.shared.restore(self)) { + center() + } + + return + } + + // Prefer the screen our window is being placed on otherwise our primary screen. + guard let screen = screen ?? NSScreen.screens.first else { + center() + return + } + + // Orient based on the top left of the primary monitor + let frame = screen.visibleFrame + setFrameOrigin(.init( + x: frame.minX + CGFloat(x), + y: frame.maxY - (CGFloat(y) + frame.height))) + } + + private func hideWindowButtons() { + standardWindowButton(.closeButton)?.isHidden = true + standardWindowButton(.miniaturizeButton)?.isHidden = true + standardWindowButton(.zoomButton)?.isHidden = true + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift new file mode 100644 index 000000000..4b3336874 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -0,0 +1,72 @@ +import AppKit + +class TransparentTitlebarTerminalWindow: TerminalWindow { + private var reapplyTimer: Timer? + + override func awakeFromNib() { + super.awakeFromNib() + } + + deinit { + reapplyTimer?.invalidate() + } + + override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + if #available(macOS 26.0, *) { + syncAppearanceTahoe(surfaceConfig) + } else { + syncAppearanceVentura(surfaceConfig) + } + } + + @available(macOS 26.0, *) + private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + guard let titlebarBackgroundView else { return } + titlebarBackgroundView.isHidden = true + backgroundColor = NSColor(surfaceConfig.backgroundColor) + } + + @available(macOS 13.0, *) + private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + guard let titlebarContainer else { return } + + let configBgColor = NSColor(surfaceConfig.backgroundColor) + + // Set our window background color so it shows up + backgroundColor = configBgColor + + // Set the background color of our titlebar to match + titlebarContainer.wantsLayer = true + titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor + } + + private var titlebarBackgroundView: NSView? { + titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView") + } + + private var titlebarContainer: NSView? { + // If we aren't fullscreen then the titlebar container is part of our window. + if !styleMask.contains(.fullScreen) { + return titlebarContainerView + } + + // If we are fullscreen, the titlebar container view is part of a separate + // "fullscreen window", we need to find the window and then get the view. + for window in NSApplication.shared.windows { + // This is the private window class that contains the toolbar + guard window.className == "NSToolbarFullScreenWindow" else { continue } + + // The parent will match our window. This is used to filter the correct + // fullscreen window if we have multiple. + guard window.parent == self else { continue } + + return titlebarContainerView + } + + return nil + } + + private var titlebarContainerView: NSView? { + contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") + } +} diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 48284df74..121d9a62a 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -13,6 +13,19 @@ extension NSView { return false } +} + +// MARK: View Traversal and Search + +extension NSView { + /// Returns the absolute root view by walking up the superview chain. + var rootView: NSView { + var root: NSView = self + while let superview = root.superview { + root = superview + } + return root + } /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { @@ -54,4 +67,18 @@ extension NSView { return nil } + + /// Finds and returns the first view with the given class name starting from the absolute root of the view hierarchy. + /// This includes private views like title bar views. + func firstViewFromRoot(withClassName name: String) -> NSView? { + let root = rootView + + // Check if the root view itself matches + if String(describing: type(of: root)) == name { + return root + } + + // Otherwise search descendants + return root.firstDescendant(withClassName: name) + } } diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 6b10ceb40..3200608d0 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -78,10 +78,12 @@ class FullscreenBase { } @objc private func didEnterFullScreenNotification(_ notification: Notification) { + NotificationCenter.default.post(name: .fullscreenDidEnter, object: self) delegate?.fullscreenDidChange() } @objc private func didExitFullScreenNotification(_ notification: Notification) { + NotificationCenter.default.post(name: .fullscreenDidExit, object: self) delegate?.fullscreenDidChange() } } @@ -238,6 +240,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.window.makeFirstResponder(firstResponder) } + NotificationCenter.default.post(name: .fullscreenDidEnter, object: self) self.delegate?.fullscreenDidChange() } } @@ -268,7 +271,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. - if let window = window as? TerminalWindow, + if let window = window as? LegacyTerminalWindow, window.titlebarTabs { window.titlebarTabs = true } @@ -303,6 +306,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { window.makeKeyAndOrderFront(nil) // Notify the delegate + NotificationCenter.default.post(name: .fullscreenDidExit, object: self) self.delegate?.fullscreenDidChange() } @@ -422,3 +426,8 @@ class NonNativeFullscreenVisibleMenu: NonNativeFullscreen { class NonNativeFullscreenPaddedNotch: NonNativeFullscreen { override var properties: Properties { Properties(paddedNotch: true) } } + +extension Notification.Name { + static let fullscreenDidEnter = Notification.Name("com.mitchellh.fullscreenDidEnter") + static let fullscreenDidExit = Notification.Name("com.mitchellh.fullscreenDidExit") +} From 7d02977482a3e8f6c1565c24ee26986038e0bf16 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 07:00:40 -0700 Subject: [PATCH 443/642] macos: add NSView hierarchy debugging code --- .../TransparentTitlebarTerminalWindow.swift | 11 ++++- .../Helpers/Extensions/NSView+Extension.swift | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 4b3336874..d9d42365a 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -1,14 +1,21 @@ import AppKit class TransparentTitlebarTerminalWindow: TerminalWindow { - private var reapplyTimer: Timer? + private var debugTimer: Timer? override func awakeFromNib() { super.awakeFromNib() + + // Debug timer to print view hierarchy every second + debugTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + print("=== TransparentTitlebarTerminalWindow Debug ===") + self?.contentView?.rootView.printViewHierarchy() + print("===============================================\n") + } } deinit { - reapplyTimer?.invalidate() + debugTimer?.invalidate() } override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 121d9a62a..14c07f6c9 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -82,3 +82,51 @@ extension NSView { return root.firstDescendant(withClassName: name) } } + +// MARK: Debug + +extension NSView { + /// Prints the view hierarchy from the root in a tree-like ASCII format. + /// + /// I need this because the "Capture View Hiearchy" was broken under some scenarios in + /// Xcode 26 (FB17912569). But, I kept it around because it might be useful to print out + /// the view hierarchy without halting the program. + func printViewHierarchy() { + let root = rootView + print("View Hierarchy from Root:") + print(root.viewHierarchyDescription()) + } + + /// Returns a string representation of the view hierarchy in a tree-like format. + private func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { + var result = "" + + // Add the tree branch characters + result += indent + if !indent.isEmpty { + result += isLast ? "└── " : "├── " + } + + // Add the class name and optional identifier + let className = String(describing: type(of: self)) + result += className + + // Add identifier if present + if let identifier = self.identifier { + result += " (id: \(identifier.rawValue))" + } + + // Add frame info + result += " [frame: \(frame)]" + result += "\n" + + // Process subviews + for (index, subview) in subviews.enumerated() { + let isLastSubview = index == subviews.count - 1 + let newIndent = indent + (isLast ? " " : "│ ") + result += subview.viewHierarchyDescription(indent: newIndent, isLast: isLastSubview) + } + + return result + } +} From 6ce7f612a66ea6e50884b426a4b0c3cfd6dd68be Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 07:29:44 -0700 Subject: [PATCH 444/642] macos: transparent titlebar needs to be rehidden when tabs change --- .../TransparentTitlebarTerminalWindow.swift | 64 ++++++++++++++++--- .../Helpers/Extensions/NSView+Extension.swift | 30 +++++++++ 2 files changed, 85 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index d9d42365a..9f7b5e62f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -1,24 +1,39 @@ import AppKit class TransparentTitlebarTerminalWindow: TerminalWindow { - private var debugTimer: Timer? + // We need to restore our last synced appearance so that we can reapply + // the appearance in certain scenarios. + private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig? + + // KVO observations + private var tabGroupWindowsObservation: NSKeyValueObservation? override func awakeFromNib() { super.awakeFromNib() - - // Debug timer to print view hierarchy every second - debugTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in - print("=== TransparentTitlebarTerminalWindow Debug ===") - self?.contentView?.rootView.printViewHierarchy() - print("===============================================\n") - } + + // We need to observe the tab group because we need to redraw on + // tabbed window changes and there is no notification for that. + setupTabGroupObservation() } deinit { - debugTimer?.invalidate() + tabGroupWindowsObservation?.invalidate() } + override func becomeMain() { + // On macOS Tahoe, the tab bar redraws and restores non-transparency when + // switching tabs. To overcome this, we resync the appearance whenever this + // window becomes main (focused). + if #available(macOS 26.0, *), + let lastSurfaceConfig { + syncAppearance(lastSurfaceConfig) + } + } + + // MARK: Appearance + override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + lastSurfaceConfig = surfaceConfig if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) } else { @@ -47,6 +62,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor } + // MARK: View Finders + private var titlebarBackgroundView: NSView? { titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView") } @@ -76,4 +93,33 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { private var titlebarContainerView: NSView? { contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") } + + // MARK: Tab Group Observation + + private func setupTabGroupObservation() { + // Remove existing observation if any + tabGroupWindowsObservation?.invalidate() + tabGroupWindowsObservation = nil + + // Check if tabGroup is available + guard let tabGroup else { return } + + // Set up KVO observation for the windows array. Whenever it changes + // we resync the appearance because it can cause macOS to redraw the + // tab bar. + tabGroupWindowsObservation = tabGroup.observe( + \.windows, + options: [.new] + ) { [weak self] _, _ in + // NOTE: At one point, I guarded this on only if we went from 0 to N + // or N to 0 under the assumption that the tab bar would only get + // replaced on those cases. This turned out to be false (Tahoe). + // It's cheap enough to always redraw this so we should just do it + // unconditionally. + + guard let self else { return } + guard let lastSurfaceConfig else { return } + self.syncAppearance(lastSurfaceConfig) + } + } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 14c07f6c9..0cf71138d 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -118,6 +118,36 @@ extension NSView { // Add frame info result += " [frame: \(frame)]" + + // Add visual properties + var properties: [String] = [] + + // Hidden status + if isHidden { + properties.append("hidden") + } + + // Opaque status + properties.append(isOpaque ? "opaque" : "transparent") + + // Layer backing + if wantsLayer { + properties.append("layer-backed") + if let bgColor = layer?.backgroundColor { + let color = NSColor(cgColor: bgColor) + if let rgb = color?.usingColorSpace(.deviceRGB) { + properties.append(String(format: "bg:rgba(%.0f,%.0f,%.0f,%.2f)", + rgb.redComponent * 255, + rgb.greenComponent * 255, + rgb.blueComponent * 255, + rgb.alphaComponent)) + } else { + properties.append("bg:\(bgColor)") + } + } + } + + result += " [\(properties.joined(separator: ", "))]" result += "\n" // Process subviews From 3595b2a8476ff16dae2ba266ef672d4fa295a777 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 12:37:15 -0700 Subject: [PATCH 445/642] macos: transparent titlebar handles transparent background --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Terminal/TerminalController.swift | 53 +++-------- .../Window Styles/TerminalWindow.swift | 95 ++++++++++++++++++- .../TransparentTitlebarTerminalWindow.swift | 22 ++++- .../Helpers/Extensions/Double+Extension.swift | 5 + 5 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/Double+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 594579744..c00d6119a 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; }; 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; + A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; @@ -134,6 +135,7 @@ 552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = ""; }; 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 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 = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; @@ -464,6 +466,7 @@ isa = PBXGroup; children = ( A586366A2DF0A98900E04A10 /* Array+Extension.swift */, + A50297342DFA0F3300B4E924 /* Double+Extension.swift */, A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, @@ -737,6 +740,7 @@ A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */, A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */, A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, + A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 7fb8f9a07..5adef8ded 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -449,57 +449,34 @@ class TerminalController: BaseTerminalController { } private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + // Let our window handle its own appearance if let window = window as? TerminalWindow { window.syncAppearance(surfaceConfig) } - guard let window = self.window as? LegacyTerminalWindow else { return } + guard let window else { return } - // Set our explicit appearance if we need to based on the configuration. - window.appearance = surfaceConfig.windowAppearance + if let window = window as? LegacyTerminalWindow { + // Update our window light/darkness based on our updated background color + window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor - // Update our window light/darkness based on our updated background color - window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor + // Sync our zoom state for splits + window.surfaceIsZoomed = surfaceTree.zoomed != nil - // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree.zoomed != nil + // Set the font for the window and tab titles. + if let titleFontName = surfaceConfig.windowTitleFontFamily { + window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) + } else { + window.titlebarFont = nil + } + } // If our window is not visible, then we do nothing. Some things such as blurring // have no effect if the window is not visible. Ultimately, we'll have this called // at some point when a surface becomes focused. guard window.isVisible else { return } - // Set the font for the window and tab titles. - if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) - } else { - window.titlebarFont = nil - } - - // If we have window transparency then set it transparent. Otherwise set it opaque. - - // Window transparency only takes effect if our window is not native fullscreen. - // In native fullscreen we disable transparency/opacity because the background - // becomes gray and widgets show through. - if (!window.styleMask.contains(.fullScreen) && - surfaceConfig.backgroundOpacity < 1 - ) { - window.isOpaque = false - - // This is weird, but we don't use ".clear" because this creates a look that - // matches Terminal.app much more closer. This lets users transition from - // Terminal.app more easily. - window.backgroundColor = .white.withAlphaComponent(0.001) - - ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque()) - } else { - window.isOpaque = true - window.backgroundColor = .windowBackgroundColor - } - - window.hasShadow = surfaceConfig.macosWindowShadow - - guard window.hasStyledTabs else { return } + guard let window = window as? LegacyTerminalWindow, window.hasStyledTabs else { return } // Our background color depends on if our focused surface borders the top or not. // If it does, we match the focused surface. If it doesn't, we use the app diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 74744a962..daf5b4554 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -1,4 +1,5 @@ import AppKit +import GhosttyKit /// The base class for all standalone, "normal" terminal windows. This sets the basic /// style and configuration of the window based on the app configuration. @@ -7,6 +8,14 @@ class TerminalWindow: NSWindow { /// used by the manual float on top menu item feature. static let defaultLevelKey: String = "TerminalDefaultLevel" + /// The configuration derived from the Ghostty config so we don't need to rely on references. + private var derivedConfig: DerivedConfig? + + /// Gets the terminal controller from the window controller. + var terminalController: TerminalController? { + windowController as? TerminalController + } + // MARK: NSWindow Overrides override func awakeFromNib() { @@ -15,6 +24,9 @@ class TerminalWindow: NSWindow { // All new windows are based on the app config at the time of creation. let config = appDelegate.ghostty.config + // Setup our initial config + derivedConfig = .init(config) + // If window decorations are disabled, remove our title if (!config.windowDecorations) { styleMask.remove(.titled) } @@ -42,7 +54,71 @@ class TerminalWindow: NSWindow { // MARK: Positioning And Styling /// This is called by the controller when there is a need to reset the window apperance. - func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {} + func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + // If our window is not visible, then we do nothing. Some things such as blurring + // have no effect if the window is not visible. Ultimately, we'll have this called + // at some point when a surface becomes focused. + guard isVisible else { return } + + // Basic properties + appearance = surfaceConfig.windowAppearance + hasShadow = surfaceConfig.macosWindowShadow + + // Window transparency only takes effect if our window is not native fullscreen. + // In native fullscreen we disable transparency/opacity because the background + // becomes gray and widgets show through. + if !styleMask.contains(.fullScreen) && + surfaceConfig.backgroundOpacity < 1 + { + isOpaque = false + + // This is weird, but we don't use ".clear" because this creates a look that + // matches Terminal.app much more closer. This lets users transition from + // Terminal.app more easily. + backgroundColor = .white.withAlphaComponent(0.001) + + if let appDelegate = NSApp.delegate as? AppDelegate { + ghostty_set_window_background_blur( + appDelegate.ghostty.app, + Unmanaged.passUnretained(self).toOpaque()) + } + } else { + isOpaque = true + + let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor) + self.backgroundColor = backgroundColor.withAlphaComponent(1) + } + } + + /// The preferred window background color. The current window background color may not be set + /// to this, since this is dynamic based on the state of the surface tree. + /// + /// This background color will include alpha transparency if set. If the caller doesn't want that, + /// change the alpha channel again manually. + var preferredBackgroundColor: NSColor? { + if let terminalController, !terminalController.surfaceTree.isEmpty { + // If our focused surface borders the top then we prefer its background color + if let focusedSurface = terminalController.focusedSurface, + let treeRoot = terminalController.surfaceTree.root, + let focusedNode = treeRoot.node(view: focusedSurface), + treeRoot.spatial().doesBorder(side: .up, from: focusedNode), + let backgroundcolor = focusedSurface.backgroundColor { + let alpha = focusedSurface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) + return NSColor(backgroundcolor).withAlphaComponent(alpha) + } + + // Doesn't border the top or we don't have a focused surface, so + // we try to match the top-left surface. + let topLeftSurface = terminalController.surfaceTree.root?.leftmostLeaf() + if let topLeftBgColor = topLeftSurface?.backgroundColor { + let alpha = topLeftSurface?.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) ?? 1 + return NSColor(topLeftBgColor).withAlphaComponent(alpha) + } + } + + let alpha = derivedConfig?.backgroundOpacity.clamped(to: 0.001...1) ?? 1 + return derivedConfig?.backgroundColor.withAlphaComponent(alpha) + } private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { // If we don't have an X/Y then we try to use the previously saved window pos. @@ -72,4 +148,21 @@ class TerminalWindow: NSWindow { standardWindowButton(.miniaturizeButton)?.isHidden = true standardWindowButton(.zoomButton)?.isHidden = true } + + // MARK: Config + + struct DerivedConfig { + let backgroundColor: NSColor + let backgroundOpacity: Double + + init() { + self.backgroundColor = NSColor.windowBackgroundColor + self.backgroundOpacity = 1 + } + + init(_ config: Ghostty.Config) { + self.backgroundColor = NSColor(config.backgroundColor) + self.backgroundOpacity = config.backgroundOpacity + } + } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 9f7b5e62f..dfe2d35a1 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -33,6 +33,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // MARK: Appearance override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + super.syncAppearance(surfaceConfig) + lastSurfaceConfig = surfaceConfig if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) @@ -43,9 +45,23 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 26.0, *) private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { - guard let titlebarBackgroundView else { return } - titlebarBackgroundView.isHidden = true - backgroundColor = NSColor(surfaceConfig.backgroundColor) + // When we have transparency, we need to set the titlebar background to match the + // window background but with opacity. The window background is set using the + // "preferred background color" property. + // + // As an inverse, if we don't have transparency, we don't bother with this because + // the window background will be set to the correct color so we can just hide the + // titlebar completely and we're good to go. + if !isOpaque { + if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") { + titlebarView.wantsLayer = true + titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor + } + } + + // In all cases, we have to hide the background view since this has multiple subviews + // that force a background color. + titlebarBackgroundView?.isHidden = true } @available(macOS 13.0, *) diff --git a/macos/Sources/Helpers/Extensions/Double+Extension.swift b/macos/Sources/Helpers/Extensions/Double+Extension.swift new file mode 100644 index 000000000..8d1151bac --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Double+Extension.swift @@ -0,0 +1,5 @@ +extension Double { + func clamped(to range: ClosedRange) -> Double { + return Swift.min(Swift.max(self, range.lowerBound), range.upperBound) + } +} From dfa7a114def0f4a9617ef3433dd67c3bb288b924 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 13:12:25 -0700 Subject: [PATCH 446/642] macos: make transparent titlebars robust against show/hide tabs --- .../TransparentTitlebarTerminalWindow.swift | 81 +++++++++++++------ 1 file changed, 58 insertions(+), 23 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index dfe2d35a1..98dd9f834 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -1,32 +1,41 @@ import AppKit +/// A terminal window style that provides a transparent titlebar effect. With this effect, the titlebar +/// matches the background color of the window. class TransparentTitlebarTerminalWindow: TerminalWindow { - // We need to restore our last synced appearance so that we can reapply - // the appearance in certain scenarios. + /// Stores the last surface configuration to reapply appearance when needed. + /// This is necessary because various macOS operations (tab switching, tab bar + /// visibility changes) can reset the titlebar appearance. private var lastSurfaceConfig: Ghostty.SurfaceView.DerivedConfig? - // KVO observations + /// KVO observation for tab group window changes. private var tabGroupWindowsObservation: NSKeyValueObservation? + private var tabBarVisibleObservation: NSKeyValueObservation? override func awakeFromNib() { super.awakeFromNib() - // We need to observe the tab group because we need to redraw on - // tabbed window changes and there is no notification for that. - setupTabGroupObservation() + // Setup all the KVO we will use, see the docs for the respective functions + // to learn why we need KVO. + setupKVO() } deinit { tabGroupWindowsObservation?.invalidate() + tabBarVisibleObservation?.invalidate() } override func becomeMain() { - // On macOS Tahoe, the tab bar redraws and restores non-transparency when - // switching tabs. To overcome this, we resync the appearance whenever this - // window becomes main (focused). - if #available(macOS 26.0, *), - let lastSurfaceConfig { - syncAppearance(lastSurfaceConfig) + guard let lastSurfaceConfig else { return } + syncAppearance(lastSurfaceConfig) + + // This is a nasty edge case. If we're going from 2 to 1 tab and the tab bar + // automatically disappears, then we need to resync our appearance because + // at some point macOS replaces the tab views. + if tabGroup?.windows.count ?? 0 == 2 { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(50)) { [weak self] in + self?.syncAppearance(self?.lastSurfaceConfig ?? lastSurfaceConfig) + } } } @@ -34,8 +43,14 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { super.syncAppearance(surfaceConfig) - + + // Save our config in case we need to reapply lastSurfaceConfig = surfaceConfig + + // Everytime we change appearance, set KVO up again in case any of our + // references changed (e.g. tabGroup is new). + setupKVO() + if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) } else { @@ -67,15 +82,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 13.0, *) private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { guard let titlebarContainer else { return } - - let configBgColor = NSColor(surfaceConfig.backgroundColor) - - // Set our window background color so it shows up - backgroundColor = configBgColor - - // Set the background color of our titlebar to match titlebarContainer.wantsLayer = true - titlebarContainer.layer?.backgroundColor = configBgColor.withAlphaComponent(surfaceConfig.backgroundOpacity).cgColor + titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor } // MARK: View Finders @@ -111,7 +119,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } // MARK: Tab Group Observation - + + private func setupKVO() { + // See the docs for the respective setup functions for why. + setupTabGroupObservation() + setupTabBarVisibleObservation() + } + + /// Monitors the tabGroup windows value for any changes and resyncs the appearance on change. + /// This is necessary because when the windows change, the tab bar and titlebar are recreated + /// which breaks our changes. private func setupTabGroupObservation() { // Remove existing observation if any tabGroupWindowsObservation?.invalidate() @@ -126,7 +143,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { tabGroupWindowsObservation = tabGroup.observe( \.windows, options: [.new] - ) { [weak self] _, _ in + ) { [weak self] _, change in // NOTE: At one point, I guarded this on only if we went from 0 to N // or N to 0 under the assumption that the tab bar would only get // replaced on those cases. This turned out to be false (Tahoe). @@ -138,4 +155,22 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { self.syncAppearance(lastSurfaceConfig) } } + + /// Monitors the tab bar for visibility. This lets the "Show/Hide Tab Bar" manual menu item + /// to not break our appearance. + private func setupTabBarVisibleObservation() { + // Remove existing observation if any + tabBarVisibleObservation?.invalidate() + tabBarVisibleObservation = nil + + // Set up KVO observation for isTabBarVisible + tabBarVisibleObservation = tabGroup?.observe( + \.isTabBarVisible, + options: [.new] + ) { [weak self] _, change in + guard let self else { return } + guard let lastSurfaceConfig else { return } + self.syncAppearance(lastSurfaceConfig) + } + } } From a804dab28845675271afa13a80e23fc524386c78 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 14:35:49 -0700 Subject: [PATCH 447/642] macos: native terminal style works with new subclasses --- macos/Ghostty.xcodeproj/project.pbxproj | 6 +- .../Terminal/BaseTerminalController.swift | 6 +- .../Terminal/TerminalController.swift | 54 ++++++++---- .../TerminalTransparentTitlebar.xib | 2 +- .../Window Styles/TerminalWindow.swift | 88 +++++++++++++++++++ .../TransparentTitlebarTerminalWindow.swift | 2 + .../Helpers/Extensions/NSView+Extension.swift | 15 ++++ 7 files changed, 151 insertions(+), 22 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index c00d6119a..5f5b3013c 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -402,12 +402,12 @@ isa = PBXGroup; children = ( A59630992AEE1C6400D64628 /* Terminal.xib */, - A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, - A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, - A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, + A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, + A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, + A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, ); path = "Window Styles"; diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e91199358..849f13b34 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -568,7 +568,11 @@ class BaseTerminalController: NSWindowController, // Not zoomed or different node zoomed, zoom this node surfaceTree = SplitTree(root: surfaceTree.root, zoomed: targetNode) } - + + // Move focus to our window. Importantly this ensures that if we click the + // reset zoom button in a tab bar of an unfocused tab that we become focused. + window?.makeKeyAndOrderFront(nil) + // Ensure focus stays on the target surface. We lose focus when we do // this so we need to grab it again. DispatchQueue.main.async { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 5adef8ded..082a3c806 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -14,6 +14,7 @@ class TerminalController: BaseTerminalController { guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { + case "native": "Terminal" case "tabs": defaultValue case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" @@ -132,6 +133,9 @@ class TerminalController: BaseTerminalController { if let window = window as? LegacyTerminalWindow { window.surfaceIsZoomed = to.zoomed != nil } + if let window = window as? TerminalWindow { + window.surfaceIsZoomed2 = to.zoomed != nil + } // If our surface tree is now nil then we close our window. if (to.isEmpty) { @@ -395,28 +399,44 @@ class TerminalController: BaseTerminalController { /// changes, when a window is closed, and when tabs are reordered /// with the mouse. func relabelTabs() { - // Reset this to false. It'll be set back to true later. - tabListenForFrame = false - - guard let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] else { return } - // We only listen for frame changes if we have more than 1 window, // otherwise the accessory view doesn't matter. - tabListenForFrame = windows.count > 1 + tabListenForFrame = window?.tabbedWindows?.count ?? 0 > 1 - for (tab, window) in zip(1..., windows) { - // We need to clear any windows beyond this because they have had - // a keyEquivalent set previously. - guard tab <= 9 else { - window.keyEquivalent = "" - continue + if let windows = window?.tabbedWindows as? [TerminalWindow] { + for (tab, window) in zip(1..., windows) { + // We need to clear any windows beyond this because they have had + // a keyEquivalent set previously. + guard tab <= 9 else { + window.keyEquivalent2 = "" + continue + } + + let action = "goto_tab:\(tab)" + if let equiv = ghostty.config.keyboardShortcut(for: action) { + window.keyEquivalent2 = "\(equiv)" + } else { + window.keyEquivalent2 = "" + } } + } - let action = "goto_tab:\(tab)" - if let equiv = ghostty.config.keyboardShortcut(for: action) { - window.keyEquivalent = "\(equiv)" - } else { - window.keyEquivalent = "" + // Legacy + if let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] { + for (tab, window) in zip(1..., windows) { + // We need to clear any windows beyond this because they have had + // a keyEquivalent set previously. + guard tab <= 9 else { + window.keyEquivalent = "" + continue + } + + let action = "goto_tab:\(tab)" + if let equiv = ghostty.config.keyboardShortcut(for: action) { + window.keyEquivalent = "\(equiv)" + } else { + window.keyEquivalent = "" + } } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib index ada6959b3..25922e2f3 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTransparentTitlebar.xib @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index daf5b4554..9fac08c4b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -1,4 +1,5 @@ import AppKit +import SwiftUI import GhosttyKit /// The base class for all standalone, "normal" terminal windows. This sets the basic @@ -42,6 +43,14 @@ class TerminalWindow: NSWindow { hideWindowButtons() } + // Setup the accessory view for tabs that shows our keyboard shortcuts, + // zoomed state, etc. Note I tried to use SwiftUI here but ran into issues + // where buttons were not clickable. + let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) + stackView.setHuggingPriority(.defaultHigh, for: .horizontal) + stackView.spacing = 3 + tab.accessoryView = stackView + // Get our saved level level = UserDefaults.standard.value(forKey: Self.defaultLevelKey) as? NSWindow.Level ?? .normal } @@ -51,6 +60,85 @@ class TerminalWindow: NSWindow { override var canBecomeKey: Bool { return true } override var canBecomeMain: Bool { return true } + override func becomeKey() { + super.becomeKey() + resetZoomTabButton.contentTintColor = .controlAccentColor + } + + override func resignKey() { + super.resignKey() + resetZoomTabButton.contentTintColor = .secondaryLabelColor + } + + override func mergeAllWindows(_ sender: Any?) { + super.mergeAllWindows(sender) + + // It takes an event loop cycle to merge all the windows so we set a + // short timer to relabel the tabs (issue #1902) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.terminalController?.relabelTabs() + } + } + + // MARK: Tab Key Equivalents + + // TODO: rename once Legacy window removes + var keyEquivalent2: String? = nil { + didSet { + // When our key equivalent is set, we must update the tab label. + guard let keyEquivalent2 else { + keyEquivalentLabel.attributedStringValue = NSAttributedString() + return + } + + keyEquivalentLabel.attributedStringValue = NSAttributedString( + string: "\(keyEquivalent2) ", + attributes: [ + .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), + .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ]) + } + } + + /// The label that has the key equivalent for tab views. + private lazy var keyEquivalentLabel: NSTextField = { + let label = NSTextField(labelWithAttributedString: NSAttributedString()) + label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) + label.postsFrameChangedNotifications = true + return label + }() + + // MARK: Surface Zoom + + /// Set to true if a surface is currently zoomed to show the reset zoom button. + var surfaceIsZoomed2: Bool = false { + didSet { + // Show/hide our reset zoom button depending on if we're zoomed. + // We want to show it if we are zoomed. + resetZoomTabButton.isHidden = !surfaceIsZoomed2 + } + } + + private lazy var resetZoomTabButton: NSButton = generateResetZoomButton() + + private func generateResetZoomButton() -> NSButton { + let button = NSButton() + button.isHidden = true + button.target = terminalController + button.action = #selector(TerminalController.splitZoom(_:)) + button.isBordered = false + button.allowsExpansionToolTips = true + button.toolTip = "Reset Zoom" + button.contentTintColor = .controlAccentColor + button.state = .on + button.image = NSImage(named:"ResetZoom") + button.frame = NSRect(x: 0, y: 0, width: 20, height: 20) + button.translatesAutoresizingMaskIntoConstraints = false + button.widthAnchor.constraint(equalToConstant: 20).isActive = true + button.heightAnchor.constraint(equalToConstant: 20).isActive = true + return button + } + // MARK: Positioning And Styling /// This is called by the controller when there is a need to reset the window apperance. diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 98dd9f834..ada84ff12 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -26,6 +26,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } override func becomeMain() { + super.becomeMain() + guard let lastSurfaceConfig else { return } syncAppearance(lastSurfaceConfig) diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 0cf71138d..aa56fe32e 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -27,6 +27,21 @@ extension NSView { return root } + /// Checks if a view contains another view in its hierarchy. + func contains(_ view: NSView) -> Bool { + if self == view { + return true + } + + for subview in subviews { + if subview.contains(view) { + return true + } + } + + return false + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { From 63e56d0402a04696fbaff2eb7e5dbbf8f6257740 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 15:08:12 -0700 Subject: [PATCH 448/642] macos: titlebar fonts work with new terminal window --- .../Terminal/TerminalController.swift | 11 ++++ .../Window Styles/LegacyTerminalWindow.swift | 42 ------------- .../Window Styles/TerminalWindow.swift | 62 ++++++++++++++++++- .../TransparentTitlebarTerminalWindow.swift | 26 -------- 4 files changed, 72 insertions(+), 69 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 082a3c806..cf771d556 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -471,6 +471,17 @@ class TerminalController: BaseTerminalController { private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // Let our window handle its own appearance if let window = window as? TerminalWindow { + // Sync our zoom state for splits + window.surfaceIsZoomed2 = surfaceTree.zoomed != nil + + // Set the font for the window and tab titles. + if let titleFontName = surfaceConfig.windowTitleFontFamily { + window.titlebarFont2 = NSFont(name: titleFontName, size: NSFont.systemFontSize) + } else { + window.titlebarFont2 = nil + } + + // Call this last in case it uses any of the properties above. window.syncAppearance(surfaceConfig) } diff --git a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift index 208e86343..e63681463 100644 --- a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift @@ -77,48 +77,6 @@ class LegacyTerminalWindow: TerminalWindow { bindings.forEach() { $0.invalidate() } } - // MARK: Titlebar Helpers - // These helpers are generic to what we're trying to achieve (i.e. titlebar - // style tabs, titlebar styling, etc.). They're just here to make it easier. - - private var titlebarContainer: NSView? { - // If we aren't fullscreen then the titlebar container is part of our window. - if !styleMask.contains(.fullScreen) { - guard let view = contentView?.superview ?? contentView else { return nil } - return titlebarContainerView(in: view) - } - - // If we are fullscreen, the titlebar container view is part of a separate - // "fullscreen window", we need to find the window and then get the view. - for window in NSApplication.shared.windows { - // This is the private window class that contains the toolbar - guard window.className == "NSToolbarFullScreenWindow" else { continue } - - // The parent will match our window. This is used to filter the correct - // fullscreen window if we have multiple. - guard window.parent == self else { continue } - - guard let view = window.contentView else { continue } - return titlebarContainerView(in: view) - } - - return nil - } - - private func titlebarContainerView(in view: NSView) -> NSView? { - if view.className == "NSTitlebarContainerView" { - return view - } - - for subview in view.subviews { - if let found = titlebarContainerView(in: subview) { - return found - } - } - - return nil - } - // MARK: - NSWindow override var title: String { diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 9fac08c4b..1e9ca1b01 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -72,7 +72,7 @@ class TerminalWindow: NSWindow { override func mergeAllWindows(_ sender: Any?) { super.mergeAllWindows(sender) - + // It takes an event loop cycle to merge all the windows so we set a // short timer to relabel the tabs (issue #1902) DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in @@ -139,6 +139,66 @@ class TerminalWindow: NSWindow { return button } + // MARK: Title Text + + override var title: String { + didSet { + // Whenever we change the window title we must also update our + // tab title if we're using custom fonts. + tab.attributedTitle = attributedTitle + } + } + + // Used to set the titlebar font. + var titlebarFont2: NSFont? { + didSet { + let font = titlebarFont2 ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) + + titlebarTextField?.font = font + tab.attributedTitle = attributedTitle + } + } + + // Find the NSTextField responsible for displaying the titlebar's title. + private var titlebarTextField: NSTextField? { + titlebarContainer? + .firstDescendant(withClassName: "NSTitlebarView")? + .firstDescendant(withClassName: "NSTextField") as? NSTextField + } + + // Return a styled representation of our title property. + private var attributedTitle: NSAttributedString? { + guard let titlebarFont = titlebarFont2 else { return nil } + + let attributes: [NSAttributedString.Key: Any] = [ + .font: titlebarFont, + .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, + ] + return NSAttributedString(string: title, attributes: attributes) + } + + var titlebarContainer: NSView? { + // If we aren't fullscreen then the titlebar container is part of our window. + if !styleMask.contains(.fullScreen) { + return contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") + } + + // If we are fullscreen, the titlebar container view is part of a separate + // "fullscreen window", we need to find the window and then get the view. + for window in NSApplication.shared.windows { + // This is the private window class that contains the toolbar + guard window.className == "NSToolbarFullScreenWindow" else { continue } + + // The parent will match our window. This is used to filter the correct + // fullscreen window if we have multiple. + guard window.parent == self else { continue } + + return window.contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") + } + + return nil + } + // MARK: Positioning And Styling /// This is called by the controller when there is a need to reset the window apperance. diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index ada84ff12..f949b6094 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -94,32 +94,6 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { titlebarContainer?.firstDescendant(withClassName: "NSTitlebarBackgroundView") } - private var titlebarContainer: NSView? { - // If we aren't fullscreen then the titlebar container is part of our window. - if !styleMask.contains(.fullScreen) { - return titlebarContainerView - } - - // If we are fullscreen, the titlebar container view is part of a separate - // "fullscreen window", we need to find the window and then get the view. - for window in NSApplication.shared.windows { - // This is the private window class that contains the toolbar - guard window.className == "NSToolbarFullScreenWindow" else { continue } - - // The parent will match our window. This is used to filter the correct - // fullscreen window if we have multiple. - guard window.parent == self else { continue } - - return titlebarContainerView - } - - return nil - } - - private var titlebarContainerView: NSView? { - contentView?.firstViewFromRoot(withClassName: "NSTitlebarContainerView") - } - // MARK: Tab Group Observation private func setupKVO() { From e5cb33e9114a7b0818043c0f210bc2039b10cc00 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 15:09:42 -0700 Subject: [PATCH 449/642] typos --- .../Features/Terminal/Window Styles/TerminalWindow.swift | 2 +- macos/Sources/Helpers/Extensions/NSView+Extension.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 1e9ca1b01..eb4b4a6da 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -201,7 +201,7 @@ class TerminalWindow: NSWindow { // MARK: Positioning And Styling - /// This is called by the controller when there is a need to reset the window apperance. + /// This is called by the controller when there is a need to reset the window appearance. func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // If our window is not visible, then we do nothing. Some things such as blurring // have no effect if the window is not visible. Ultimately, we'll have this called diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index aa56fe32e..b958130f1 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -103,7 +103,7 @@ extension NSView { extension NSView { /// Prints the view hierarchy from the root in a tree-like ASCII format. /// - /// I need this because the "Capture View Hiearchy" was broken under some scenarios in + /// I need this because the "Capture View Hierarchy" was broken under some scenarios in /// Xcode 26 (FB17912569). But, I kept it around because it might be useful to print out /// the view hierarchy without halting the program. func printViewHierarchy() { From ccfd33022f2402b294aafdc9ce8224b15069a5f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 11 Jun 2025 15:15:06 -0700 Subject: [PATCH 450/642] macos: only titlebar tabs uses legacy styling now --- macos/Sources/Features/Terminal/TerminalController.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cf771d556..86b47b9bd 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -7,15 +7,13 @@ import GhosttyKit /// A classic, tabbed terminal experience. class TerminalController: BaseTerminalController { override var windowNibName: NSNib.Name? { - //NOTE(mitchellh): switch to this when we've transitioned all legacy logic out - //let defaultValue = "Terminal" - let defaultValue = "TerminalLegacy" + let defaultValue = "Terminal" guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue } let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { case "native": "Terminal" - case "tabs": defaultValue + case "tabs": "TerminalLegacy" case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" default: defaultValue From fd785f98bb5d794645079918df2828c4e0e09e1f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 11:36:38 -0700 Subject: [PATCH 451/642] macos: titlebar tabs uses legacy window for now --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++ .../Terminal/TerminalController.swift | 56 +++-------- .../Window Styles/LegacyTerminalWindow.swift | 93 ++----------------- .../TabsTitlebarTerminalWindow.swift | 58 ++++++++++++ .../Window Styles/TerminalHiddenTitlebar.xib | 2 +- .../Window Styles/TerminalTabsTitlebar.xib | 31 +++++++ .../Window Styles/TerminalWindow.swift | 12 +-- 7 files changed, 124 insertions(+), 136 deletions(-) create mode 100644 macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift create mode 100644 macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 5f5b3013c..3f1cddf44 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; + A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */; }; + A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */; }; A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; @@ -137,6 +139,8 @@ 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 = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; + A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTitlebarTerminalWindow.swift; sourceTree = ""; }; + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebar.xib; sourceTree = ""; }; A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; @@ -404,10 +408,12 @@ A59630992AEE1C6400D64628 /* Terminal.xib */, A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */, A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, + A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */, A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, ); path = "Window Styles"; @@ -694,6 +700,7 @@ A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, + A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */, A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -761,6 +768,7 @@ A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, + A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 86b47b9bd..d59f71619 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -13,6 +13,7 @@ class TerminalController: BaseTerminalController { let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { case "native": "Terminal" + //case "tabs": "TerminalTabsTitlebar" case "tabs": "TerminalLegacy" case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" @@ -128,11 +129,8 @@ class TerminalController: BaseTerminalController { invalidateRestorableState() // Update our zoom state - if let window = window as? LegacyTerminalWindow { - window.surfaceIsZoomed = to.zoomed != nil - } if let window = window as? TerminalWindow { - window.surfaceIsZoomed2 = to.zoomed != nil + window.surfaceIsZoomed = to.zoomed != nil } // If our surface tree is now nil then we close our window. @@ -418,25 +416,6 @@ class TerminalController: BaseTerminalController { } } } - - // Legacy - if let windows = self.window?.tabbedWindows as? [LegacyTerminalWindow] { - for (tab, window) in zip(1..., windows) { - // We need to clear any windows beyond this because they have had - // a keyEquivalent set previously. - guard tab <= 9 else { - window.keyEquivalent = "" - continue - } - - let action = "goto_tab:\(tab)" - if let equiv = ghostty.config.keyboardShortcut(for: action) { - window.keyEquivalent = "\(equiv)" - } else { - window.keyEquivalent = "" - } - } - } } private func fixTabBar() { @@ -470,13 +449,13 @@ class TerminalController: BaseTerminalController { // Let our window handle its own appearance if let window = window as? TerminalWindow { // Sync our zoom state for splits - window.surfaceIsZoomed2 = surfaceTree.zoomed != nil + window.surfaceIsZoomed = surfaceTree.zoomed != nil // Set the font for the window and tab titles. if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont2 = NSFont(name: titleFontName, size: NSFont.systemFontSize) + window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) } else { - window.titlebarFont2 = nil + window.titlebarFont = nil } // Call this last in case it uses any of the properties above. @@ -488,16 +467,6 @@ class TerminalController: BaseTerminalController { if let window = window as? LegacyTerminalWindow { // Update our window light/darkness based on our updated background color window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor - - // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree.zoomed != nil - - // Set the font for the window and tab titles. - if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) - } else { - window.titlebarFont = nil - } } // If our window is not visible, then we do nothing. Some things such as blurring @@ -916,18 +885,15 @@ class TerminalController: BaseTerminalController { } // TODO: remove - if let window = window as? LegacyTerminalWindow { + if let window = window as? LegacyTerminalWindow, + config.macosTitlebarStyle == "tabs" { // Handle titlebar tabs config option. Something about what we do while setting up the // titlebar tabs interferes with the window restore process unless window.tabbingMode // is set to .preferred, so we set it, and switch back to automatic as soon as we can. - if (config.macosTitlebarStyle == "tabs") { - window.tabbingMode = .preferred - window.titlebarTabs = true - DispatchQueue.main.async { - window.tabbingMode = .automatic - } - } else if (config.macosTitlebarStyle == "transparent") { - window.transparentTabs = true + window.tabbingMode = .preferred + window.titlebarTabs = true + DispatchQueue.main.async { + window.tabbingMode = .automatic } if window.hasStyledTabs { diff --git a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift index e63681463..89afbf72f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift @@ -3,12 +3,16 @@ import Cocoa /// The terminal window that we originally had in Ghostty for a long time. Kind of a soupy mess /// of styling. class LegacyTerminalWindow: TerminalWindow { - @objc dynamic var keyEquivalent: String = "" - /// This is used to determine if certain elements should be drawn light or dark and should /// be updated whenever the window background color or surrounding elements changes. var isLightTheme: Bool = false + override var surfaceIsZoomed: Bool { + didSet { + updateResetZoomTitlebarButtonVisibility() + } + } + lazy var titlebarColor: NSColor = backgroundColor { didSet { guard let titlebarContainer else { return } @@ -17,33 +21,6 @@ class LegacyTerminalWindow: TerminalWindow { } } - private lazy var keyEquivalentLabel: NSTextField = { - let label = NSTextField(labelWithAttributedString: NSAttributedString()) - label.setContentCompressionResistancePriority(.windowSizeStayPut, for: .horizontal) - label.postsFrameChangedNotifications = true - - return label - }() - - private lazy var bindings = [ - observe(\.surfaceIsZoomed, options: [.initial, .new]) { [weak self] window, _ in - guard let tabGroup = self?.tabGroup else { return } - - self?.resetZoomTabButton.isHidden = !window.surfaceIsZoomed - self?.updateResetZoomTitlebarButtonVisibility() - }, - - observe(\.keyEquivalent, options: [.initial, .new]) { [weak self] window, _ in - let attributes: [NSAttributedString.Key: Any] = [ - .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), - .foregroundColor: window.isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - let attributedString = NSAttributedString(string: " \(window.keyEquivalent) ", attributes: attributes) - - self?.keyEquivalentLabel.attributedStringValue = attributedString - }, - ] - // false if all three traffic lights are missing/hidden, otherwise true private var hasWindowButtons: Bool { get { @@ -60,31 +37,13 @@ class LegacyTerminalWindow: TerminalWindow { override func awakeFromNib() { super.awakeFromNib() - _ = bindings - - // Create the tab accessory view that houses the key-equivalent label and optional un-zoom button - let stackView = NSStackView(views: [keyEquivalentLabel, resetZoomTabButton]) - stackView.setHuggingPriority(.defaultHigh, for: .horizontal) - stackView.spacing = 3 - tab.accessoryView = stackView - if titlebarTabs { generateToolbar() } } - deinit { - bindings.forEach() { $0.invalidate() } - } - // MARK: - NSWindow - override var title: String { - didSet { - tab.attributedTitle = attributedTitle - } - } - // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to // remove the effect view if the default tab bar is being used since the effect created in @@ -101,7 +60,6 @@ class LegacyTerminalWindow: TerminalWindow { super.becomeKey() updateNewTabButtonOpacity() - resetZoomTabButton.contentTintColor = .controlAccentColor resetZoomToolbarButton.contentTintColor = .controlAccentColor tab.attributedTitle = attributedTitle } @@ -110,7 +68,6 @@ class LegacyTerminalWindow: TerminalWindow { super.resignKey() updateNewTabButtonOpacity() - resetZoomTabButton.contentTintColor = .secondaryLabelColor resetZoomToolbarButton.contentTintColor = .tertiaryLabelColor tab.attributedTitle = attributedTitle } @@ -284,16 +241,8 @@ class LegacyTerminalWindow: TerminalWindow { // MARK: - Split Zoom Button - @objc dynamic var surfaceIsZoomed: Bool = false - private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() - private lazy var resetZoomTabButton: NSButton = { - let button = generateResetZoomButton() - button.action = #selector(selectTabAndZoom(_:)) - return button - }() - private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { guard let titlebarContainer else { return nil } let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) @@ -356,37 +305,13 @@ class LegacyTerminalWindow: TerminalWindow { // MARK: - Titlebar Font // Used to set the titlebar font. - var titlebarFont: NSFont? { + override var titlebarFont: NSFont? { didSet { - let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) - - titlebarTextField?.font = font - tab.attributedTitle = attributedTitle - - if let toolbar = toolbar as? TerminalToolbar { - toolbar.titleFont = font - } + guard let toolbar = toolbar as? TerminalToolbar else { return } + toolbar.titleFont = titlebarFont ?? .titleBarFont(ofSize: NSFont.systemFontSize) } } - // Find the NSTextField responsible for displaying the titlebar's title. - private var titlebarTextField: NSTextField? { - guard let titlebarView = titlebarContainer?.subviews - .first(where: { $0.className == "NSTitlebarView" }) else { return nil } - return titlebarView.subviews.first(where: { $0 is NSTextField }) as? NSTextField - } - - // Return a styled representation of our title property. - private var attributedTitle: NSAttributedString? { - guard let titlebarFont else { return nil } - - let attributes: [NSAttributedString.Key: Any] = [ - .font: titlebarFont, - .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, - ] - return NSAttributedString(string: title, attributes: attributes) - } - // MARK: - Titlebar Tabs private var windowButtonsBackdrop: WindowButtonsBackdropView? = nil diff --git a/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift new file mode 100644 index 000000000..858b54829 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift @@ -0,0 +1,58 @@ +import AppKit +import SwiftUI + +class TabsTitlebarTerminalWindow: TerminalWindow, NSToolbarDelegate { + override func awakeFromNib() { + super.awakeFromNib() + + // We must hide the title since we're going to be moving tabs into + // the titlebar which have their own title. + titleVisibility = .hidden + + // Create a toolbar + let toolbar = NSToolbar(identifier: "TerminalToolbar") + toolbar.delegate = self + toolbar.centeredItemIdentifiers.insert(.title) + self.toolbar = toolbar + //toolbarStyle = .unifiedCompact + } + + // MARK: NSToolbarDelegate + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.title, .flexibleSpace, .space] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.flexibleSpace, .title, .flexibleSpace] + } + + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + switch itemIdentifier { + case .title: + let item = NSToolbarItem(itemIdentifier: .title) + item.view = NSHostingView(rootView: TitleItem()) + item.visibilityPriority = .user + item.isEnabled = true + return item + default: + return NSToolbarItem(itemIdentifier: itemIdentifier) + } + } + +} + +extension NSToolbarItem.Identifier { + /// Displays the title of the window + static let title = NSToolbarItem.Identifier("Title") +} + +extension TabsTitlebarTerminalWindow { + struct TitleItem: View { + var body: some View { + Text("HELLO THIS IS A PRETTY LONG TITLE") + } + } +} diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib index eb4675657..1a2a6c192 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib new file mode 100644 index 000000000..779b6e094 --- /dev/null +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index eb4b4a6da..a0e18d283 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -111,11 +111,11 @@ class TerminalWindow: NSWindow { // MARK: Surface Zoom /// Set to true if a surface is currently zoomed to show the reset zoom button. - var surfaceIsZoomed2: Bool = false { + var surfaceIsZoomed: Bool = false { didSet { // Show/hide our reset zoom button depending on if we're zoomed. // We want to show it if we are zoomed. - resetZoomTabButton.isHidden = !surfaceIsZoomed2 + resetZoomTabButton.isHidden = !surfaceIsZoomed } } @@ -150,9 +150,9 @@ class TerminalWindow: NSWindow { } // Used to set the titlebar font. - var titlebarFont2: NSFont? { + var titlebarFont: NSFont? { didSet { - let font = titlebarFont2 ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) + let font = titlebarFont ?? NSFont.titleBarFont(ofSize: NSFont.systemFontSize) titlebarTextField?.font = font tab.attributedTitle = attributedTitle @@ -167,8 +167,8 @@ class TerminalWindow: NSWindow { } // Return a styled representation of our title property. - private var attributedTitle: NSAttributedString? { - guard let titlebarFont = titlebarFont2 else { return nil } + var attributedTitle: NSAttributedString? { + guard let titlebarFont = titlebarFont else { return nil } let attributes: [NSAttributedString.Key: Any] = [ .font: titlebarFont, From 5877913ab8104d9887efb6dbacee8c2bc7b39200 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 12:02:31 -0700 Subject: [PATCH 452/642] macoS: Split out terminal tabs for ventura vs tahoe --- macos/Ghostty.xcodeproj/project.pbxproj | 32 ++--- .../Terminal/TerminalController.swift | 114 +++--------------- .../HiddenTitlebarTerminalWindow.swift | 29 +++-- .../Window Styles/TerminalHiddenTitlebar.xib | 2 +- ...gacy.xib => TerminalTabsTitlebarTahoe.xib} | 2 +- ...ar.xib => TerminalTabsTitlebarVentura.xib} | 4 +- .../Window Styles/TerminalWindow.swift | 2 +- ... => TitlebarTabsTahoeTerminalWindow.swift} | 5 +- ...> TitlebarTabsVenturaTerminalWindow.swift} | 72 +++++++++-- macos/Sources/Helpers/Fullscreen.swift | 3 +- 10 files changed, 120 insertions(+), 145 deletions(-) rename macos/Sources/Features/Terminal/Window Styles/{TerminalLegacy.xib => TerminalTabsTitlebarTahoe.xib} (94%) rename macos/Sources/Features/Terminal/Window Styles/{TerminalTabsTitlebar.xib => TerminalTabsTitlebarVentura.xib} (91%) rename macos/Sources/Features/Terminal/Window Styles/{TabsTitlebarTerminalWindow.swift => TitlebarTabsTahoeTerminalWindow.swift} (90%) rename macos/Sources/Features/Terminal/Window Styles/{LegacyTerminalWindow.swift => TitlebarTabsVenturaTerminalWindow.swift} (91%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 3f1cddf44..cd0c17f9b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -16,9 +16,9 @@ A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; - A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */; }; - A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */; }; - A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */; }; + A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */; }; + A51545002DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */; }; + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */; }; A51BFC1E2B2FB5CE00E92F16 /* About.xib in Resources */ = {isa = PBXBuildFile; fileRef = A51BFC1D2B2FB5CE00E92F16 /* About.xib */; }; A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */; }; A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51BFC212B2FB6B400E92F16 /* AboutView.swift */; }; @@ -57,7 +57,7 @@ A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; }; A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; }; A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; }; - A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */; }; + A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */; }; A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */; }; A5593FE92DF927DF00B47B10 /* TerminalTransparentTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */; }; A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55B7BB729B6F53A0055DE60 /* Package.swift */; }; @@ -139,9 +139,9 @@ 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 = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; - A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTitlebarTerminalWindow.swift; sourceTree = ""; }; - A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebar.xib; sourceTree = ""; }; - A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = ""; }; + A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; }; + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; }; + A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsVenturaTerminalWindow.swift; sourceTree = ""; }; A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = ""; }; A51BFC1F2B2FB64F00E92F16 /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = ""; }; A51BFC212B2FB6B400E92F16 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; @@ -174,7 +174,7 @@ A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; }; A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = ""; }; - A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalLegacy.xib; sourceTree = ""; }; + A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarVentura.xib; sourceTree = ""; }; A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentTitlebarTerminalWindow.swift; sourceTree = ""; }; A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTransparentTitlebar.xib; sourceTree = ""; }; A55B7BB729B6F53A0055DE60 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; @@ -407,13 +407,13 @@ children = ( A59630992AEE1C6400D64628 /* Terminal.xib */, A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */, - A5593FE42DF8DE3000B47B10 /* TerminalLegacy.xib */, - A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebar.xib */, + A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */, + A5593FE42DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib */, A5593FE82DF927DF00B47B10 /* TerminalTransparentTitlebar.xib */, A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */, A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */, - A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */, - A51544FD2DFB1110009E85D8 /* TabsTitlebarTerminalWindow.swift */, + A51B78462AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift */, + A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */, A5593FE62DF927CC00B47B10 /* TransparentTitlebarTerminalWindow.swift */, ); path = "Window Styles"; @@ -682,7 +682,7 @@ buildActionMask = 2147483647; files = ( FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */, - A5593FE52DF8DE3000B47B10 /* TerminalLegacy.xib in Resources */, + A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib in Resources */, 29C15B1D2CDC3B2900520DD4 /* bat in Resources */, A586167C2B7703CC009BDB1D /* fish in Resources */, 55154BE02B33911F001622DC /* ghostty in Resources */, @@ -700,7 +700,7 @@ A5CDF1912AAF9A5800513312 /* ConfigurationErrors.xib in Resources */, 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */, A596309A2AEE1C6400D64628 /* Terminal.xib in Resources */, - A51545002DFB112E009E85D8 /* TerminalTabsTitlebar.xib in Resources */, + A51545002DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib in Resources */, A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -768,7 +768,7 @@ A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, - A51544FE2DFB111C009E85D8 /* TabsTitlebarTerminalWindow.swift in Sources */, + A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, @@ -777,7 +777,7 @@ A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, - A51B78472AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift in Sources */, + A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index d59f71619..977a064e0 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -13,10 +13,15 @@ class TerminalController: BaseTerminalController { let config = appDelegate.ghostty.config let nib = switch config.macosTitlebarStyle { case "native": "Terminal" - //case "tabs": "TerminalTabsTitlebar" - case "tabs": "TerminalLegacy" case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" + case "tabs": + if #available(macOS 26.0, *) { + // TODO: Switch to Tahoe when ready + "TerminalTabsTitlebarVentura" + } else { + "TerminalTabsTitlebarVentura" + } default: defaultValue } @@ -447,68 +452,20 @@ class TerminalController: BaseTerminalController { private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { // Let our window handle its own appearance - if let window = window as? TerminalWindow { - // Sync our zoom state for splits - window.surfaceIsZoomed = surfaceTree.zoomed != nil + guard let window = window as? TerminalWindow else { return } - // Set the font for the window and tab titles. - if let titleFontName = surfaceConfig.windowTitleFontFamily { - window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) - } else { - window.titlebarFont = nil - } + // Sync our zoom state for splits + window.surfaceIsZoomed = surfaceTree.zoomed != nil - // Call this last in case it uses any of the properties above. - window.syncAppearance(surfaceConfig) - } - - guard let window else { return } - - if let window = window as? LegacyTerminalWindow { - // Update our window light/darkness based on our updated background color - window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor - } - - // If our window is not visible, then we do nothing. Some things such as blurring - // have no effect if the window is not visible. Ultimately, we'll have this called - // at some point when a surface becomes focused. - guard window.isVisible else { return } - - guard let window = window as? LegacyTerminalWindow, window.hasStyledTabs else { return } - - // Our background color depends on if our focused surface borders the top or not. - // If it does, we match the focused surface. If it doesn't, we use the app - // configuration. - let backgroundColor: OSColor - if !surfaceTree.isEmpty { - if let focusedSurface = focusedSurface, - let treeRoot = surfaceTree.root, - let focusedNode = treeRoot.node(view: focusedSurface), - treeRoot.spatial().doesBorder(side: .up, from: focusedNode) { - // Similar to above, an alpha component of "0" causes compositor issues, so - // we use 0.001. See: https://github.com/ghostty-org/ghostty/pull/4308 - backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.001) - } else { - // We don't have a focused surface or our surface doesn't border the - // top. We choose to match the color of the top-left most surface. - let topLeftSurface = surfaceTree.root?.leftmostLeaf() - backgroundColor = OSColor(topLeftSurface?.backgroundColor ?? derivedConfig.backgroundColor) - } + // Set the font for the window and tab titles. + if let titleFontName = surfaceConfig.windowTitleFontFamily { + window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize) } else { - backgroundColor = OSColor(self.derivedConfig.backgroundColor) + window.titlebarFont = nil } - window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity) - if (window.isOpaque) { - // Bg color is only synced if we have no transparency. This is because - // the transparency is handled at the surface level (window.backgroundColor - // ignores alpha components) - window.backgroundColor = backgroundColor - - // If there is transparency, calling this will make the titlebar opaque - // so we only call this if we are opaque. - window.updateTabBar() - } + // Call this last in case it uses any of the properties above. + window.syncAppearance(surfaceConfig) } /// Returns the default size of the window. This is contextual based on the focused surface because @@ -884,28 +841,6 @@ class TerminalController: BaseTerminalController { } } - // TODO: remove - if let window = window as? LegacyTerminalWindow, - config.macosTitlebarStyle == "tabs" { - // Handle titlebar tabs config option. Something about what we do while setting up the - // titlebar tabs interferes with the window restore process unless window.tabbingMode - // is set to .preferred, so we set it, and switch back to automatic as soon as we can. - window.tabbingMode = .preferred - window.titlebarTabs = true - DispatchQueue.main.async { - window.tabbingMode = .automatic - } - - if window.hasStyledTabs { - // Set the background color of the window - let backgroundColor = NSColor(config.backgroundColor) - window.backgroundColor = backgroundColor - - // This makes sure our titlebar renders correctly when there is a transparent background - window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) - } - } - // Initialize our content view to the SwiftUI root window.contentView = NSHostingView(rootView: TerminalView( ghostty: self.ghostty, @@ -1110,23 +1045,6 @@ class TerminalController: BaseTerminalController { } //MARK: - TerminalViewDelegate - - override func titleDidChange(to: String) { - super.titleDidChange(to: to) - - guard let window = window as? LegacyTerminalWindow else { return } - - // Custom toolbar-based title used when titlebar tabs are enabled. - if let toolbar = window.toolbar as? TerminalToolbar { - if (window.titlebarTabs || derivedConfig.macosTitlebarStyle == "hidden") { - // Updating the title text as above automatically reveals the - // native title view in macOS 15.0 and above. Since we're using - // a custom view instead, we need to re-hide it. - window.titleVisibility = .hidden - } - toolbar.titleText = to - } - } override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { super.focusedSurfaceDidChange(to: to) diff --git a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift index f2d3b9b85..5f4d6b177 100644 --- a/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/HiddenTitlebarTerminalWindow.swift @@ -19,15 +19,6 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { NotificationCenter.default.removeObserver(self) } - // We override this so that with the hidden titlebar style the titlebar - // area is not draggable. - override var contentLayoutRect: CGRect { - var rect = super.contentLayoutRect - rect.origin.y = 0 - rect.size.height = self.frame.height - return rect - } - /// Apply the hidden titlebar style. private func reapplyHiddenStyle() { styleMask = [ @@ -64,6 +55,26 @@ class HiddenTitlebarTerminalWindow: TerminalWindow { } } + // MARK: NSWindow + + override var title: String { + didSet { + // Updating the title text as above automatically reveals the + // native title view in macOS 15.0 and above. Since we're using + // a custom view instead, we need to re-hide it. + reapplyHiddenStyle() + } + } + + // We override this so that with the hidden titlebar style the titlebar + // area is not draggable. + override var contentLayoutRect: CGRect { + var rect = super.contentLayoutRect + rect.origin.y = 0 + rect.size.height = self.frame.height + return rect + } + // MARK: Notifications @objc private func fullscreenDidExit(_ notification: Notification) { diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib index 1a2a6c192..eb4675657 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalHiddenTitlebar.xib @@ -17,7 +17,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib similarity index 94% rename from macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib rename to macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib index 61ed6f782..deaeded9f 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalLegacy.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarTahoe.xib @@ -13,7 +13,7 @@ - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarVentura.xib similarity index 91% rename from macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib rename to macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarVentura.xib index 779b6e094..bf53a4510 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebar.xib +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalTabsTitlebarVentura.xib @@ -13,11 +13,11 @@ - + - + diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index a0e18d283..f6b53c289 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -10,7 +10,7 @@ class TerminalWindow: NSWindow { static let defaultLevelKey: String = "TerminalDefaultLevel" /// The configuration derived from the Ghostty config so we don't need to rely on references. - private var derivedConfig: DerivedConfig? + private(set) var derivedConfig: DerivedConfig? /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { diff --git a/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift similarity index 90% rename from macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift rename to macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 858b54829..c45e93d79 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TabsTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -1,7 +1,8 @@ import AppKit import SwiftUI -class TabsTitlebarTerminalWindow: TerminalWindow, NSToolbarDelegate { +/// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later. +class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { override func awakeFromNib() { super.awakeFromNib() @@ -49,7 +50,7 @@ extension NSToolbarItem.Identifier { static let title = NSToolbarItem.Identifier("Title") } -extension TabsTitlebarTerminalWindow { +extension TitlebarTabsTahoeTerminalWindow { struct TitleItem: View { var body: some View { Text("HELLO THIS IS A PRETTY LONG TITLE") diff --git a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift similarity index 91% rename from macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift rename to macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 89afbf72f..2f8eb5840 100644 --- a/macos/Sources/Features/Terminal/Window Styles/LegacyTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -1,11 +1,10 @@ import Cocoa -/// The terminal window that we originally had in Ghostty for a long time. Kind of a soupy mess -/// of styling. -class LegacyTerminalWindow: TerminalWindow { +/// Titlebar tabs for macOS 13 to 15. +class TitlebarTabsVenturaTerminalWindow: TerminalWindow { /// This is used to determine if certain elements should be drawn light or dark and should /// be updated whenever the window background color or surrounding elements changes. - var isLightTheme: Bool = false + fileprivate var isLightTheme: Bool = false override var surfaceIsZoomed: Bool { didSet { @@ -32,17 +31,30 @@ class LegacyTerminalWindow: TerminalWindow { } } - // MARK: - Lifecycle + // MARK: NSWindow override func awakeFromNib() { super.awakeFromNib() - if titlebarTabs { - generateToolbar() - } - } + // Handle titlebar tabs config option. Something about what we do while setting up the + // titlebar tabs interferes with the window restore process unless window.tabbingMode + // is set to .preferred, so we set it, and switch back to automatic as soon as we can. + tabbingMode = .preferred + DispatchQueue.main.async { + self.tabbingMode = .automatic + } - // MARK: - NSWindow + titlebarTabs = true + + // This should always be true since our super sets this up. + if let derivedConfig { + // Set the background color of the window + backgroundColor = derivedConfig.backgroundColor + + // This makes sure our titlebar renders correctly when there is a transparent background + titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) + } + } // We only need to set this once, but need to do it after the window has been created in order // to determine if the theme is using a very dark background, in which case we don't want to @@ -145,7 +157,29 @@ class LegacyTerminalWindow: TerminalWindow { } } - // MARK: - Tab Bar Styling + // MARK: Appearance + + override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { + super.syncAppearance(surfaceConfig) + + // Update our window light/darkness based on our updated background color + isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor + + // Update our titlebar color + if let preferredBackgroundColor { + titlebarColor = preferredBackgroundColor + } else if let derivedConfig { + titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) + } + + if (isOpaque) { + // If there is transparency, calling this will make the titlebar opaque + // so we only call this if we are opaque. + updateTabBar() + } + } + + // MARK: Tab Bar Styling // This is true if we should apply styles to the titlebar or tab bar. var hasStyledTabs: Bool { @@ -333,6 +367,18 @@ class LegacyTerminalWindow: TerminalWindow { } } + override var title: String { + didSet { + // Updating the title text as above automatically reveals the + // native title view in macOS 15.0 and above. Since we're using + // a custom view instead, we need to re-hide it. + titleVisibility = .hidden + if let toolbar = toolbar as? TerminalToolbar { + toolbar.titleText = title + } + } + } + // We have to regenerate a toolbar when the titlebar tabs setting changes since our // custom toolbar conditionally generates the items based on this setting. I tried to // invalidate the toolbar items and force a refresh, but as far as I can tell that @@ -559,7 +605,7 @@ fileprivate class WindowDragView: NSView { fileprivate class WindowButtonsBackdropView: NSView { // This must be weak because the window has this view. Otherwise // a retain cycle occurs. - private weak var terminalWindow: LegacyTerminalWindow? + private weak var terminalWindow: TitlebarTabsVenturaTerminalWindow? private let isLightTheme: Bool private let overlayLayer = VibrantLayer() @@ -587,7 +633,7 @@ fileprivate class WindowButtonsBackdropView: NSView { fatalError("init(coder:) has not been implemented") } - init(window: LegacyTerminalWindow) { + init(window: TitlebarTabsVenturaTerminalWindow) { self.terminalWindow = window self.isLightTheme = window.isLightTheme diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 3200608d0..d1dac49a3 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -271,8 +271,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. - if let window = window as? LegacyTerminalWindow, - window.titlebarTabs { + if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { window.titlebarTabs = true } From 70029bf82ae13cc5697b7961db08fa2178740327 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 13:39:17 -0700 Subject: [PATCH 453/642] macos: tahoe terminal tabs shows title --- .../Terminal/TerminalController.swift | 8 ++-- .../Window Styles/TerminalWindow.swift | 7 ++-- .../TitlebarTabsTahoeTerminalWindow.swift | 42 +++++++++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 977a064e0..848617a53 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -18,7 +18,7 @@ class TerminalController: BaseTerminalController { case "tabs": if #available(macOS 26.0, *) { // TODO: Switch to Tahoe when ready - "TerminalTabsTitlebarVentura" + "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } @@ -409,15 +409,15 @@ class TerminalController: BaseTerminalController { // We need to clear any windows beyond this because they have had // a keyEquivalent set previously. guard tab <= 9 else { - window.keyEquivalent2 = "" + window.keyEquivalent = "" continue } let action = "goto_tab:\(tab)" if let equiv = ghostty.config.keyboardShortcut(for: action) { - window.keyEquivalent2 = "\(equiv)" + window.keyEquivalent = "\(equiv)" } else { - window.keyEquivalent2 = "" + window.keyEquivalent = "" } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index f6b53c289..907e0b250 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -82,17 +82,16 @@ class TerminalWindow: NSWindow { // MARK: Tab Key Equivalents - // TODO: rename once Legacy window removes - var keyEquivalent2: String? = nil { + var keyEquivalent: String? = nil { didSet { // When our key equivalent is set, we must update the tab label. - guard let keyEquivalent2 else { + guard let keyEquivalent else { keyEquivalentLabel.attributedStringValue = NSAttributedString() return } keyEquivalentLabel.attributedStringValue = NSAttributedString( - string: "\(keyEquivalent2) ", + string: "\(keyEquivalent) ", attributes: [ .font: NSFont.systemFont(ofSize: NSFont.smallSystemFontSize), .foregroundColor: isKeyWindow ? NSColor.labelColor : NSColor.secondaryLabelColor, diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index c45e93d79..a139b1b62 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -3,6 +3,9 @@ import SwiftUI /// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later. class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { + /// The view model for SwiftUI views + private var viewModel = ViewModel() + override func awakeFromNib() { super.awakeFromNib() @@ -15,7 +18,23 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { toolbar.delegate = self toolbar.centeredItemIdentifiers.insert(.title) self.toolbar = toolbar - //toolbarStyle = .unifiedCompact + toolbarStyle = .unifiedCompact + } + + // MARK: NSWindow + + override var title: String { + didSet { + viewModel.title = title + } + } + + override func update() { + super.update() + + if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { + glass.isHidden = true + } } // MARK: NSToolbarDelegate @@ -34,7 +53,7 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { switch itemIdentifier { case .title: let item = NSToolbarItem(itemIdentifier: .title) - item.view = NSHostingView(rootView: TitleItem()) + item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel)) item.visibilityPriority = .user item.isEnabled = true return item @@ -43,6 +62,11 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { } } + // MARK: SwiftUI + + class ViewModel: ObservableObject { + @Published var title: String = "👻 Ghostty" + } } extension NSToolbarItem.Identifier { @@ -51,9 +75,21 @@ extension NSToolbarItem.Identifier { } extension TitlebarTabsTahoeTerminalWindow { + /// Displays the window title struct TitleItem: View { + @ObservedObject var viewModel: ViewModel + + var title: String { + // An empty title makes this view zero-sized and NSToolbar on macOS + // tahoe just deletes the item when that happens. So we use a space + // instead to ensure there's always some size. + viewModel.title.isEmpty ? " " : viewModel.title + } + var body: some View { - Text("HELLO THIS IS A PRETTY LONG TITLE") + Text(title) + .lineLimit(1) + .truncationMode(.tail) } } } From 658ec2eb6f13a7896720a3e95db87d2b808309d6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 14:33:18 -0700 Subject: [PATCH 454/642] macos: add reset zoom to all window titles --- .../Terminal/BaseTerminalController.swift | 2 + .../Terminal/TerminalController.swift | 4 +- .../Window Styles/TerminalWindow.swift | 62 +++++++++++++++++++ macos/Sources/Helpers/Fullscreen.swift | 12 ++-- 4 files changed, 75 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 849f13b34..bc91b920e 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -758,6 +758,8 @@ class BaseTerminalController: NSWindowController, } } + func fullscreenDidChange() {} + // MARK: Clipboard Confirmation @objc private func onConfirmClipboardRequest(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 848617a53..cff230249 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -145,7 +145,9 @@ class TerminalController: BaseTerminalController { } - func fullscreenDidChange() { + override func fullscreenDidChange() { + super.fullscreenDidChange() + // When our fullscreen state changes, we resync our appearance because some // properties change when fullscreen or not. guard let focusedSurface else { return } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 907e0b250..4221d9ba4 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -9,6 +9,9 @@ class TerminalWindow: NSWindow { /// used by the manual float on top menu item feature. static let defaultLevelKey: String = "TerminalDefaultLevel" + /// The view model for SwiftUI views + private var viewModel = ViewModel() + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig? @@ -19,6 +22,15 @@ class TerminalWindow: NSWindow { // MARK: NSWindow Overrides + override var toolbar: NSToolbar? { + didSet { + DispatchQueue.main.async { + // When we have a toolbar, our SwiftUI view needs to know for layout + self.viewModel.hasToolbar = self.toolbar != nil + } + } + } + override func awakeFromNib() { guard let appDelegate = NSApp.delegate as? AppDelegate else { return } @@ -43,6 +55,18 @@ class TerminalWindow: NSWindow { hideWindowButtons() } + // Create our reset zoom titlebar accessory. + let resetZoomAccessory = NSTitlebarAccessoryViewController() + resetZoomAccessory.layoutAttribute = .right + resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView( + viewModel: viewModel, + action: { [weak self] in + guard let self else { return } + self.terminalController?.splitZoom(self) + })) + addTitlebarAccessoryViewController(resetZoomAccessory) + resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false + // Setup the accessory view for tabs that shows our keyboard shortcuts, // zoomed state, etc. Note I tried to use SwiftUI here but ran into issues // where buttons were not clickable. @@ -115,6 +139,10 @@ class TerminalWindow: NSWindow { // Show/hide our reset zoom button depending on if we're zoomed. // We want to show it if we are zoomed. resetZoomTabButton.isHidden = !surfaceIsZoomed + + DispatchQueue.main.async { + self.viewModel.isSurfaceZoomed = self.surfaceIsZoomed + } } } @@ -313,3 +341,37 @@ class TerminalWindow: NSWindow { } } } + +// MARK: SwiftUI View + +extension TerminalWindow { + class ViewModel: ObservableObject { + @Published var isSurfaceZoomed: Bool = false + @Published var hasToolbar: Bool = false + } + + struct ResetZoomAccessoryView: View { + @ObservedObject var viewModel: ViewModel + let action: () -> Void + + var body: some View { + if viewModel.isSurfaceZoomed { + VStack { + Button(action: action) { + Image("ResetZoom") + .foregroundColor(.accentColor) + } + .buttonStyle(.plain) + .help("Reset Split Zoom") + .frame(width: 20, height: 20) + Spacer() + } + // With a toolbar, the window title is taller, so we need more padding + // to properly align. + .padding(.top, viewModel.hasToolbar ? 10 : 5) + // We always need space at the end of the titlebar + .padding(.trailing, 10) + } + } + } +} diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index d1dac49a3..49cab0756 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -45,10 +45,6 @@ protocol FullscreenDelegate: AnyObject { func fullscreenDidChange() } -extension FullscreenDelegate { - func fullscreenDidChange() {} -} - /// The base class for fullscreen implementations, cannot be used as a FullscreenStyle on its own. class FullscreenBase { let window: NSWindow @@ -269,6 +265,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { window.styleMask = savedState.styleMask window.setFrame(window.frameRect(forContentRect: savedState.contentFrame), display: true) + // Removing the "titled" style also derefs all our accessory view controllers + // so we need to restore those. + for c in savedState.titlebarAccessoryViewControllers { + window.addTitlebarAccessoryViewController(c) + } + // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { @@ -383,6 +385,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { let tabGroupIndex: Int? let contentFrame: NSRect let styleMask: NSWindow.StyleMask + let titlebarAccessoryViewControllers: [NSTitlebarAccessoryViewController] let dock: Bool let menu: Bool @@ -394,6 +397,7 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.tabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) self.contentFrame = window.convertToScreen(contentView.frame) self.styleMask = window.styleMask + self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers self.dock = window.screen?.hasDock ?? false if let cgWindowId = window.cgWindowId { From de40e7ce028b57dc993fafc3c18be863d52437c1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 14:36:33 -0700 Subject: [PATCH 455/642] macos: non-native fullscreen should restore toolbars --- macos/Sources/Helpers/Fullscreen.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index 49cab0756..a2294a0af 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -271,6 +271,10 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { window.addTitlebarAccessoryViewController(c) } + // Removing "titled" also clears our toolbar + window.toolbar = savedState.toolbar + window.toolbarStyle = savedState.toolbarStyle + // This is a hack that I want to remove from this but for now, we need to // fix up the titlebar tabs here before we do everything below. if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { @@ -385,6 +389,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { let tabGroupIndex: Int? let contentFrame: NSRect let styleMask: NSWindow.StyleMask + let toolbar: NSToolbar? + let toolbarStyle: NSWindow.ToolbarStyle let titlebarAccessoryViewControllers: [NSTitlebarAccessoryViewController] let dock: Bool let menu: Bool @@ -397,6 +403,8 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { self.tabGroupIndex = window.tabGroup?.windows.firstIndex(of: window) self.contentFrame = window.convertToScreen(contentView.frame) self.styleMask = window.styleMask + self.toolbar = window.toolbar + self.toolbarStyle = window.toolbarStyle self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers self.dock = window.screen?.hasDock ?? false From 5c8f1948cecf1c4dc63fb53fae0ef6203dd0d691 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 14:42:08 -0700 Subject: [PATCH 456/642] macos: remove the duplicated reset zoom accessory view from legacy --- .../Terminal/TerminalController.swift | 3 +- .../TitlebarTabsVenturaTerminalWindow.swift | 36 ------------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index cff230249..9fbf154bc 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -18,7 +18,8 @@ class TerminalController: BaseTerminalController { case "tabs": if #available(macOS 26.0, *) { // TODO: Switch to Tahoe when ready - "TerminalTabsTitlebarTahoe" + "TerminalTabsTitlebarVentura" + //"TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 2f8eb5840..bc6a66a87 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -6,12 +6,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { /// be updated whenever the window background color or surrounding elements changes. fileprivate var isLightTheme: Bool = false - override var surfaceIsZoomed: Bool { - didSet { - updateResetZoomTitlebarButtonVisibility() - } - } - lazy var titlebarColor: NSColor = backgroundColor { didSet { guard let titlebarContainer else { return } @@ -108,8 +102,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - updateResetZoomTitlebarButtonVisibility() - // The remainder of this function only applies to styled tabs. guard hasStyledTabs else { return } @@ -277,33 +269,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private lazy var resetZoomToolbarButton: NSButton = generateResetZoomButton() - private lazy var resetZoomTitlebarAccessoryViewController: NSTitlebarAccessoryViewController? = { - guard let titlebarContainer else { return nil } - let size = NSSize(width: titlebarContainer.bounds.height, height: titlebarContainer.bounds.height) - let view = NSView(frame: NSRect(origin: .zero, size: size)) - - let button = generateResetZoomButton() - button.frame.origin.x = size.width/2 - button.bounds.width/2 - button.frame.origin.y = size.height/2 - button.bounds.height/2 - view.addSubview(button) - - let titlebarAccessoryViewController = NSTitlebarAccessoryViewController() - titlebarAccessoryViewController.view = view - titlebarAccessoryViewController.layoutAttribute = .right - - return titlebarAccessoryViewController - }() - - private func updateResetZoomTitlebarButtonVisibility() { - guard let tabGroup, let resetZoomTitlebarAccessoryViewController else { return } - - if !titlebarAccessoryViewControllers.contains(resetZoomTitlebarAccessoryViewController) { - addTitlebarAccessoryViewController(resetZoomTitlebarAccessoryViewController) - } - - resetZoomTitlebarAccessoryViewController.view.isHidden = tabGroup.isTabBarVisible ? true : !surfaceIsZoomed - } - private func generateResetZoomButton() -> NSButton { let button = NSButton() button.target = nil @@ -394,7 +359,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { resetZoomItem.view!.widthAnchor.constraint(equalToConstant: 22).isActive = true resetZoomItem.view!.heightAnchor.constraint(equalToConstant: 20).isActive = true } - updateResetZoomTitlebarButtonVisibility() } // For titlebar tabs, we want to hide the separator view so that we get rid From 6ae8bd737ae63bb15ba796512319632054248e21 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 15:11:35 -0700 Subject: [PATCH 457/642] macos: hide the reset zoom titlebar accessory when tab bar is shown --- .../Window Styles/TerminalWindow.swift | 23 ++++++++++++++++++- .../Helpers/Extensions/Array+Extension.swift | 4 ++++ .../Helpers/Extensions/NSView+Extension.swift | 17 +++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 4221d9ba4..fe7293d5b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -104,6 +104,26 @@ class TerminalWindow: NSWindow { } } + override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { + super.addTitlebarAccessoryViewController(childViewController) + + // Tab bar is attached as a titlebar accessory view controller (layout bottom). We + // can detect when it is shown or hidden by overriding add/remove and searching for + // it. This has been verified to work on macOS 12 to 26 + if childViewController.view.contains(className: "NSTabBar") { + viewModel.hasTabBar = true + } + } + + override func removeTitlebarAccessoryViewController(at index: Int) { + if let childViewController = titlebarAccessoryViewControllers[safe: index], + childViewController.view.contains(className: "NSTabBar") { + viewModel.hasTabBar = false + } + + super.removeTitlebarAccessoryViewController(at: index) + } + // MARK: Tab Key Equivalents var keyEquivalent: String? = nil { @@ -348,6 +368,7 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false + @Published var hasTabBar: Bool = false } struct ResetZoomAccessoryView: View { @@ -355,7 +376,7 @@ extension TerminalWindow { let action: () -> Void var body: some View { - if viewModel.isSurfaceZoomed { + if viewModel.isSurfaceZoomed && !viewModel.hasTabBar { VStack { Button(action: action) { Image("ResetZoom") diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index 6f005a349..12f2de43d 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -1,4 +1,8 @@ extension Array { + subscript(safe index: Int) -> Element? { + return indices.contains(index) ? self[index] : nil + } + /// Returns the index before i, with wraparound. Assumes i is a valid index. func indexWrapping(before i: Int) -> Int { if i == 0 { diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index b958130f1..0da84abda 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -42,6 +42,21 @@ extension NSView { return false } + /// Checks if the view contains the given class in its hierarchy. + func contains(className name: String) -> Bool { + if String(describing: type(of: self)) == name { + return true + } + + for subview in subviews { + if subview.contains(className: name) { + return true + } + } + + return false + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { @@ -113,7 +128,7 @@ extension NSView { } /// Returns a string representation of the view hierarchy in a tree-like format. - private func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { + func viewHierarchyDescription(indent: String = "", isLast: Bool = true) -> String { var result = "" // Add the tree branch characters From 5f9967024744827314535026221565763fd4791d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 16:37:26 -0700 Subject: [PATCH 458/642] macos: tahoe titlebar tabs taking shape --- .../Terminal/TerminalController.swift | 4 +- .../Window Styles/TerminalWindow.swift | 36 +++++- .../TitlebarTabsTahoeTerminalWindow.swift | 106 ++++++++++++++++-- .../Helpers/Extensions/NSView+Extension.swift | 10 ++ 4 files changed, 142 insertions(+), 14 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 9fbf154bc..0b0b264d3 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -18,8 +18,8 @@ class TerminalController: BaseTerminalController { case "tabs": if #available(macOS 26.0, *) { // TODO: Switch to Tahoe when ready - "TerminalTabsTitlebarVentura" - //"TerminalTabsTitlebarTahoe" + //"TerminalTabsTitlebarVentura" + "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" } diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index fe7293d5b..032886802 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -110,20 +110,50 @@ class TerminalWindow: NSWindow { // Tab bar is attached as a titlebar accessory view controller (layout bottom). We // can detect when it is shown or hidden by overriding add/remove and searching for // it. This has been verified to work on macOS 12 to 26 - if childViewController.view.contains(className: "NSTabBar") { + if isTabBar(childViewController) { + childViewController.identifier = Self.tabBarIdentifier viewModel.hasTabBar = true } } override func removeTitlebarAccessoryViewController(at index: Int) { - if let childViewController = titlebarAccessoryViewControllers[safe: index], - childViewController.view.contains(className: "NSTabBar") { + if let childViewController = titlebarAccessoryViewControllers[safe: index], isTabBar(childViewController) { viewModel.hasTabBar = false } super.removeTitlebarAccessoryViewController(at: index) } + // MARK: Tab Bar + + /// This identifier is attached to the tab bar view controller when we detect it being + /// added. + private static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") + + func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { + if childViewController.identifier == nil { + // The good case + if childViewController.view.contains(className: "NSTabBar") { + return true + } + + // When a new window is attached to an existing tab group, AppKit adds + // an empty NSView as an accessory view and adds the tab bar later. If + // we're at the bottom and are a single NSView we assume its a tab bar. + if childViewController.layoutAttribute == .bottom && + childViewController.view.className == "NSView" && + childViewController.view.subviews.isEmpty { + return true + } + + return false + } + + // View controllers should be tagged with this as soon as possible to + // increase our accuracy. We do this manually. + return childViewController.identifier == Self.tabBarIdentifier + } + // MARK: Tab Key Equivalents var keyEquivalent: String? = nil { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index a139b1b62..42bcabee7 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -20,7 +20,6 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { self.toolbar = toolbar toolbarStyle = .unifiedCompact } - // MARK: NSWindow override var title: String { @@ -29,11 +28,92 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { } } - override func update() { - super.update() + override var toolbar: NSToolbar? { + didSet{ + guard toolbar != nil else { return } - if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { - glass.isHidden = true + // When a toolbar is added, remove the Liquid Glass look because we're + // abusing the toolbar as a tab bar. + if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { + glass.isHidden = true + } + } + } + + override func becomeMain() { + super.becomeMain() + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { + self.contentView?.printViewHierarchy() + } + } + + // This is called by macOS for native tabbing in order to add the tab bar. We hook into + // this, detect the tab bar being added, and override its behavior. + override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { + // If this is the tab bar then we need to set it up for the titlebar + guard isTabBar(childViewController) else { + super.addTitlebarAccessoryViewController(childViewController) + return + } + + // Some setup needs to happen BEFORE it is added, such as layout. If + // we don't do this before the call below, we'll trigger an AppKit + // assertion. + childViewController.layoutAttribute = .right + + super.addTitlebarAccessoryViewController(childViewController) + + // View model updates must happen on their own ticks + DispatchQueue.main.async { + self.viewModel.hasTabBar = true + } + + // Setup the tab bar to go into the titlebar. + DispatchQueue.main.async { + // HACK: wait a tick before doing anything, to avoid edge cases during startup... :/ + // If we don't do this then on launch windows with restored state with tabs will end + // up with messed up tab bars that don't show all tabs. + let accessoryView = childViewController.view + guard let clipView = accessoryView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } + guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } + guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } + + // The container is the view that we'll constrain our tab bar within. + let container = toolbarView + + // Constrain the accessory clip view (the parent of the accessory view + // usually that clips the children) to the container view. + clipView.translatesAutoresizingMaskIntoConstraints = false + clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 78).isActive = true + clipView.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true + clipView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + clipView.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true + clipView.needsLayout = true + + // Constrain the actual accessory view (the tab bar) to the clip view + // so it takes up the full space. + accessoryView.translatesAutoresizingMaskIntoConstraints = false + accessoryView.leftAnchor.constraint(equalTo: clipView.leftAnchor).isActive = true + accessoryView.rightAnchor.constraint(equalTo: clipView.rightAnchor).isActive = true + accessoryView.topAnchor.constraint(equalTo: clipView.topAnchor).isActive = true + accessoryView.heightAnchor.constraint(equalTo: clipView.heightAnchor).isActive = true + accessoryView.needsLayout = true + } + } + + override func removeTitlebarAccessoryViewController(at index: Int) { + guard let childViewController = titlebarAccessoryViewControllers[safe: index], + isTabBar(childViewController) else { + super.removeTitlebarAccessoryViewController(at: index) + return + } + + super.removeTitlebarAccessoryViewController(at: index) + + // View model needs to be updated on another tick because it + // triggers view updates. + DispatchQueue.main.async { + self.viewModel.hasTabBar = false } } @@ -66,6 +146,7 @@ class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { class ViewModel: ObservableObject { @Published var title: String = "👻 Ghostty" + @Published var hasTabBar: Bool = false } } @@ -83,13 +164,20 @@ extension TitlebarTabsTahoeTerminalWindow { // An empty title makes this view zero-sized and NSToolbar on macOS // tahoe just deletes the item when that happens. So we use a space // instead to ensure there's always some size. - viewModel.title.isEmpty ? " " : viewModel.title + return viewModel.title.isEmpty ? " " : viewModel.title } var body: some View { - Text(title) - .lineLimit(1) - .truncationMode(.tail) + if !viewModel.hasTabBar { + Text(title) + .lineLimit(1) + .truncationMode(.tail) + } else { + // 1x1.gif strikes again! For real: if we render a zero-sized + // view here then the toolbar just disappears our view. I don't + // know. + Color.clear.frame(width: 1, height: 1) + } } } } diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index 0da84abda..b3628d406 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -57,6 +57,16 @@ extension NSView { return false } + /// Finds the superview with the given class name. + func firstSuperview(withClassName name: String) -> NSView? { + guard let superview else { return nil } + if String(describing: type(of: superview)) == name { + return superview + } + + return superview.firstSuperview(withClassName: name) + } + /// Recursively finds and returns the first descendant view that has the given class name. func firstDescendant(withClassName name: String) -> NSView? { for subview in subviews { From d84c30ce71546efc36e671beac1febf5b2a92875 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 18:10:25 -0700 Subject: [PATCH 459/642] macos: titlebar tabs should be transparent --- .../Window Styles/TitlebarTabsTahoeTerminalWindow.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 42bcabee7..ca88322ed 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -2,7 +2,10 @@ import AppKit import SwiftUI /// `macos-titlebar-style = tabs` for macOS 26 (Tahoe) and later. -class TitlebarTabsTahoeTerminalWindow: TerminalWindow, NSToolbarDelegate { +/// +/// This inherits from transparent styling so that the titlebar matches the background color +/// of the window. +class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSToolbarDelegate { /// The view model for SwiftUI views private var viewModel = ViewModel() From 9d9c451b0a893c35c29ba453722cfb337d73817a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 20:03:19 -0700 Subject: [PATCH 460/642] macos: titlebar tabs handle hidden traffic buttons --- .../Terminal/Window Styles/TerminalWindow.swift | 9 ++++++--- .../TitlebarTabsTahoeTerminalWindow.swift | 11 +++++++++-- .../TitlebarTabsVenturaTerminalWindow.swift | 13 +++++-------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 032886802..d9b98695e 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -13,7 +13,7 @@ class TerminalWindow: NSWindow { private var viewModel = ViewModel() /// The configuration derived from the Ghostty config so we don't need to rely on references. - private(set) var derivedConfig: DerivedConfig? + private(set) var derivedConfig: DerivedConfig = .init() /// Gets the terminal controller from the window controller. var terminalController: TerminalController? { @@ -341,8 +341,8 @@ class TerminalWindow: NSWindow { } } - let alpha = derivedConfig?.backgroundOpacity.clamped(to: 0.001...1) ?? 1 - return derivedConfig?.backgroundColor.withAlphaComponent(alpha) + let alpha = derivedConfig.backgroundOpacity.clamped(to: 0.001...1) + return derivedConfig.backgroundColor.withAlphaComponent(alpha) } private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) { @@ -379,15 +379,18 @@ class TerminalWindow: NSWindow { struct DerivedConfig { let backgroundColor: NSColor let backgroundOpacity: Double + let macosWindowButtons: Ghostty.MacOSWindowButtons init() { self.backgroundColor = NSColor.windowBackgroundColor self.backgroundOpacity = 1 + self.macosWindowButtons = .visible } init(_ config: Ghostty.Config) { self.backgroundColor = NSColor(config.backgroundColor) self.backgroundOpacity = config.backgroundOpacity + self.macosWindowButtons = config.macosWindowButtons } } } diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index ca88322ed..3dc505088 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -84,12 +84,19 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool // The container is the view that we'll constrain our tab bar within. let container = toolbarView + // The padding for the tab bar. If we're showing window buttons then + // we need to offset the window buttons. + let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + case .hidden: 0 + case .visible: 70 + } + // Constrain the accessory clip view (the parent of the accessory view // usually that clips the children) to the container view. clipView.translatesAutoresizingMaskIntoConstraints = false - clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: 78).isActive = true + clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: leftPadding).isActive = true clipView.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true - clipView.topAnchor.constraint(equalTo: container.topAnchor).isActive = true + clipView.topAnchor.constraint(equalTo: container.topAnchor, constant: 2).isActive = true clipView.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true clipView.needsLayout = true diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index bc6a66a87..6e19d144d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -40,14 +40,11 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { titlebarTabs = true - // This should always be true since our super sets this up. - if let derivedConfig { - // Set the background color of the window - backgroundColor = derivedConfig.backgroundColor + // Set the background color of the window + backgroundColor = derivedConfig.backgroundColor - // This makes sure our titlebar renders correctly when there is a transparent background - titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) - } + // This makes sure our titlebar renders correctly when there is a transparent background + titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } // We only need to set this once, but need to do it after the window has been created in order @@ -160,7 +157,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // Update our titlebar color if let preferredBackgroundColor { titlebarColor = preferredBackgroundColor - } else if let derivedConfig { + } else { titlebarColor = derivedConfig.backgroundColor.withAlphaComponent(derivedConfig.backgroundOpacity) } From 17ad77b5b0b7b7ead22bcfda11c9250d064d408d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 12 Jun 2025 21:33:40 -0700 Subject: [PATCH 461/642] macos: fix background color of terminal window to match surface --- .../Window Styles/TerminalWindow.swift | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index d9b98695e..a1bb1d86d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -322,22 +322,23 @@ class TerminalWindow: NSWindow { /// change the alpha channel again manually. var preferredBackgroundColor: NSColor? { if let terminalController, !terminalController.surfaceTree.isEmpty { + let surface: Ghostty.SurfaceView? + // If our focused surface borders the top then we prefer its background color if let focusedSurface = terminalController.focusedSurface, let treeRoot = terminalController.surfaceTree.root, let focusedNode = treeRoot.node(view: focusedSurface), - treeRoot.spatial().doesBorder(side: .up, from: focusedNode), - let backgroundcolor = focusedSurface.backgroundColor { - let alpha = focusedSurface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) - return NSColor(backgroundcolor).withAlphaComponent(alpha) + treeRoot.spatial().doesBorder(side: .up, from: focusedNode) { + surface = focusedSurface + } else { + // If it doesn't border the top, we use the top-left leaf + surface = terminalController.surfaceTree.root?.leftmostLeaf() } - // Doesn't border the top or we don't have a focused surface, so - // we try to match the top-left surface. - let topLeftSurface = terminalController.surfaceTree.root?.leftmostLeaf() - if let topLeftBgColor = topLeftSurface?.backgroundColor { - let alpha = topLeftSurface?.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) ?? 1 - return NSColor(topLeftBgColor).withAlphaComponent(alpha) + if let surface { + let backgroundColor = surface.backgroundColor ?? surface.derivedConfig.backgroundColor + let alpha = surface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) + return NSColor(backgroundColor).withAlphaComponent(alpha) } } From 8824d11e1c8d0a3cbafeb0e88dd1b6dd7fa645d0 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Sat, 24 May 2025 17:01:06 -0500 Subject: [PATCH 462/642] linux: add dbus and systemd activation services --- dist/linux/app.desktop | 5 ++++- dist/linux/dbus.service | 4 ++++ dist/linux/systemd.service | 7 +++++++ nix/package.nix | 5 +++++ src/apprt/gtk/App.zig | 23 ++++++++++++++++++++--- src/apprt/gtk/Surface.zig | 9 +++++++++ src/build/GhosttyResources.zig | 10 ++++++++++ 7 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 dist/linux/dbus.service create mode 100644 dist/linux/systemd.service diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index 6e464ea87..bb25eec65 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -1,8 +1,10 @@ [Desktop Entry] +Version=1.0 Name=Ghostty Type=Application Comment=A terminal emulator -Exec=ghostty +TryExec=ghostty +Exec=ghostty %F Icon=com.mitchellh.ghostty Categories=System;TerminalEmulator; Keywords=terminal;tty;pty; @@ -16,6 +18,7 @@ X-TerminalArgTitle=--title= X-TerminalArgAppId=--class= X-TerminalArgDir=--working-directory= X-TerminalArgHold=--wait-after-command +DBusActivatable=true [Desktop Action new-window] Name=New Window diff --git a/dist/linux/dbus.service b/dist/linux/dbus.service new file mode 100644 index 000000000..4d508d168 --- /dev/null +++ b/dist/linux/dbus.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=com.mitchellh.ghostty +SystemdService=com.mitchellh.ghostty.service +Exec=ghostty diff --git a/dist/linux/systemd.service b/dist/linux/systemd.service new file mode 100644 index 000000000..dcc354eff --- /dev/null +++ b/dist/linux/systemd.service @@ -0,0 +1,7 @@ +[Unit] +Description=Ghostty + +[Service] +Type=dbus +BusName=com.mitchellh.ghostty +ExecStart=ghostty diff --git a/nix/package.nix b/nix/package.nix index 08dfd710b..9b793bc4b 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -117,6 +117,11 @@ in mkdir -p "$out/nix-support" + sed -i -e "s@^Exec=.*ghostty@Exec=$out/bin/ghostty@" $out/share/applications/com.mitchellh.ghostty.desktop + sed -i -e "s@^TryExec=.*ghostty@TryExec=$out/bin/ghostty@" $out/share/applications/com.mitchellh.ghostty.desktop + sed -i -e "s@^Exec=.*ghostty@Exec=$out/bin/ghostty@" $out/share/dbus-1/services/com.mitchellh.ghostty.service + sed -i -e "s@^ExecStart=.*ghostty@ExecStart=$out/bin/ghostty@" $out/lib/systemd/user/com.mitchellh.ghostty.service + mkdir -p "$terminfo/share" mv "$terminfo_src" "$terminfo/share/terminfo" ln -sf "$terminfo/share/terminfo" "$terminfo_src" diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 099a051a4..f431c0594 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -400,11 +400,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // This just calls the `activate` signal but its part of the normal startup // routine so we just call it, but only if the config allows it (this allows // for launching Ghostty in the "background" without immediately opening - // a window) + // a window). An initial window will not be immediately created if we were + // launched by D-Bus activation or systemd. D-Bus activation will send it's + // own `activate` or `new-window` signal later. // // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 - if (config.@"initial-window") - gio_app.activate(); + if (config.@"initial-window") switch (config.@"launched-from".?) { + .dbus, .systemd => {}, + else => gio_app.activate(), + }; // Internally, GTK ensures that only one instance of this provider exists in the provider list // for the display. @@ -1678,6 +1682,17 @@ fn gtkActionShowGTKInspector( }; } +fn gtkActionNewWindow( + _: *gio.SimpleAction, + _: ?*glib.Variant, + self: *App, +) callconv(.c) void { + log.info("received new window action", .{}); + _ = self.core_app.mailbox.push(.{ + .new_window = .{}, + }, .{ .forever = {} }); +} + /// This is called to setup the action map that this application supports. /// This should be called only once on startup. fn initActions(self: *App) void { @@ -1697,7 +1712,9 @@ fn initActions(self: *App) void { .{ "reload-config", gtkActionReloadConfig, null }, .{ "present-surface", gtkActionPresentSurface, t }, .{ "show-gtk-inspector", gtkActionShowGTKInspector, null }, + .{ "new-window", gtkActionNewWindow, null }, }; + inline for (actions) |entry| { const action = gio.SimpleAction.new(entry[0], entry[2]); defer action.unref(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1e5b1bfe8..6c3101c3a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -2325,6 +2325,15 @@ pub fn defaultTermioEnv(self: *Surface) !std.process.EnvMap { env.remove("GDK_DISABLE"); env.remove("GSK_RENDERER"); + // Remove some environment variables that are set when Ghostty is launched + // from a `.desktop` file, by D-Bus activation, or systemd. + env.remove("GIO_LAUNCHED_DESKTOP_FILE"); + env.remove("GIO_LAUNCHED_DESKTOP_FILE_PID"); + env.remove("DBUS_STARTER_ADDRESS"); + env.remove("DBUS_STARTER_BUS_TYPE"); + env.remove("INVOCATION_ID"); + env.remove("JOURNAL_STREAM"); + // Unset environment varies set by snaps if we're running in a snap. // This allows Ghostty to further launch additional snaps. if (env.get("SNAP")) |_| { diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 3d6b99a34..13ceeaac3 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -201,6 +201,16 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { b.path("dist/linux/app.desktop"), "share/applications/com.mitchellh.ghostty.desktop", ).step); + // DBus service for DBus activation + try steps.append(&b.addInstallFile( + b.path("dist/linux/dbus.service"), + "share/dbus-1/services/com.mitchellh.ghostty.service", + ).step); + // systemd user service + try steps.append(&b.addInstallFile( + b.path("dist/linux/systemd.service"), + "lib/systemd/user/com.mitchellh.ghostty.service", + ).step); // AppStream metainfo so that application has rich metadata within app stores try steps.append(&b.addInstallFile( From 649cca61ebb4fcfaca4fa99e988fbc2177d0a047 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Jun 2025 14:37:03 -0500 Subject: [PATCH 463/642] gtk: use exhaustive switch for initial-window --- src/apprt/gtk/App.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index f431c0594..7aff9e1d2 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -406,8 +406,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // // https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302 if (config.@"initial-window") switch (config.@"launched-from".?) { + .desktop, .cli => gio_app.activate(), .dbus, .systemd => {}, - else => gio_app.activate(), }; // Internally, GTK ensures that only one instance of this provider exists in the provider list From 57392dfcb5509f6960dc0e5055a242a87da7be34 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Jun 2025 14:38:58 -0500 Subject: [PATCH 464/642] linux: use explicit launched-from config in service files --- dist/linux/app.desktop | 2 +- dist/linux/dbus.service | 2 +- dist/linux/systemd.service | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index bb25eec65..b3f2d0d66 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -4,7 +4,7 @@ Name=Ghostty Type=Application Comment=A terminal emulator TryExec=ghostty -Exec=ghostty %F +Exec=ghostty --launched-from=desktop Icon=com.mitchellh.ghostty Categories=System;TerminalEmulator; Keywords=terminal;tty;pty; diff --git a/dist/linux/dbus.service b/dist/linux/dbus.service index 4d508d168..67a80d5dd 100644 --- a/dist/linux/dbus.service +++ b/dist/linux/dbus.service @@ -1,4 +1,4 @@ [D-BUS Service] Name=com.mitchellh.ghostty SystemdService=com.mitchellh.ghostty.service -Exec=ghostty +Exec=ghostty --launched-from=dbus diff --git a/dist/linux/systemd.service b/dist/linux/systemd.service index dcc354eff..9699dccdf 100644 --- a/dist/linux/systemd.service +++ b/dist/linux/systemd.service @@ -4,4 +4,4 @@ Description=Ghostty [Service] Type=dbus BusName=com.mitchellh.ghostty -ExecStart=ghostty +ExecStart=ghostty --launched-from=systemd From e5c737a423ef373aa94762476445ac9071fad989 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Jun 2025 15:24:32 -0500 Subject: [PATCH 465/642] linux: use launched-from for new window action --- dist/linux/app.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dist/linux/app.desktop b/dist/linux/app.desktop index b3f2d0d66..4475617f9 100644 --- a/dist/linux/app.desktop +++ b/dist/linux/app.desktop @@ -22,4 +22,4 @@ DBusActivatable=true [Desktop Action new-window] Name=New Window -Exec=ghostty +Exec=ghostty --launched-from=desktop From c1d04a61759d04f6adcff22bad3455d095b96c7c Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Mon, 2 Jun 2025 15:42:22 -0500 Subject: [PATCH 466/642] gtk: document effect of changing the class on launching Ghostty --- src/config/Config.zig | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index 2df66ba45..e4222583b 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -894,12 +894,17 @@ title: ?[:0]const u8 = null, /// The setting that will change the application class value. /// /// This controls the class field of the `WM_CLASS` X11 property (when running -/// under X11), and the Wayland application ID (when running under Wayland). +/// under X11), the Wayland application ID (when running under Wayland), and the +/// bus name that Ghostty uses to connect to DBus. /// /// Note that changing this value between invocations will create new, separate /// instances, of Ghostty when running with `gtk-single-instance=true`. See that /// option for more details. /// +/// Changing this value may break launching Ghostty from `.desktop` files, via +/// DBus activation, or systemd user services as the system is expecting Ghostty +/// to connect to DBus using the default `class` when it is launched. +/// /// The class name must follow the requirements defined [in the GTK /// documentation](https://docs.gtk.org/gio/type_func.Application.id_is_valid.html). /// From 00d41239dad768d903790b86d56bd8a3bb1de82b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 11:11:00 -0700 Subject: [PATCH 467/642] macOS: prep the tab bar when system appearance changes --- .../TitlebarTabsTahoeTerminalWindow.swift | 182 +++++++++++++----- 1 file changed, 129 insertions(+), 53 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 3dc505088..ac4fae12a 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -9,20 +9,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool /// The view model for SwiftUI views private var viewModel = ViewModel() - override func awakeFromNib() { - super.awakeFromNib() - - // We must hide the title since we're going to be moving tabs into - // the titlebar which have their own title. - titleVisibility = .hidden - - // Create a toolbar - let toolbar = NSToolbar(identifier: "TerminalToolbar") - toolbar.delegate = self - toolbar.centeredItemIdentifiers.insert(.title) - self.toolbar = toolbar - toolbarStyle = .unifiedCompact + deinit { + tabBarObserver = nil } + // MARK: NSWindow override var title: String { @@ -43,11 +33,27 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool } } + override func awakeFromNib() { + super.awakeFromNib() + + // We must hide the title since we're going to be moving tabs into + // the titlebar which have their own title. + titleVisibility = .hidden + + // Create a toolbar + let toolbar = NSToolbar(identifier: "TerminalToolbar") + toolbar.delegate = self + toolbar.centeredItemIdentifiers.insert(.title) + self.toolbar = toolbar + toolbarStyle = .unifiedCompact + } + override func becomeMain() { super.becomeMain() - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(2)) { - self.contentView?.printViewHierarchy() - } + + // Check if we have a tab bar and set it up if we have to. See the comment + // on this function to learn why we need to check this here. + setupTabBar() } // This is called by macOS for native tabbing in order to add the tab bar. We hook into @@ -66,48 +72,12 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.addTitlebarAccessoryViewController(childViewController) - // View model updates must happen on their own ticks - DispatchQueue.main.async { - self.viewModel.hasTabBar = true - } - // Setup the tab bar to go into the titlebar. DispatchQueue.main.async { // HACK: wait a tick before doing anything, to avoid edge cases during startup... :/ // If we don't do this then on launch windows with restored state with tabs will end // up with messed up tab bars that don't show all tabs. - let accessoryView = childViewController.view - guard let clipView = accessoryView.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } - guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } - guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } - - // The container is the view that we'll constrain our tab bar within. - let container = toolbarView - - // The padding for the tab bar. If we're showing window buttons then - // we need to offset the window buttons. - let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { - case .hidden: 0 - case .visible: 70 - } - - // Constrain the accessory clip view (the parent of the accessory view - // usually that clips the children) to the container view. - clipView.translatesAutoresizingMaskIntoConstraints = false - clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: leftPadding).isActive = true - clipView.rightAnchor.constraint(equalTo: container.rightAnchor).isActive = true - clipView.topAnchor.constraint(equalTo: container.topAnchor, constant: 2).isActive = true - clipView.heightAnchor.constraint(equalTo: container.heightAnchor).isActive = true - clipView.needsLayout = true - - // Constrain the actual accessory view (the tab bar) to the clip view - // so it takes up the full space. - accessoryView.translatesAutoresizingMaskIntoConstraints = false - accessoryView.leftAnchor.constraint(equalTo: clipView.leftAnchor).isActive = true - accessoryView.rightAnchor.constraint(equalTo: clipView.rightAnchor).isActive = true - accessoryView.topAnchor.constraint(equalTo: clipView.topAnchor).isActive = true - accessoryView.heightAnchor.constraint(equalTo: clipView.heightAnchor).isActive = true - accessoryView.needsLayout = true + self.setupTabBar() } } @@ -120,11 +90,117 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool super.removeTitlebarAccessoryViewController(at: index) + removeTabBar() + } + + // MARK: Tab Bar Setup + + private var tabBarObserver: NSObjectProtocol? { + didSet { + // When we change this we want to clear our old observer + guard let oldValue else { return } + NotificationCenter.default.removeObserver(oldValue) + } + } + + /// Take the NSTabBar that is on the window and convert it into titlebar tabs. + /// + /// Let me explain more background on what is happening here. When a tab bar is created, only the + /// main window actually has an NSTabBar. When an NSWindow in the tab group gains main, AppKit + /// creates/moves (unsure which) the NSTabBar for it and shows it. When it loses main, the tab bar + /// is removed from the view hierarchy. + /// + /// We can't detect this via `addTitlebarAccessoryViewController` because AppKit + /// _always_ creates an accessory view controller for every window in the tab group, but puts a + /// zero-sized NSView into it (that the tab bar is then attached to later). + /// + /// The best way I've found to detect this is to search for and setup the tab bar anytime the + /// window gains focus. There are probably edge cases to check but to resolve all this I made + /// this function which is idempotent to call. + /// + /// There are more scenarios to look out for and they're documented within the method. + func setupTabBar() { + // We only want to setup the observer once + guard tabBarObserver == nil else { return } + + // Find our tab bar. If it doesn't exist we don't do anything. + guard let tabBar = contentView?.rootView.firstDescendant(withClassName: "NSTabBar") else { return } + + // View model updates must happen on their own ticks. + DispatchQueue.main.async { + self.viewModel.hasTabBar = true + } + + // Find our clip view + guard let clipView = tabBar.firstSuperview(withClassName: "NSTitlebarAccessoryClipView") else { return } + guard let accessoryView = clipView.subviews[safe: 0] else { return } + guard let titlebarView = clipView.firstSuperview(withClassName: "NSTitlebarView") else { return } + guard let toolbarView = titlebarView.firstDescendant(withClassName: "NSToolbarView") else { return } + + // The container is the view that we'll constrain our tab bar within. + let container = toolbarView + + // The padding for the tab bar. If we're showing window buttons then + // we need to offset the window buttons. + let leftPadding: CGFloat = switch(self.derivedConfig.macosWindowButtons) { + case .hidden: 0 + case .visible: 70 + } + + // Constrain the accessory clip view (the parent of the accessory view + // usually that clips the children) to the container view. + clipView.translatesAutoresizingMaskIntoConstraints = false + accessoryView.translatesAutoresizingMaskIntoConstraints = false + + // Setup all our constraints + NSLayoutConstraint.activate([ + clipView.leftAnchor.constraint(equalTo: container.leftAnchor, constant: leftPadding), + clipView.rightAnchor.constraint(equalTo: container.rightAnchor), + clipView.topAnchor.constraint(equalTo: container.topAnchor, constant: 2), + clipView.heightAnchor.constraint(equalTo: container.heightAnchor), + accessoryView.leftAnchor.constraint(equalTo: clipView.leftAnchor), + accessoryView.rightAnchor.constraint(equalTo: clipView.rightAnchor), + accessoryView.topAnchor.constraint(equalTo: clipView.topAnchor), + accessoryView.heightAnchor.constraint(equalTo: clipView.heightAnchor), + ]) + + clipView.needsLayout = true + accessoryView.needsLayout = true + + // We need to setup an observer for the NSTabBar frame. When we change system + // appearance, the tab bar temporarily becomes width/height 0 and breaks all our + // constraints and AppKit responds by nuking the whole tab bar cause it doesn't + // know what to do with it. We need to detect this before bad things happen. + tabBar.postsFrameChangedNotifications = true + tabBarObserver = NotificationCenter.default.addObserver( + forName: NSView.frameDidChangeNotification, + object: tabBar, + queue: .main + ) { [weak self] _ in + guard let self else { return } + + // Check if either width or height is zero + guard tabBar.frame.size.width == 0 || tabBar.frame.size.height == 0 else { return } + + // Remove the observer so we can call setup again. + self.tabBarObserver = nil + + // Wait a tick to let the new tab bars appear and then set them up. + DispatchQueue.main.async { + self.setupTabBar() + } + } + } + + func removeTabBar() { // View model needs to be updated on another tick because it // triggers view updates. DispatchQueue.main.async { self.viewModel.hasTabBar = false } + + // Clear our observations + self.tabBarObserver = nil } // MARK: NSToolbarDelegate From b1b74d3421693fa9d7042fd9167603ade3619aa3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 12:25:21 -0700 Subject: [PATCH 468/642] comments --- .../TitlebarTabsTahoeTerminalWindow.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index ac4fae12a..145c37c59 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -25,8 +25,8 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool didSet{ guard toolbar != nil else { return } - // When a toolbar is added, remove the Liquid Glass look because we're - // abusing the toolbar as a tab bar. + // When a toolbar is added, remove the Liquid Glass look to have a cleaner + // appearance for our custom titlebar tabs. if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { glass.isHidden = true } @@ -110,9 +110,9 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool /// creates/moves (unsure which) the NSTabBar for it and shows it. When it loses main, the tab bar /// is removed from the view hierarchy. /// - /// We can't detect this via `addTitlebarAccessoryViewController` because AppKit - /// _always_ creates an accessory view controller for every window in the tab group, but puts a - /// zero-sized NSView into it (that the tab bar is then attached to later). + /// We can't reliably detect this via `addTitlebarAccessoryViewController` because AppKit + /// creates an accessory view controller for every window in the tab group, but only attaches + /// the actual NSTabBar to the main window's accessory view. /// /// The best way I've found to detect this is to search for and setup the tab bar anytime the /// window gains focus. There are probably edge cases to check but to resolve all this I made @@ -167,10 +167,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool clipView.needsLayout = true accessoryView.needsLayout = true - // We need to setup an observer for the NSTabBar frame. When we change system - // appearance, the tab bar temporarily becomes width/height 0 and breaks all our - // constraints and AppKit responds by nuking the whole tab bar cause it doesn't - // know what to do with it. We need to detect this before bad things happen. + // Setup an observer for the NSTabBar frame. When system appearance changes or + // other events occur, the tab bar can temporarily become zero-sized. When this + // happens, we need to remove our custom constraints and re-apply them once the + // tab bar has proper dimensions again to avoid constraint conflicts. tabBar.postsFrameChangedNotifications = true tabBarObserver = NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, From 59812c3b02ec6fffe0d9febf3b234faf27bd4dbc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 12:27:44 -0700 Subject: [PATCH 469/642] macos: remove TODO --- macos/Sources/Features/Terminal/TerminalController.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 0b0b264d3..03a4e548e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -17,8 +17,6 @@ class TerminalController: BaseTerminalController { case "transparent": "TerminalTransparentTitlebar" case "tabs": if #available(macOS 26.0, *) { - // TODO: Switch to Tahoe when ready - //"TerminalTabsTitlebarVentura" "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" From f7f0514b9ff8065e7e0b75cf0e05e75d1ef00642 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 13:14:14 -0700 Subject: [PATCH 470/642] macos: move old toolbar into ventura file --- macos/Ghostty.xcodeproj/project.pbxproj | 4 - .../Features/Terminal/TerminalToolbar.swift | 130 ----------------- .../TitlebarTabsVenturaTerminalWindow.swift | 131 ++++++++++++++++++ 3 files changed, 131 insertions(+), 134 deletions(-) delete mode 100644 macos/Sources/Features/Terminal/TerminalToolbar.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index cd0c17f9b..e9c02ef41 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -120,7 +120,6 @@ A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; - AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; }; @@ -239,7 +238,6 @@ A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; - AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalToolbar.swift; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; C1F26EA62B738B9900404083 /* NSView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSView+Extension.swift"; sourceTree = ""; }; C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = ""; }; @@ -507,7 +505,6 @@ A596309B2AEE1C9E00D64628 /* TerminalController.swift */, A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */, A596309D2AEE1D6C00D64628 /* TerminalView.swift */, - AEF9CE232B6AD07A0017E195 /* TerminalToolbar.swift */, A535B9D9299C569B0017E2E4 /* ErrorView.swift */, A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */, ); @@ -799,7 +796,6 @@ A596309E2AEE1D6C00D64628 /* TerminalView.swift in Sources */, A58636662DEF964100E04A10 /* TerminalSplitTreeView.swift in Sources */, A52FFF592CAA4FF3000C6A5B /* Fullscreen.swift in Sources */, - AEF9CE242B6AD07A0017E195 /* TerminalToolbar.swift in Sources */, C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */, A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */, A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalToolbar.swift b/macos/Sources/Features/Terminal/TerminalToolbar.swift deleted file mode 100644 index 9da14562c..000000000 --- a/macos/Sources/Features/Terminal/TerminalToolbar.swift +++ /dev/null @@ -1,130 +0,0 @@ -import Cocoa - -// Custom NSToolbar subclass that displays a centered window title, -// in order to accommodate the titlebar tabs feature. -class TerminalToolbar: NSToolbar, NSToolbarDelegate { - private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") - - var titleText: String { - get { - titleTextField.stringValue - } - - set { - titleTextField.stringValue = newValue - } - } - - var titleFont: NSFont? { - get { - titleTextField.font - } - - set { - titleTextField.font = newValue - } - } - - var titleIsHidden: Bool { - get { - titleTextField.isHidden - } - - set { - titleTextField.isHidden = newValue - } - } - - override init(identifier: NSToolbar.Identifier) { - super.init(identifier: identifier) - - delegate = self - centeredItemIdentifiers.insert(.titleText) - } - - func toolbar(_ toolbar: NSToolbar, - itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, - willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { - var item: NSToolbarItem - - switch itemIdentifier { - case .titleText: - item = NSToolbarItem(itemIdentifier: .titleText) - item.view = self.titleTextField - item.visibilityPriority = .user - - // This ensures the title text field doesn't disappear when shrinking the view - self.titleTextField.translatesAutoresizingMaskIntoConstraints = false - self.titleTextField.setContentHuggingPriority(.defaultLow, for: .horizontal) - self.titleTextField.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - - // Add constraints to the toolbar item's view - NSLayoutConstraint.activate([ - // Set the height constraint to match the toolbar's height - self.titleTextField.heightAnchor.constraint(equalToConstant: 22), // Adjust as needed - ]) - - item.isEnabled = true - case .resetZoom: - item = NSToolbarItem(itemIdentifier: .resetZoom) - default: - item = NSToolbarItem(itemIdentifier: itemIdentifier) - } - - return item - } - - func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.titleText, .flexibleSpace, .space, .resetZoom] - } - - func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - // These space items are here to ensure that the title remains centered when it starts - // getting smaller than the max size so starts clipping. Lucky for us, two of the - // built-in spacers plus the un-zoom button item seems to exactly match the space - // on the left that's reserved for the window buttons. - return [.flexibleSpace, .titleText, .flexibleSpace] - } -} - -/// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. -fileprivate class CenteredDynamicLabel: NSTextField { - override func viewDidMoveToSuperview() { - // Configure the text field - isEditable = false - isBordered = false - drawsBackground = false - alignment = .center - lineBreakMode = .byTruncatingTail - cell?.truncatesLastVisibleLine = true - - // Use Auto Layout - translatesAutoresizingMaskIntoConstraints = false - - // Set content hugging and compression resistance priorities - setContentHuggingPriority(.defaultLow, for: .horizontal) - setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) - } - - // Vertically center the text - override func draw(_ dirtyRect: NSRect) { - guard let attributedString = self.attributedStringValue.mutableCopy() as? NSMutableAttributedString else { - super.draw(dirtyRect) - return - } - - let textSize = attributedString.size() - - let yOffset = (self.bounds.height - textSize.height) / 2 - 1 // -1 to center it better - - let centeredRect = NSRect(x: self.bounds.origin.x, y: self.bounds.origin.y + yOffset, - width: self.bounds.width, height: textSize.height) - - attributedString.draw(in: centeredRect) - } -} - -extension NSToolbarItem.Identifier { - static let resetZoom = NSToolbarItem.Identifier("ResetZoom") - static let titleText = NSToolbarItem.Identifier("TitleText") -} diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 6e19d144d..20280b982 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -609,3 +609,134 @@ fileprivate class WindowButtonsBackdropView: NSView { layer?.addSublayer(overlayLayer) } } + +// MARK: Toolbar + +// Custom NSToolbar subclass that displays a centered window title, +// in order to accommodate the titlebar tabs feature. +fileprivate class TerminalToolbar: NSToolbar, NSToolbarDelegate { + private let titleTextField = CenteredDynamicLabel(labelWithString: "👻 Ghostty") + + var titleText: String { + get { + titleTextField.stringValue + } + + set { + titleTextField.stringValue = newValue + } + } + + var titleFont: NSFont? { + get { + titleTextField.font + } + + set { + titleTextField.font = newValue + } + } + + var titleIsHidden: Bool { + get { + titleTextField.isHidden + } + + set { + titleTextField.isHidden = newValue + } + } + + override init(identifier: NSToolbar.Identifier) { + super.init(identifier: identifier) + + delegate = self + centeredItemIdentifiers.insert(.titleText) + } + + func toolbar(_ toolbar: NSToolbar, + itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, + willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { + var item: NSToolbarItem + + switch itemIdentifier { + case .titleText: + item = NSToolbarItem(itemIdentifier: .titleText) + item.view = self.titleTextField + item.visibilityPriority = .user + + // This ensures the title text field doesn't disappear when shrinking the view + self.titleTextField.translatesAutoresizingMaskIntoConstraints = false + self.titleTextField.setContentHuggingPriority(.defaultLow, for: .horizontal) + self.titleTextField.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + + // Add constraints to the toolbar item's view + NSLayoutConstraint.activate([ + // Set the height constraint to match the toolbar's height + self.titleTextField.heightAnchor.constraint(equalToConstant: 22), // Adjust as needed + ]) + + item.isEnabled = true + case .resetZoom: + item = NSToolbarItem(itemIdentifier: .resetZoom) + default: + item = NSToolbarItem(itemIdentifier: itemIdentifier) + } + + return item + } + + func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + return [.titleText, .flexibleSpace, .space, .resetZoom] + } + + func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { + // These space items are here to ensure that the title remains centered when it starts + // getting smaller than the max size so starts clipping. Lucky for us, two of the + // built-in spacers plus the un-zoom button item seems to exactly match the space + // on the left that's reserved for the window buttons. + return [.flexibleSpace, .titleText, .flexibleSpace] + } +} + +/// A label that expands to fit whatever text you put in it and horizontally centers itself in the current window. +fileprivate class CenteredDynamicLabel: NSTextField { + override func viewDidMoveToSuperview() { + // Configure the text field + isEditable = false + isBordered = false + drawsBackground = false + alignment = .center + lineBreakMode = .byTruncatingTail + cell?.truncatesLastVisibleLine = true + + // Use Auto Layout + translatesAutoresizingMaskIntoConstraints = false + + // Set content hugging and compression resistance priorities + setContentHuggingPriority(.defaultLow, for: .horizontal) + setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + } + + // Vertically center the text + override func draw(_ dirtyRect: NSRect) { + guard let attributedString = self.attributedStringValue.mutableCopy() as? NSMutableAttributedString else { + super.draw(dirtyRect) + return + } + + let textSize = attributedString.size() + + let yOffset = (self.bounds.height - textSize.height) / 2 - 1 // -1 to center it better + + let centeredRect = NSRect(x: self.bounds.origin.x, y: self.bounds.origin.y + yOffset, + width: self.bounds.width, height: textSize.height) + + attributedString.draw(in: centeredRect) + } +} + +extension NSToolbarItem.Identifier { + static let resetZoom = NSToolbarItem.Identifier("ResetZoom") + static let titleText = NSToolbarItem.Identifier("TitleText") +} From a7df90ee5529b2cccac5651c57661dac1f350b99 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 13:36:03 -0700 Subject: [PATCH 471/642] macos: remove split zoom accessory when tabs appear --- .../Window Styles/TerminalWindow.swift | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index a1bb1d86d..d588a5944 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -12,6 +12,9 @@ class TerminalWindow: NSWindow { /// The view model for SwiftUI views private var viewModel = ViewModel() + /// Reset split zoom button in titlebar + private let resetZoomAccessory = NSTitlebarAccessoryViewController() + /// The configuration derived from the Ghostty config so we don't need to rely on references. private(set) var derivedConfig: DerivedConfig = .init() @@ -56,7 +59,6 @@ class TerminalWindow: NSWindow { } // Create our reset zoom titlebar accessory. - let resetZoomAccessory = NSTitlebarAccessoryViewController() resetZoomAccessory.layoutAttribute = .right resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView( viewModel: viewModel, @@ -94,6 +96,18 @@ class TerminalWindow: NSWindow { resetZoomTabButton.contentTintColor = .secondaryLabelColor } + override func becomeMain() { + super.becomeMain() + + // Its possible we miss the accessory titlebar call so we check again + // whenever the window becomes main. Both of these are idempotent. + if hasTabBar { + tabBarDidAppear() + } else { + tabBarDidDisappear() + } + } + override func mergeAllWindows(_ sender: Any?) { super.mergeAllWindows(sender) @@ -112,13 +126,13 @@ class TerminalWindow: NSWindow { // it. This has been verified to work on macOS 12 to 26 if isTabBar(childViewController) { childViewController.identifier = Self.tabBarIdentifier - viewModel.hasTabBar = true + tabBarDidAppear() } } override func removeTitlebarAccessoryViewController(at index: Int) { if let childViewController = titlebarAccessoryViewControllers[safe: index], isTabBar(childViewController) { - viewModel.hasTabBar = false + tabBarDidDisappear() } super.removeTitlebarAccessoryViewController(at: index) @@ -130,6 +144,11 @@ class TerminalWindow: NSWindow { /// added. private static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") + /// Returns true if there is a tab bar visible on this window. + var hasTabBar: Bool { + contentView?.firstViewFromRoot(withClassName: "NSTabBar") != nil + } + func isTabBar(_ childViewController: NSTitlebarAccessoryViewController) -> Bool { if childViewController.identifier == nil { // The good case @@ -154,6 +173,28 @@ class TerminalWindow: NSWindow { return childViewController.identifier == Self.tabBarIdentifier } + /// Ensures we only run didAppear/didDisappear once per state. + private var tabBarDidAppearRan = false + + private func tabBarDidAppear() { + guard !tabBarDidAppearRan else { return } + tabBarDidAppearRan = true + + // Remove our reset zoom accessory. For some reason having a SwiftUI + // titlebar accessory causes our content view scaling to be wrong. + // Removing it fixes it, we just need to remember to add it again later. + if let idx = titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) { + removeTitlebarAccessoryViewController(at: idx) + } + } + + private func tabBarDidDisappear() { + guard tabBarDidAppearRan else { return } + tabBarDidAppearRan = false + + addTitlebarAccessoryViewController(resetZoomAccessory) + } + // MARK: Tab Key Equivalents var keyEquivalent: String? = nil { @@ -402,7 +443,6 @@ extension TerminalWindow { class ViewModel: ObservableObject { @Published var isSurfaceZoomed: Bool = false @Published var hasToolbar: Bool = false - @Published var hasTabBar: Bool = false } struct ResetZoomAccessoryView: View { @@ -410,7 +450,7 @@ extension TerminalWindow { let action: () -> Void var body: some View { - if viewModel.isSurfaceZoomed && !viewModel.hasTabBar { + if viewModel.isSurfaceZoomed { VStack { Button(action: action) { Image("ResetZoom") From 8cfc904c0c86a8e87cb6f5234aa6fe55e4bd2d53 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 14:38:07 -0700 Subject: [PATCH 472/642] macos: fix up some sequoia regressions --- .../Window Styles/TerminalWindow.swift | 27 +++++++++++-------- macos/Sources/Helpers/Fullscreen.swift | 12 +++------ 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index d588a5944..74181089d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -173,13 +173,7 @@ class TerminalWindow: NSWindow { return childViewController.identifier == Self.tabBarIdentifier } - /// Ensures we only run didAppear/didDisappear once per state. - private var tabBarDidAppearRan = false - private func tabBarDidAppear() { - guard !tabBarDidAppearRan else { return } - tabBarDidAppearRan = true - // Remove our reset zoom accessory. For some reason having a SwiftUI // titlebar accessory causes our content view scaling to be wrong. // Removing it fixes it, we just need to remember to add it again later. @@ -189,10 +183,11 @@ class TerminalWindow: NSWindow { } private func tabBarDidDisappear() { - guard tabBarDidAppearRan else { return } - tabBarDidAppearRan = false - - addTitlebarAccessoryViewController(resetZoomAccessory) + if styleMask.contains(.titled) { + if titlebarAccessoryViewControllers.firstIndex(of: resetZoomAccessory) == nil { + addTitlebarAccessoryViewController(resetZoomAccessory) + } + } } // MARK: Tab Key Equivalents @@ -448,6 +443,16 @@ extension TerminalWindow { struct ResetZoomAccessoryView: View { @ObservedObject var viewModel: ViewModel let action: () -> Void + + // The padding from the top that the view appears. This was all just manually + // measured based on the OS. + var topPadding: CGFloat { + if #available(macOS 26.0, *) { + return viewModel.hasToolbar ? 10 : 5 + } else { + return viewModel.hasToolbar ? 9 : 4 + } + } var body: some View { if viewModel.isSurfaceZoomed { @@ -463,7 +468,7 @@ extension TerminalWindow { } // With a toolbar, the window title is taller, so we need more padding // to properly align. - .padding(.top, viewModel.hasToolbar ? 10 : 5) + .padding(.top, topPadding) // We always need space at the end of the titlebar .padding(.trailing, 10) } diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index a2294a0af..d78775a1d 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -268,19 +268,15 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Removing the "titled" style also derefs all our accessory view controllers // so we need to restore those. for c in savedState.titlebarAccessoryViewControllers { - window.addTitlebarAccessoryViewController(c) + if window.titlebarAccessoryViewControllers.firstIndex(of: c) == nil { + window.addTitlebarAccessoryViewController(c) + } } // Removing "titled" also clears our toolbar window.toolbar = savedState.toolbar window.toolbarStyle = savedState.toolbarStyle - - // This is a hack that I want to remove from this but for now, we need to - // fix up the titlebar tabs here before we do everything below. - if let window = window as? TitlebarTabsVenturaTerminalWindow, window.titlebarTabs { - window.titlebarTabs = true - } - + // If the window was previously in a tab group that isn't empty now, // we re-add it. We have to do this because our process of doing non-native // fullscreen removes the window from the tab group. From 1388c277d5978094994b3b928b9d79a950974989 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 14:43:01 -0700 Subject: [PATCH 473/642] macos: sequoia should use same tab bar identifier as TerminalWindow --- .../Terminal/Window Styles/TerminalWindow.swift | 2 +- .../TitlebarTabsVenturaTerminalWindow.swift | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 74181089d..f9dfb9591 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -142,7 +142,7 @@ class TerminalWindow: NSWindow { /// This identifier is attached to the tab bar view controller when we detect it being /// added. - private static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") + static let tabBarIdentifier: NSUserInterfaceItemIdentifier = .init("_ghosttyTabBar") /// Returns true if there is a tab bar visible on this window. var hasTabBar: Bool { diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index 20280b982..a236df107 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -196,8 +196,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // We can only update titlebar tabs if there is a titlebar. Without the // styleMask check the app will crash (issue #1876) if titlebarTabs && styleMask.contains(.titled) { - guard let tabBarAccessoryViewController = titlebarAccessoryViewControllers.first(where: { $0.identifier == Self.TabBarController}) else { return } - + guard let tabBarAccessoryViewController = titlebarAccessoryViewControllers.first(where: { $0.identifier == Self.tabBarIdentifier}) else { return } tabBarAccessoryViewController.layoutAttribute = .right pushTabsToTitlebar(tabBarAccessoryViewController) } @@ -314,9 +313,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { private var windowDragHandle: WindowDragView? = nil - // The tab bar controller ID from macOS - static private let TabBarController = NSUserInterfaceItemIdentifier("_tabBarController") - // Used by the window controller to enable/disable titlebar tabs. var titlebarTabs = false { didSet { @@ -384,10 +380,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // This is called by macOS for native tabbing in order to add the tab bar. We hook into // this, detect the tab bar being added, and override its behavior. override func addTitlebarAccessoryViewController(_ childViewController: NSTitlebarAccessoryViewController) { - let isTabBar = self.titlebarTabs && ( - childViewController.layoutAttribute == .bottom || - childViewController.identifier == Self.TabBarController - ) + let isTabBar = self.titlebarTabs && isTabBar(childViewController) if (isTabBar) { // Ensure it has the right layoutAttribute to force it next to our titlebar @@ -399,7 +392,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // Mark the controller for future reference so we can easily find it. Otherwise // the tab bar has no ID by default. - childViewController.identifier = Self.TabBarController + childViewController.identifier = Self.tabBarIdentifier } super.addTitlebarAccessoryViewController(childViewController) @@ -410,7 +403,7 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } override func removeTitlebarAccessoryViewController(at index: Int) { - let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.TabBarController + let isTabBar = titlebarAccessoryViewControllers[index].identifier == Self.tabBarIdentifier super.removeTitlebarAccessoryViewController(at: index) if (isTabBar) { resetCustomTabBarViews() From ac4b0dcac0b5b864a212ddf2c08280b72c5a8016 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 14:57:49 -0700 Subject: [PATCH 474/642] macos: fix transparent tabs on sequoia --- .../TitlebarTabsVenturaTerminalWindow.swift | 16 ------- .../TransparentTitlebarTerminalWindow.swift | 47 +++++++++++++++++-- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift index a236df107..99111b55b 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsVenturaTerminalWindow.swift @@ -99,9 +99,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { } } - // The remainder of this function only applies to styled tabs. - guard hasStyledTabs else { return } - titlebarSeparatorStyle = tabbedWindows != nil && !titlebarTabs ? .line : .none if titlebarTabs { hideToolbarOverflowButton() @@ -170,19 +167,6 @@ class TitlebarTabsVenturaTerminalWindow: TerminalWindow { // MARK: Tab Bar Styling - // This is true if we should apply styles to the titlebar or tab bar. - var hasStyledTabs: Bool { - // If we have titlebar tabs then we always style. - guard !titlebarTabs else { return true } - - // We style the tabs if they're transparent - return transparentTabs - } - - // Set to true if the background color should bleed through the titlebar/tab bar. - // This only applies to non-titlebar tabs. - var transparentTabs: Bool = false - var hasVeryDarkBackground: Bool { backgroundColor.luminance < 0.05 } diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index f949b6094..94e938326 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -11,6 +11,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { /// KVO observation for tab group window changes. private var tabGroupWindowsObservation: NSKeyValueObservation? private var tabBarVisibleObservation: NSKeyValueObservation? + + deinit { + tabGroupWindowsObservation?.invalidate() + tabBarVisibleObservation?.invalidate() + } + + // MARK: NSWindow override func awakeFromNib() { super.awakeFromNib() @@ -19,11 +26,6 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // to learn why we need KVO. setupKVO() } - - deinit { - tabGroupWindowsObservation?.invalidate() - tabBarVisibleObservation?.invalidate() - } override func becomeMain() { super.becomeMain() @@ -40,6 +42,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { } } } + + override func update() { + super.update() + + // On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our + // titlebar to be truly transparent. + if #unavailable(macOS 26.0) { + hideEffectView() + } + } // MARK: Appearance @@ -86,6 +98,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { guard let titlebarContainer else { return } titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor + effectViewIsHidden = false } // MARK: View Finders @@ -149,4 +162,28 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { self.syncAppearance(lastSurfaceConfig) } } + + // MARK: macOS 13 to 15 + + // We only need to set this once, but need to do it after the window has been created in order + // to determine if the theme is using a very dark background, in which case we don't want to + // remove the effect view if the default tab bar is being used since the effect created in + // `updateTabsForVeryDarkBackgrounds` creates a confusing visual design. + private var effectViewIsHidden = false + + private func hideEffectView() { + guard !effectViewIsHidden else { return } + + // By hiding the visual effect view, we allow the window's (or titlebar's in this case) + // background color to show through. If we were to set `titlebarAppearsTransparent` to true + // the selected tab would look fine, but the unselected ones and new tab button backgrounds + // would be an opaque color. When the titlebar isn't transparent, however, the system applies + // a compositing effect to the unselected tab backgrounds, which makes them blend with the + // titlebar's/window's background. + if let effectView = titlebarContainer?.descendants(withClassName: "NSVisualEffectView").first { + effectView.isHidden = true + } + + effectViewIsHidden = true + } } From 1b6142b271b72048f82648920f6de0b2b48960f5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 15:02:05 -0700 Subject: [PATCH 475/642] macos: don't restore tab bar with non-native fs --- macos/Sources/Helpers/Fullscreen.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/macos/Sources/Helpers/Fullscreen.swift b/macos/Sources/Helpers/Fullscreen.swift index d78775a1d..f3940a9aa 100644 --- a/macos/Sources/Helpers/Fullscreen.swift +++ b/macos/Sources/Helpers/Fullscreen.swift @@ -268,6 +268,12 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle { // Removing the "titled" style also derefs all our accessory view controllers // so we need to restore those. for c in savedState.titlebarAccessoryViewControllers { + // Restoring the tab bar causes all sorts of problems. Its best to just ignore it, + // even though this is kind of a hack. + if let window = window as? TerminalWindow, window.isTabBar(c) { + continue + } + if window.titlebarAccessoryViewControllers.firstIndex(of: c) == nil { window.addTitlebarAccessoryViewController(c) } From 928603c23e1e59717e1c042e3d39487066d9f12f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 13 Jun 2025 20:20:49 -0700 Subject: [PATCH 476/642] macos: use a runtime liquid glass check for our Tahoe styling --- macos/Ghostty.xcodeproj/project.pbxproj | 8 ++-- .../Terminal/TerminalController.swift | 2 +- .../Window Styles/TerminalWindow.swift | 2 +- .../TransparentTitlebarTerminalWindow.swift | 4 +- macos/Sources/Helpers/AppInfo.swift | 44 +++++++++++++++++++ macos/Sources/Helpers/Xcode.swift | 10 ----- 6 files changed, 52 insertions(+), 18 deletions(-) create mode 100644 macos/Sources/Helpers/AppInfo.swift delete mode 100644 macos/Sources/Helpers/Xcode.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index e9c02ef41..4943f2f4d 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -90,7 +90,7 @@ A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; }; A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; }; A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; }; - A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; }; + A5A6F72A2CC41B8900B232A5 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* AppInfo.swift */; }; A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; @@ -205,7 +205,7 @@ A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = ""; }; A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = ""; }; A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = ""; }; - A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = ""; }; + A5A6F7292CC41B8700B232A5 /* AppInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfo.swift; sourceTree = ""; }; A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LastWindowPosition.swift; sourceTree = ""; }; A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -312,8 +312,8 @@ children = ( A58636692DF0A98100E04A10 /* Extensions */, A5874D9B2DAD781100E83852 /* Private */, + A5A6F7292CC41B8700B232A5 /* AppInfo.swift */, A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */, - A5A6F7292CC41B8700B232A5 /* Xcode.swift */, A5CEAFFE29C2410700646FDA /* Backport.swift */, A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, @@ -756,7 +756,7 @@ A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, - A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */, + A5A6F72A2CC41B8900B232A5 /* AppInfo.swift in Sources */, A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */, A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */, A5CA378E2D31D6C300931030 /* Weak.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03a4e548e..49b3fea34 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -16,7 +16,7 @@ class TerminalController: BaseTerminalController { case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" case "tabs": - if #available(macOS 26.0, *) { + if #available(macOS 26.0, *), hasLiquidGlass() { "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index f9dfb9591..e24323113 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -447,7 +447,7 @@ extension TerminalWindow { // The padding from the top that the view appears. This was all just manually // measured based on the OS. var topPadding: CGFloat { - if #available(macOS 26.0, *) { + if #available(macOS 26.0, *), hasLiquidGlass() { return viewModel.hasToolbar ? 10 : 5 } else { return viewModel.hasToolbar ? 9 : 4 diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 94e938326..1a92fa024 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -48,7 +48,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our // titlebar to be truly transparent. - if #unavailable(macOS 26.0) { + if !effectViewIsHidden && !hasLiquidGlass() { hideEffectView() } } @@ -65,7 +65,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // references changed (e.g. tabGroup is new). setupKVO() - if #available(macOS 26.0, *) { + if #available(macOS 26.0, *), hasLiquidGlass() { syncAppearanceTahoe(surfaceConfig) } else { syncAppearanceVentura(surfaceConfig) diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift new file mode 100644 index 000000000..cf66e332d --- /dev/null +++ b/macos/Sources/Helpers/AppInfo.swift @@ -0,0 +1,44 @@ +import Foundation + +/// True if we appear to be running in Xcode. +func isRunningInXcode() -> Bool { + if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { + return true + } + + return false +} + +/// True if we have liquid glass available. +func hasLiquidGlass() -> Bool { + // Can't have liquid glass unless we're in macOS 26+ + if #unavailable(macOS 26.0) { + return false + } + + // If we aren't running SDK 26.0 or later then we definitely + // do not have liquid glass. + guard let sdkName = Bundle.main.infoDictionary?["DTSDKName"] as? String else { + // If we don't have this, we assume we're built against the latest + // since we're on macOS 26+ + return true + } + + // If the SDK doesn't start with macosx then we just assume we + // have it because we already verified we're on macOS above. + guard sdkName.hasPrefix("macosx") else { + return true + } + + // The SDK version must be at least 26 + let versionString = String(sdkName.dropFirst("macosx".count)) + guard let major = if let dotIndex = versionString.firstIndex(of: ".") { + Int(String(versionString[..= 26 +} diff --git a/macos/Sources/Helpers/Xcode.swift b/macos/Sources/Helpers/Xcode.swift deleted file mode 100644 index 281bad18b..000000000 --- a/macos/Sources/Helpers/Xcode.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -/// True if we appear to be running in Xcode. -func isRunningInXcode() -> Bool { - if let _ = ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] { - return true - } - - return false -} From 5b9f4acbc8d3498614d8784008f735d9333e3752 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 14 Jun 2025 12:30:09 -0700 Subject: [PATCH 477/642] ci: update macOS builders to Sequoia (15) and Xcode 16.4 We have been building on macOS 14 and Xcode 16.0 for a longggg time now. This gets us to a version that will be running Xcode 26 eventually so we can ultimately build for Tahoe on a stable OS. This should change nothing in the interim. --- .github/workflows/release-pr.yml | 8 ++++---- .github/workflows/release-tag.yml | 6 +++--- .github/workflows/release-tip.yml | 12 ++++++------ .github/workflows/test.yml | 12 ++++++------ 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 574b1ab73..3f89bd702 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -47,7 +47,7 @@ jobs: sentry-cli dif upload --project ghostty --wait dsym.zip build-macos: - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -94,7 +94,7 @@ jobs: - name: Build Ghostty.app run: | cd macos - sudo xcode-select -s /Applications/Xcode_16.0.app + sudo xcode-select -s /Applications/Xcode_16.4.app xcodebuild -target Ghostty -configuration Release # We inject the "build number" as simply the number of commits since HEAD. @@ -199,7 +199,7 @@ jobs: destination-dir: ./ build-macos-debug: - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -246,7 +246,7 @@ jobs: - name: Build Ghostty.app run: | cd macos - sudo xcode-select -s /Applications/Xcode_16.0.app + sudo xcode-select -s /Applications/Xcode_16.4.app xcodebuild -target Ghostty -configuration Release # We inject the "build number" as simply the number of commits since HEAD. diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index db8049df7..3deafd066 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -120,7 +120,7 @@ jobs: build-macos: needs: [setup] - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 env: GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} @@ -139,7 +139,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: Setup Sparkle env: @@ -288,7 +288,7 @@ jobs: appcast: needs: [setup, build-macos] - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia env: GHOSTTY_VERSION: ${{ needs.setup.outputs.version }} GHOSTTY_BUILD: ${{ needs.setup.outputs.build }} diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 73a1ddeeb..6c6399afd 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -154,7 +154,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -173,7 +173,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app # Setup Sparkle - name: Setup Sparkle @@ -369,7 +369,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -388,7 +388,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app # Setup Sparkle - name: Setup Sparkle @@ -544,7 +544,7 @@ jobs: ) }} - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia timeout-minutes: 90 steps: - name: Checkout code @@ -563,7 +563,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app # Setup Sparkle - name: Setup Sparkle diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a98584a2..814acec8f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -270,7 +270,7 @@ jobs: ghostty-source.tar.gz build-macos: - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: - name: Checkout code @@ -286,7 +286,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: get the Zig deps id: deps @@ -361,7 +361,7 @@ jobs: xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" build-macos-matrix: - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: - name: Checkout code @@ -377,7 +377,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: get the Zig deps id: deps @@ -679,7 +679,7 @@ jobs: nix develop -c zig build -Dsentry=${{ matrix.sentry }} test-macos: - runs-on: namespace-profile-ghostty-macos + runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: - name: Checkout code @@ -695,7 +695,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.0.app + run: sudo xcode-select -s /Applications/Xcode_16.4.app - name: get the Zig deps id: deps From 2f33eee166d68f3775387df14e752244c8626bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristo=CC=81fer=20R?= Date: Sat, 14 Jun 2025 16:26:03 -0400 Subject: [PATCH 478/642] fix comptime if statement --- src/os/hostname.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/os/hostname.zig b/src/os/hostname.zig index ddcdeb59e..a75ca1cbb 100644 --- a/src/os/hostname.zig +++ b/src/os/hostname.zig @@ -48,7 +48,7 @@ pub fn parseUrl(url: []const u8) UrlParsingError!std.Uri { return std.Uri.parse(url) catch |e| { // The mac-address-as-hostname issue is specific to macOS so we just return an error if we // hit it on other platforms. - comptime if (builtin.os.tag != .macos) return e; + if (comptime builtin.os.tag != .macos) return e; // It's possible this is a mac address on macOS where the last 2 characters in the // address are non-digits, e.g. 'ff', and thus an invalid port. From c4a978b07aa1ada5bd6817b2194fa8f853bbff5e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 14 Jun 2025 13:49:58 -0700 Subject: [PATCH 479/642] macos: set toolbar title `isBordered` to avoid glass view This was recommended by the WWDC25 session on AppKit updates. My hack was not the right approach. --- .../TitlebarTabsTahoeTerminalWindow.swift | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 145c37c59..9381f7329 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -21,18 +21,6 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool } } - override var toolbar: NSToolbar? { - didSet{ - guard toolbar != nil else { return } - - // When a toolbar is added, remove the Liquid Glass look to have a cleaner - // appearance for our custom titlebar tabs. - if let glass = titlebarContainer?.firstDescendant(withClassName: "NSGlassContainerView") { - glass.isHidden = true - } - } - } - override func awakeFromNib() { super.awakeFromNib() @@ -222,6 +210,11 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool item.view = NSHostingView(rootView: TitleItem(viewModel: viewModel)) item.visibilityPriority = .user item.isEnabled = true + + // This is the documented way to avoid the glass view on an item. + // We don't want glass on our title. + item.isBordered = false + return item default: return NSToolbarItem(itemIdentifier: itemIdentifier) From 202020cd7d10bb6fa7b61c5d67033ddb5565b52c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 14 Jun 2025 14:21:40 -0700 Subject: [PATCH 480/642] macos: menu item symbols for Tahoe This is recommended for macOS Tahoe and all standard menu items now have associated images. This makes our app look more polished and native for macOS Tahoe. For icon choice, I tried to copy other native macOS apps as much as possible, mostly from Xcode. It looks like a lot of apps aren't updated yet. I'm absolutely open to suggestions for better icons but I think these are a good starting point. One menu change is I moved "reset font size" above "increase font size" which better matches other apps (e.g. Terminal.app). --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ macos/Sources/App/macOS/AppDelegate.swift | 40 ++++++++++++++++++- macos/Sources/App/macOS/MainMenu.xib | 16 ++++---- .../Sources/Ghostty/SurfaceView_AppKit.swift | 25 ++++++++---- .../Extensions/NSMenuItem+Extension.swift | 11 +++++ 5 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 4943f2f4d..a5663202b 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -93,6 +93,7 @@ A5A6F72A2CC41B8900B232A5 /* AppInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* AppInfo.swift */; }; A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; }; A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; }; + A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; }; A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; }; A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; }; A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; }; @@ -210,6 +211,7 @@ A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; }; A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = ""; }; + A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = ""; }; A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = ""; }; A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = ""; }; A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = ""; }; @@ -478,6 +480,7 @@ A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */, + A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */, A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */, AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */, C1F26EA62B738B9900404083 /* NSView+Extension.swift */, @@ -763,6 +766,7 @@ A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */, A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */, A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */, + A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7fb52a025..f460017f5 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -166,7 +166,7 @@ class AppDelegate: NSObject, // This registers the Ghostty => Services menu to exist. NSApp.servicesMenu = menuServices - + // Setup a local event monitor for app-level keyboard shortcuts. See // localEventHandler for more info why. _ = NSEvent.addLocalMonitorForEvents( @@ -242,6 +242,9 @@ class AppDelegate: NSObject, ghostty_app_set_color_scheme(app, scheme) } + + // Setup our menu + setupMenuImages() } func applicationDidBecomeActive(_ notification: Notification) { @@ -392,6 +395,41 @@ class AppDelegate: NSObject, return dockMenu } + /// Setup all the images for our menu items. + private func setupMenuImages() { + // Note: This COULD Be done all in the xib file, but I find it easier to + // modify this stuff as code. + self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus") + self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow") + self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") + self.menuSplitLeft?.setImageIfDesired(systemSymbolName: "rectangle.leadinghalf.inset.filled") + self.menuSplitUp?.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled") + self.menuSplitDown?.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled") + self.menuClose?.setImageIfDesired(systemSymbolName: "xmark") + self.menuIncreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.larger") + self.menuResetFontSize?.setImageIfDesired(systemSymbolName: "textformat.size") + self.menuDecreaseFontSize?.setImageIfDesired(systemSymbolName: "textformat.size.smaller") + self.menuCommandPalette?.setImageIfDesired(systemSymbolName: "filemenu.and.selection") + self.menuQuickTerminal?.setImageIfDesired(systemSymbolName: "apple.terminal") + self.menuChangeTitle?.setImageIfDesired(systemSymbolName: "pencil.line") + self.menuTerminalInspector?.setImageIfDesired(systemSymbolName: "scope") + self.menuToggleFullScreen?.setImageIfDesired(systemSymbolName: "square.arrowtriangle.4.outward") + self.menuToggleVisibility?.setImageIfDesired(systemSymbolName: "eye") + self.menuZoomSplit?.setImageIfDesired(systemSymbolName: "arrow.up.left.and.arrow.down.right") + self.menuPreviousSplit?.setImageIfDesired(systemSymbolName: "chevron.backward.2") + self.menuNextSplit?.setImageIfDesired(systemSymbolName: "chevron.forward.2") + self.menuEqualizeSplits?.setImageIfDesired(systemSymbolName: "inset.filled.topleft.topright.bottomleft.bottomright.rectangle") + self.menuSelectSplitLeft?.setImageIfDesired(systemSymbolName: "arrow.left") + self.menuSelectSplitRight?.setImageIfDesired(systemSymbolName: "arrow.right") + self.menuSelectSplitAbove?.setImageIfDesired(systemSymbolName: "arrow.up") + self.menuSelectSplitBelow?.setImageIfDesired(systemSymbolName: "arrow.down") + self.menuMoveSplitDividerUp?.setImageIfDesired(systemSymbolName: "arrow.up.to.line") + self.menuMoveSplitDividerDown?.setImageIfDesired(systemSymbolName: "arrow.down.to.line") + self.menuMoveSplitDividerLeft?.setImageIfDesired(systemSymbolName: "arrow.left.to.line") + self.menuMoveSplitDividerRight?.setImageIfDesired(systemSymbolName: "arrow.right.to.line") + self.menuFloatOnTop?.setImageIfDesired(systemSymbolName: "square.3.layers.3d.top.filled") + } + /// Sync all of our menu item keyboard shortcuts with the Ghostty configuration. private func syncMenuShortcuts(_ config: Ghostty.Config) { guard ghostty.readiness == .ready else { return } diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index 7130d544e..c9bff8b4a 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -1,8 +1,8 @@ - + - + @@ -251,18 +251,18 @@ - - - - - - + + + + + + diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index e4f6f507c..3e87176fc 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1281,6 +1281,10 @@ extension Ghostty { let menu = NSMenu() + // We just use a floating var so we can easily setup metadata on each item + // in a row without storing it all. + var item: NSMenuItem + // If we have a selection, add copy if self.selectedRange().length > 0 { menu.addItem(withTitle: "Copy", action: #selector(copy(_:)), keyEquivalent: "") @@ -1288,16 +1292,23 @@ extension Ghostty { menu.addItem(withTitle: "Paste", action: #selector(paste(_:)), keyEquivalent: "") menu.addItem(.separator()) - menu.addItem(withTitle: "Split Right", action: #selector(splitRight(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Left", action: #selector(splitLeft(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Down", action: #selector(splitDown(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Split Right", action: #selector(splitRight(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") + item = menu.addItem(withTitle: "Split Left", action: #selector(splitLeft(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.leadinghalf.inset.filled") + item = menu.addItem(withTitle: "Split Down", action: #selector(splitDown(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.bottomhalf.inset.filled") + item = menu.addItem(withTitle: "Split Up", action: #selector(splitUp(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "rectangle.tophalf.inset.filled") menu.addItem(.separator()) - menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "") - menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Reset Terminal", action: #selector(resetTerminal(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise") + item = menu.addItem(withTitle: "Toggle Terminal Inspector", action: #selector(toggleTerminalInspector(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "scope") menu.addItem(.separator()) - menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") + item = menu.addItem(withTitle: "Change Title...", action: #selector(changeTitle(_:)), keyEquivalent: "") + item.setImageIfDesired(systemSymbolName: "pencil.line") return menu } diff --git a/macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift b/macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift new file mode 100644 index 000000000..e512904ef --- /dev/null +++ b/macos/Sources/Helpers/Extensions/NSMenuItem+Extension.swift @@ -0,0 +1,11 @@ +import AppKit + +extension NSMenuItem { + /// Sets the image property from a symbol if we want images on our menu items. + func setImageIfDesired(systemSymbolName symbol: String) { + // We only set on macOS 26 when icons on menu items became the norm. + if #available(macOS 26, *) { + image = NSImage(systemSymbolName: symbol, accessibilityDescription: title) + } + } +} From 7cc7f6cb06e67b2c5d11575bfac0d7a377d36150 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jun 2025 06:51:00 -0700 Subject: [PATCH 481/642] macos 15 regression: transparent style shouldn't draw border This fixes a regression from our Tahoe window styling changes on earlier, stable versions of macOS. We need to set "titlebarAppearsTransparent" to true in order to hide the bottom border. --- .../Window Styles/TransparentTitlebarTerminalWindow.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 1a92fa024..0d064a7f7 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -96,9 +96,16 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { @available(macOS 13.0, *) private func syncAppearanceVentura(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { guard let titlebarContainer else { return } + + // Setup the titlebar background color to match ours titlebarContainer.wantsLayer = true titlebarContainer.layer?.backgroundColor = preferredBackgroundColor?.cgColor + + // See the docs for the function that sets this to true on why effectViewIsHidden = false + + // Necessary to not draw the border around the title + titlebarAppearsTransparent = true } // MARK: View Finders From 57c79fa357bb4dce1770db99ba3269682902e460 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jun 2025 07:48:14 -0700 Subject: [PATCH 482/642] macos: Tahoe menu item icons, missed the "Ghostty" menu entirely This is a follow up to #7594, I missed an entire menu. --- macos/Sources/App/macOS/AppDelegate.swift | 6 ++++++ macos/Sources/App/macOS/MainMenu.xib | 1 + 2 files changed, 7 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index f460017f5..c56d7c3ac 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -18,6 +18,7 @@ class AppDelegate: NSObject, ) /// Various menu items so that we can programmatically sync the keyboard shortcut with the Ghostty config + @IBOutlet private var menuAbout: NSMenuItem? @IBOutlet private var menuServices: NSMenu? @IBOutlet private var menuCheckForUpdates: NSMenuItem? @IBOutlet private var menuOpenConfig: NSMenuItem? @@ -399,6 +400,11 @@ class AppDelegate: NSObject, private func setupMenuImages() { // Note: This COULD Be done all in the xib file, but I find it easier to // modify this stuff as code. + self.menuAbout?.setImageIfDesired(systemSymbolName: "info.circle") + self.menuCheckForUpdates?.setImageIfDesired(systemSymbolName: "square.and.arrow.down") + self.menuOpenConfig?.setImageIfDesired(systemSymbolName: "gear") + self.menuReloadConfig?.setImageIfDesired(systemSymbolName: "arrow.trianglehead.2.clockwise.rotate.90") + self.menuSecureInput?.setImageIfDesired(systemSymbolName: "lock.display") self.menuNewWindow?.setImageIfDesired(systemSymbolName: "macwindow.badge.plus") self.menuNewTab?.setImageIfDesired(systemSymbolName: "macwindow") self.menuSplitRight?.setImageIfDesired(systemSymbolName: "rectangle.righthalf.inset.filled") diff --git a/macos/Sources/App/macOS/MainMenu.xib b/macos/Sources/App/macOS/MainMenu.xib index c9bff8b4a..5cd6d9bec 100644 --- a/macos/Sources/App/macOS/MainMenu.xib +++ b/macos/Sources/App/macOS/MainMenu.xib @@ -14,6 +14,7 @@ + From 4237dad240089f35643b5f83ccf83c17323bb694 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Jun 2025 12:20:15 -0700 Subject: [PATCH 483/642] macOS: simple SplitView AX Proper labels, action to move the divider --- .../Features/Splits/SplitView.Divider.swift | 35 +++++++++++++++++ macos/Sources/Features/Splits/SplitView.swift | 38 ++++++++++++++++++- .../Splits/TerminalSplitTreeView.swift | 2 + 3 files changed, 74 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Splits/SplitView.Divider.swift b/macos/Sources/Features/Splits/SplitView.Divider.swift index 83847ff0c..a01175dce 100644 --- a/macos/Sources/Features/Splits/SplitView.Divider.swift +++ b/macos/Sources/Features/Splits/SplitView.Divider.swift @@ -7,6 +7,7 @@ extension SplitView { let visibleSize: CGFloat let invisibleSize: CGFloat let color: Color + @Binding var split: CGFloat private var visibleWidth: CGFloat? { switch (direction) { @@ -79,6 +80,40 @@ extension SplitView { NSCursor.pop() } } + .accessibilityElement(children: .ignore) + .accessibilityLabel(axLabel) + .accessibilityValue("\(Int(split * 100))%") + .accessibilityHint(axHint) + .accessibilityAddTraits(.isButton) + .accessibilityAdjustableAction { direction in + let adjustment: CGFloat = 0.025 + switch direction { + case .increment: + split = min(split + adjustment, 0.9) + case .decrement: + split = max(split - adjustment, 0.1) + @unknown default: + break + } + } + } + + private var axLabel: String { + switch direction { + case .horizontal: + return "Horizontal split divider" + case .vertical: + return "Vertical split divider" + } + } + + private var axHint: String { + switch direction { + case .horizontal: + return "Drag to resize the left and right panes" + case .vertical: + return "Drag to resize the top and bottom panes" + } } } } diff --git a/macos/Sources/Features/Splits/SplitView.swift b/macos/Sources/Features/Splits/SplitView.swift index 9747ac99f..3dc3c36a3 100644 --- a/macos/Sources/Features/Splits/SplitView.swift +++ b/macos/Sources/Features/Splits/SplitView.swift @@ -42,16 +42,23 @@ struct SplitView: View { left .frame(width: leftRect.size.width, height: leftRect.size.height) .offset(x: leftRect.origin.x, y: leftRect.origin.y) + .accessibilityElement(children: .contain) + .accessibilityLabel(leftPaneLabel) right .frame(width: rightRect.size.width, height: rightRect.size.height) .offset(x: rightRect.origin.x, y: rightRect.origin.y) + .accessibilityElement(children: .contain) + .accessibilityLabel(rightPaneLabel) Divider(direction: direction, visibleSize: splitterVisibleSize, invisibleSize: splitterInvisibleSize, - color: dividerColor) + color: dividerColor, + split: $split) .position(splitterPoint) .gesture(dragGesture(geo.size, splitterPoint: splitterPoint)) } + .accessibilityElement(children: .contain) + .accessibilityLabel(splitViewLabel) } } @@ -137,6 +144,35 @@ struct SplitView: View { return CGPoint(x: size.width / 2, y: leftRect.size.height) } } + + // MARK: Accessibility + + private var splitViewLabel: String { + switch direction { + case .horizontal: + return "Horizontal split view" + case .vertical: + return "Vertical split view" + } + } + + private var leftPaneLabel: String { + switch direction { + case .horizontal: + return "Left pane" + case .vertical: + return "Top pane" + } + } + + private var rightPaneLabel: String { + switch direction { + case .horizontal: + return "Right pane" + case .vertical: + return "Bottom pane" + } + } } enum SplitViewDirection: Codable { diff --git a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift index 2810fc2b4..f19640707 100644 --- a/macos/Sources/Features/Splits/TerminalSplitTreeView.swift +++ b/macos/Sources/Features/Splits/TerminalSplitTreeView.swift @@ -32,6 +32,8 @@ struct TerminalSplitSubtreeView: View { Ghostty.InspectableSurface( surfaceView: leafView, isSplit: !isRoot) + .accessibilityElement(children: .contain) + .accessibilityLabel("Terminal pane") case .split(let split): let splitViewDirection: SplitViewDirection = switch (split.direction) { From c90eb2e9525288d790d5a5be5e80a9f7272c1318 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Jun 2025 12:48:42 -0700 Subject: [PATCH 484/642] macos: AX for debug warning --- macos/Sources/Features/Terminal/TerminalView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalView.swift b/macos/Sources/Features/Terminal/TerminalView.swift index cb6f11bce..b5be0ae42 100644 --- a/macos/Sources/Features/Terminal/TerminalView.swift +++ b/macos/Sources/Features/Terminal/TerminalView.swift @@ -139,6 +139,10 @@ struct DebugBuildWarningView: View { } .background(Color(.windowBackgroundColor)) .frame(maxWidth: .infinity) + .accessibilityElement(children: .combine) + .accessibilityLabel("Debug build warning") + .accessibilityValue("Debug builds of Ghostty are very slow and you may experience performance problems. Debug builds are only recommended during development.") + .accessibilityAddTraits(.isStaticText) .onTapGesture { isPopover = true } From be437f5b64c903f82ee04f039541af83e7f08897 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Jun 2025 14:09:23 -0700 Subject: [PATCH 485/642] macos: bare minimum terminal ax --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 3e87176fc..3f9bb5e53 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1844,3 +1844,26 @@ extension Ghostty.SurfaceView { return false } } + +// MARK: Accessibility + +extension Ghostty.SurfaceView { + /// Indicates that this view should be exposed to accessibility tools like VoiceOver. + /// By returning true, we make the terminal surface accessible to screen readers + /// and other assistive technologies. + override func isAccessibilityElement() -> Bool { + return true + } + + /// Defines the accessibility role for this view, which helps assistive technologies + /// understand what kind of content this view contains and how users can interact with it. + override func accessibilityRole() -> NSAccessibility.Role? { + /// We use .textArea because the terminal surface is essentially an editable text area + /// where users can input commands and view output. + return .textArea + } + + override func accessibilityHelp() -> String? { + return "Terminal content area" + } +} From c5f921bb066d5fb6b50d3a5570ef727a2f5ea35f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 9 Jun 2025 15:48:03 -0700 Subject: [PATCH 486/642] apprt/embedded: improve text reading APIs (selection, random points) --- include/ghostty.h | 33 ++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 75 +++--- src/Surface.zig | 127 ++++++++++ src/apprt/embedded.zig | 222 ++++++++++++------ src/terminal/main.zig | 1 + 5 files changed, 357 insertions(+), 101 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 9f17d0b97..9fc58aa87 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -355,6 +355,27 @@ typedef struct { double tl_px_y; uint32_t offset_start; uint32_t offset_len; + const char* text; + uintptr_t text_len; +} ghostty_text_s; + +typedef enum { + GHOSTTY_POINT_ACTIVE, + GHOSTTY_POINT_VIEWPORT, + GHOSTTY_POINT_SCREEN, + GHOSTTY_POINT_SURFACE, +} ghostty_point_tag_e; + +typedef struct { + ghostty_point_tag_e tag; + uint32_t x; + uint32_t y; +} ghostty_point_s; + +typedef struct { + ghostty_point_s top_left; + ghostty_point_s bottom_right; + bool rectangle; } ghostty_selection_s; typedef struct { @@ -832,16 +853,16 @@ void ghostty_surface_complete_clipboard_request(ghostty_surface_t, void*, bool); bool ghostty_surface_has_selection(ghostty_surface_t); -uintptr_t ghostty_surface_selection(ghostty_surface_t, char*, uintptr_t); +bool ghostty_surface_read_selection(ghostty_surface_t, ghostty_text_s*); +bool ghostty_surface_read_text(ghostty_surface_t, + ghostty_selection_s, + ghostty_text_s*); +void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*); #ifdef __APPLE__ void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t); void* ghostty_surface_quicklook_font(ghostty_surface_t); -uintptr_t ghostty_surface_quicklook_word(ghostty_surface_t, - char*, - uintptr_t, - ghostty_selection_s*); -bool ghostty_surface_selection_info(ghostty_surface_t, ghostty_selection_s*); +bool ghostty_surface_quicklook_word(ghostty_surface_t, ghostty_text_s*); #endif ghostty_inspector_t ghostty_surface_inspector(ghostty_surface_t); diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 3f9bb5e53..cf9252c88 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1215,11 +1215,10 @@ extension Ghostty { guard let surface = self.surface else { return super.quickLook(with: event) } // Grab the text under the cursor - var info: ghostty_selection_s = ghostty_selection_s(); - let text = String(unsafeUninitializedCapacity: 1000000) { - Int(ghostty_surface_quicklook_word(surface, $0.baseAddress, UInt($0.count), &info)) - } - guard !text.isEmpty else { return super.quickLook(with: event) } + var text = ghostty_text_s() + guard ghostty_surface_quicklook_word(surface, &text) else { return super.quickLook(with: event) } + defer { ghostty_surface_free_text(surface, &text) } + guard text.text_len > 0 else { return super.quickLook(with: event) } // If we can get a font then we use the font. This should always work // since we always have a primary font. The only scenario this doesn't @@ -1236,8 +1235,8 @@ extension Ghostty { } // Ghostty coordinate system is top-left, convert to bottom-left for AppKit - let pt = NSMakePoint(info.tl_px_x, frame.size.height - info.tl_px_y) - let str = NSAttributedString.init(string: text, attributes: attributes) + let pt = NSMakePoint(text.tl_px_x, frame.size.height - text.tl_px_y) + let str = NSAttributedString.init(string: String(cString: text.text), attributes: attributes) self.showDefinition(for: str, at: pt); } @@ -1522,9 +1521,10 @@ extension Ghostty.SurfaceView: NSTextInputClient { // Get our range from the Ghostty API. There is a race condition between getting the // range and actually using it since our selection may change but there isn't a good // way I can think of to solve this for AppKit. - var sel: ghostty_selection_s = ghostty_selection_s(); - guard ghostty_surface_selection_info(surface, &sel) else { return NSRange() } - return NSRange(location: Int(sel.offset_start), length: Int(sel.offset_len)) + var text = ghostty_text_s() + guard ghostty_surface_read_selection(surface, &text) else { return NSRange() } + defer { ghostty_surface_free_text(surface, &text) } + return NSRange(location: Int(text.offset_start), length: Int(text.offset_len)) } func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) { @@ -1562,7 +1562,6 @@ extension Ghostty.SurfaceView: NSTextInputClient { func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? { // Ghostty.logger.warning("pressure substring range=\(range) selectedRange=\(self.selectedRange())") guard let surface = self.surface else { return nil } - guard ghostty_surface_has_selection(surface) else { return nil } // If the range is empty then we don't need to return anything guard range.length > 0 else { return nil } @@ -1572,11 +1571,10 @@ extension Ghostty.SurfaceView: NSTextInputClient { // bogus ranges I truly don't understand so we just always return the // attributed string containing our selection which is... weird but works? - // Get our selection. We cap it at 1MB for the purpose of this. This is - // arbitrary. If this is a good reason to increase it I'm happy to. - let v = String(unsafeUninitializedCapacity: 1000000) { - Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count))) - } + // Get our selection text + var text = ghostty_text_s() + guard ghostty_surface_read_selection(surface, &text) else { return nil } + defer { ghostty_surface_free_text(surface, &text) } // If we can get a font then we use the font. This should always work // since we always have a primary font. The only scenario this doesn't @@ -1592,7 +1590,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { font.release() } - return .init(string: v, attributes: attributes) + return .init(string: String(cString: text.text), attributes: attributes) } func characterIndex(for point: NSPoint) -> Int { @@ -1614,12 +1612,15 @@ extension Ghostty.SurfaceView: NSTextInputClient { // point right now. I'm sure I'm missing something fundamental... if range.length > 0 && range != self.selectedRange() { // QuickLook - var sel: ghostty_selection_s = ghostty_selection_s(); - if ghostty_surface_selection_info(surface, &sel) { + var text = ghostty_text_s() + if ghostty_surface_read_selection(surface, &text) { // The -2/+2 here is subjective. QuickLook seems to offset the rectangle // a bit and I think these small adjustments make it look more natural. - x = sel.tl_px_x - 2; - y = sel.tl_px_y + 2; + x = text.tl_px_x - 2; + y = text.tl_px_y + 2; + + // Free our text + ghostty_surface_free_text(surface, &text) } else { ghostty_surface_ime_point(surface, &x, &y) } @@ -1745,14 +1746,13 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor { ) -> Bool { guard let surface = self.surface else { return false } - // We currently cap the maximum copy size to 1MB. iTerm2 I believe - // caps theirs at 0.1MB (configurable) so this is probably reasonable. - let v = String(unsafeUninitializedCapacity: 1000000) { - Int(ghostty_surface_selection(surface, $0.baseAddress, UInt($0.count))) - } + // Read the selection + var text = ghostty_text_s() + guard ghostty_surface_read_selection(surface, &text) else { return false } + defer { ghostty_surface_free_text(surface, &text) } pboard.declareTypes([.string], owner: nil) - pboard.setString(v, forType: .string) + pboard.setString(String(cString: text.text), forType: .string) return true } @@ -1866,4 +1866,25 @@ extension Ghostty.SurfaceView { override func accessibilityHelp() -> String? { return "Terminal content area" } + + /// Returns the range of text that is currently selected in the terminal. + /// This allows VoiceOver and other assistive technologies to understand + /// what text the user has selected. + override func accessibilitySelectedTextRange() -> NSRange { + return selectedRange() + } + + /// Returns the currently selected text as a string. + /// This allows assistive technologies to read the selected content. + override func accessibilitySelectedText() -> String? { + guard let surface = self.surface else { return nil } + + // Attempt to read the selection + var text = ghostty_text_s() + guard ghostty_surface_read_selection(surface, &text) else { return nil } + defer { ghostty_surface_free_text(surface, &text) } + + let str = String(cString: text.text) + return str.isEmpty ? nil : str + } } diff --git a/src/Surface.zig b/src/Surface.zig index 9ab7234d6..41d40125a 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1292,6 +1292,133 @@ fn recomputeInitialSize( ) catch return error.AppActionFailed; } +/// Represents text read from the terminal and some metadata about it +/// that is often useful to apprts. +pub const Text = struct { + /// The text that was read from the terminal. + text: [:0]const u8, + + /// The viewport information about this text, if it is visible in + /// the viewport. + /// + /// NOTE(mitchellh): This will only be non-null currently if the entirety + /// of the selection is contained within the viewport. We don't have a + /// use case currently for partial bounds but we should support this + /// eventually. + viewport: ?Viewport = null, + + pub const Viewport = struct { + /// The top-left corner of the selection in pixels within the viewport. + tl_px_x: f64, + tl_px_y: f64, + + /// The linear offset of the start of the selection and the length. + /// This is "linear" in the sense that it is the offset in the + /// flattened viewport as a single array of text. + offset_start: u32, + offset_len: u32, + }; + + pub fn deinit(self: *Text, alloc: Allocator) void { + alloc.free(self.text); + } +}; + +/// Grab the value of text at the given selection point. Note that the +/// selection structure is used as a way to determine the area of the +/// screen to read from, it doesn't have to match the user's current +/// selection state. +/// +/// The returned value contains allocated data and must be deinitialized. +pub fn dumpText( + self: *Surface, + alloc: Allocator, + sel: terminal.Selection, +) !Text { + self.renderer_state.mutex.lock(); + defer self.renderer_state.mutex.unlock(); + return try self.dumpTextLocked(alloc, sel); +} + +/// Same as `dumpText` but assumes the renderer state mutex is already +/// held. +pub fn dumpTextLocked( + self: *Surface, + alloc: Allocator, + sel: terminal.Selection, +) !Text { + // Read out the text + const text = try self.io.terminal.screen.selectionString(alloc, .{ + .sel = sel, + .trim = false, + }); + errdefer alloc.free(text); + + // Calculate our viewport info if we can. + const vp: ?Text.Viewport = viewport: { + // If our tl or br is not in the viewport then we don't + // have a viewport. One day we should extend this to support + // partial selections that are in the viewport. + const tl_pt = self.io.terminal.screen.pages.pointFromPin( + .viewport, + sel.topLeft(&self.io.terminal.screen), + ) orelse break :viewport null; + const br_pt = self.io.terminal.screen.pages.pointFromPin( + .viewport, + sel.bottomRight(&self.io.terminal.screen), + ) orelse break :viewport null; + const tl_coord = tl_pt.coord(); + const br_coord = br_pt.coord(); + + // Our sizes are all scaled so we need to send the unscaled values back. + const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; + const x: f64 = x: { + // Simple x * cell width gives the left + var x: f64 = @floatFromInt(tl_coord.x * self.size.cell.width); + + // Add padding + x += @floatFromInt(self.size.padding.left); + + // Scale + x /= content_scale.x; + + break :x x; + }; + const y: f64 = y: { + // Simple y * cell height gives the top + var y: f64 = @floatFromInt(tl_coord.y * self.size.cell.height); + + // We want the text baseline + y += @floatFromInt(self.size.cell.height); + y -= @floatFromInt(self.font_metrics.cell_baseline); + + // Add padding + y += @floatFromInt(self.size.padding.top); + + // Scale + y /= content_scale.y; + + break :y y; + }; + + // Utilize viewport sizing to convert to offsets + const start = tl_coord.y * self.io.terminal.screen.pages.cols + tl_coord.x; + const end = br_coord.y * self.io.terminal.screen.pages.cols + br_coord.x; + + break :viewport .{ + .tl_px_x = x, + .tl_px_y = y, + .offset_start = start, + .offset_len = end - start, + }; + }; + + return .{ + .text = text, + .viewport = vp, + }; +} + /// Returns true if the terminal has a selection. pub fn hasSelection(self: *const Surface) bool { self.renderer_state.mutex.lock(); diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 5334c8ecd..dbc74e6ae 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1138,13 +1138,6 @@ pub const CAPI = struct { } }; - const Selection = extern struct { - tl_x_px: f64, - tl_y_px: f64, - offset_start: u32, - offset_len: u32, - }; - const SurfaceSize = extern struct { columns: u16, rows: u16, @@ -1154,6 +1147,83 @@ pub const CAPI = struct { cell_height_px: u32, }; + // ghostty_text_s + const Text = extern struct { + tl_px_x: f64, + tl_px_y: f64, + offset_start: u32, + offset_len: u32, + text: ?[*:0]const u8, + text_len: usize, + + pub fn deinit(self: *Text) void { + if (self.text) |ptr| { + global.alloc.free(ptr[0..self.text_len :0]); + } + } + }; + + // ghostty_point_s + const Point = extern struct { + tag: Tag, + x: u32, + y: u32, + + const Tag = enum(c_int) { + active = 0, + viewport = 1, + screen = 2, + history = 3, + }; + + fn core(self: Point) terminal.Point { + // This comes from the C API so we can't trust the input. + const pt_x = std.math.cast( + terminal.size.CellCountInt, + self.x, + ) orelse std.math.maxInt(terminal.size.CellCountInt); + + return switch (self.tag) { + inline else => |tag| @unionInit( + terminal.Point, + @tagName(tag), + .{ .x = pt_x, .y = self.y }, + ), + }; + } + + fn clamp(self: Point, screen: *const terminal.Screen) Point { + // Clamp our point to the screen bounds. + const clamped_x = @min(self.x, screen.pages.cols -| 1); + const clamped_y = @min(self.y, screen.pages.rows -| 1); + return .{ .tag = self.tag, .x = clamped_x, .y = clamped_y }; + } + }; + + // ghostty_selection_s + const Selection = extern struct { + tl: Point, + br: Point, + rectangle: bool, + + fn core( + self: Selection, + screen: *const terminal.Screen, + ) ?terminal.Selection { + return .{ + .bounds = .{ .untracked = .{ + .start = screen.pages.pin( + self.tl.clamp(screen).core(), + ) orelse return null, + .end = screen.pages.pin( + self.br.clamp(screen).core(), + ) orelse return null, + } }, + .rectangle = self.rectangle, + }; + } + }; + // Reference the conditional exports based on target platform // so they're included in the C API. comptime { @@ -1369,23 +1439,80 @@ pub const CAPI = struct { return surface.core_surface.hasSelection(); } - /// Copies the surface selection text into the provided buffer and - /// returns the copied size. If the buffer is too small, there is no - /// selection, or there is an error, then 0 is returned. - export fn ghostty_surface_selection(surface: *Surface, buf: [*]u8, cap: usize) usize { - const selection_ = surface.core_surface.selectionString(global.alloc) catch |err| { - log.warn("error getting selection err={}", .{err}); - return 0; + /// Same as ghostty_surface_read_text but reads from the user selection, + /// if any. + export fn ghostty_surface_read_selection( + surface: *Surface, + result: *Text, + ) bool { + const core_surface = &surface.core_surface; + core_surface.renderer_state.mutex.lock(); + defer core_surface.renderer_state.mutex.unlock(); + + // If we don't have a selection, do nothing. + const core_sel = core_surface.io.terminal.screen.selection orelse return false; + + // Read the text from the selection. + return readTextLocked(surface, core_sel, result); + } + + /// Read some arbitrary text from the surface. + /// + /// This is an expensive operation so it shouldn't be called too + /// often. We recommend that callers cache the result and throttle + /// calls to this function. + export fn ghostty_surface_read_text( + surface: *Surface, + sel: Selection, + result: *Text, + ) bool { + surface.core_surface.renderer_state.mutex.lock(); + defer surface.core_surface.renderer_state.mutex.unlock(); + + const core_sel = sel.core( + &surface.core_surface.renderer_state.terminal.screen, + ) orelse return false; + + return readTextLocked(surface, core_sel, result); + } + + fn readTextLocked( + surface: *Surface, + core_sel: terminal.Selection, + result: *Text, + ) bool { + const core_surface = &surface.core_surface; + + // Get our text directly from the core surface. + const text = core_surface.dumpTextLocked( + global.alloc, + core_sel, + ) catch |err| { + log.warn("error reading text err={}", .{err}); + return false; }; - const selection = selection_ orelse return 0; - defer global.alloc.free(selection); - // If the buffer is too small, return no selection. - if (selection.len > cap) return 0; + const vp: CoreSurface.Text.Viewport = text.viewport orelse .{ + .tl_px_x = -1, + .tl_px_y = -1, + .offset_start = 0, + .offset_len = 0, + }; - // Copy into the buffer and return the length - @memcpy(buf[0..selection.len], selection); - return selection.len; + result.* = .{ + .tl_px_x = vp.tl_px_x, + .tl_px_y = vp.tl_px_y, + .offset_start = vp.offset_start, + .offset_len = vp.offset_len, + .text = text.text.ptr, + .text_len = text.text.len, + }; + + return true; + } + + export fn ghostty_surface_free_text(ptr: *Text) void { + ptr.deinit(); } /// Tell the surface that it needs to schedule a render @@ -1888,21 +2015,12 @@ pub const CAPI = struct { /// This does not modify the selection active on the surface (if any). export fn ghostty_surface_quicklook_word( ptr: *Surface, - buf: [*]u8, - cap: usize, - info: *Selection, - ) usize { + result: *Text, + ) bool { const surface = &ptr.core_surface; surface.renderer_state.mutex.lock(); defer surface.renderer_state.mutex.unlock(); - // To make everything in this function easier, we modify the - // selection to be the word under the cursor and call normal APIs. - // We restore the old selection so it isn't ever changed. Since we hold - // the renderer mutex it'll never show up in a frame. - const prev = surface.io.terminal.screen.selection; - defer surface.io.terminal.screen.selection = prev; - // Get our word selection const sel = sel: { const screen = &surface.renderer_state.terminal.screen; @@ -1915,45 +2033,13 @@ pub const CAPI = struct { }, }) orelse { if (comptime std.debug.runtime_safety) unreachable; - return 0; + return false; }; - break :sel surface.io.terminal.screen.selectWord(pin) orelse return 0; + break :sel surface.io.terminal.screen.selectWord(pin) orelse return false; }; - // Set the selection - surface.io.terminal.screen.selection = sel; - - // No we call normal functions. These require that the lock - // is unlocked. This may cause a frame flicker with the fake - // selection but I think the lack of new complexity is worth it - // for now. - { - surface.renderer_state.mutex.unlock(); - defer surface.renderer_state.mutex.lock(); - const len = ghostty_surface_selection(ptr, buf, cap); - if (!ghostty_surface_selection_info(ptr, info)) return 0; - return len; - } - } - - /// This returns the selection metadata for the current selection. - /// This will return false if there is no selection or the - /// selection is not fully contained in the viewport (since the - /// metadata is all about that). - export fn ghostty_surface_selection_info( - ptr: *Surface, - info: *Selection, - ) bool { - const sel = ptr.core_surface.selectionInfo() orelse - return false; - - info.* = .{ - .tl_x_px = sel.tl_x_px, - .tl_y_px = sel.tl_y_px, - .offset_start = sel.offset_start, - .offset_len = sel.offset_len, - }; - return true; + // Read the selection + return readTextLocked(ptr, sel, result); } export fn ghostty_inspector_metal_init(ptr: *Inspector, device: objc.c.id) bool { diff --git a/src/terminal/main.zig b/src/terminal/main.zig index df3788d30..74ffe6341 100644 --- a/src/terminal/main.zig +++ b/src/terminal/main.zig @@ -35,6 +35,7 @@ pub const Page = page.Page; pub const PageList = @import("PageList.zig"); pub const Parser = @import("Parser.zig"); pub const Pin = PageList.Pin; +pub const Point = point.Point; pub const Screen = @import("Screen.zig"); pub const ScreenType = Terminal.ScreenType; pub const Selection = @import("Selection.zig"); From e1ee180172ae5ba192dd4f272a70e21df11138ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jun 2025 13:06:18 -0700 Subject: [PATCH 487/642] apprt/embedded: API to read text can get top left/bottom right coords --- include/ghostty.h | 7 +++++ src/apprt/embedded.zig | 59 ++++++++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 19 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 9fc58aa87..fc2c915cb 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -366,8 +366,15 @@ typedef enum { GHOSTTY_POINT_SURFACE, } ghostty_point_tag_e; +typedef enum { + GHOSTTY_POINT_COORD_EXACT, + GHOSTTY_POINT_COORD_TOP_LEFT, + GHOSTTY_POINT_COORD_BOTTOM_RIGHT, +} ghostty_point_coord_e; + typedef struct { ghostty_point_tag_e tag; + ghostty_point_coord_e coord; uint32_t x; uint32_t y; } ghostty_point_s; diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index dbc74e6ae..a61c75e96 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1166,6 +1166,7 @@ pub const CAPI = struct { // ghostty_point_s const Point = extern struct { tag: Tag, + coord_tag: CoordTag, x: u32, y: u32, @@ -1176,27 +1177,51 @@ pub const CAPI = struct { history = 3, }; - fn core(self: Point) terminal.Point { - // This comes from the C API so we can't trust the input. - const pt_x = std.math.cast( - terminal.size.CellCountInt, - self.x, - ) orelse std.math.maxInt(terminal.size.CellCountInt); + const CoordTag = enum(c_int) { + exact = 0, + top_left = 1, + bottom_right = 2, + }; - return switch (self.tag) { - inline else => |tag| @unionInit( - terminal.Point, + fn pin( + self: Point, + screen: *const terminal.Screen, + ) ?terminal.Pin { + // The core point tag. + const tag: terminal.point.Tag = switch (self.tag) { + inline else => |tag| @field( + terminal.point.Tag, @tagName(tag), - .{ .x = pt_x, .y = self.y }, ), }; - } - fn clamp(self: Point, screen: *const terminal.Screen) Point { // Clamp our point to the screen bounds. const clamped_x = @min(self.x, screen.pages.cols -| 1); const clamped_y = @min(self.y, screen.pages.rows -| 1); - return .{ .tag = self.tag, .x = clamped_x, .y = clamped_y }; + + return switch (self.coord_tag) { + // Exact coordinates require a specific pin. + .exact => exact: { + const pt_x = std.math.cast( + terminal.size.CellCountInt, + clamped_x, + ) orelse std.math.maxInt(terminal.size.CellCountInt); + + const pt: terminal.Point = switch (tag) { + inline else => |v| @unionInit( + terminal.Point, + @tagName(v), + .{ .x = pt_x, .y = clamped_y }, + ), + }; + + break :exact screen.pages.pin(pt) orelse null; + }, + + .top_left => screen.pages.getTopLeft(tag), + + .bottom_right => screen.pages.getBottomRight(tag), + }; } }; @@ -1212,12 +1237,8 @@ pub const CAPI = struct { ) ?terminal.Selection { return .{ .bounds = .{ .untracked = .{ - .start = screen.pages.pin( - self.tl.clamp(screen).core(), - ) orelse return null, - .end = screen.pages.pin( - self.br.clamp(screen).core(), - ) orelse return null, + .start = self.tl.pin(screen) orelse return null, + .end = self.br.pin(screen) orelse return null, } }, .rectangle = self.rectangle, }; From 839d89f2dcbc77dd0f1de9e34f0e5e5eb5e21316 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jun 2025 13:46:34 -0700 Subject: [PATCH 488/642] macos: simple cache of screen contents for ax --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 76 ++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index cf9252c88..046e79a23 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -138,6 +138,9 @@ extension Ghostty { // by the user, this is set to the prior value (which may be empty, but non-nil). private var titleFromTerminal: String? + // The cached contents of the screen. + private var cachedScreenContents: CachedValue + /// Event monitor (see individual events for why) private var eventMonitor: Any? = nil @@ -159,11 +162,38 @@ extension Ghostty { self.derivedConfig = DerivedConfig() } + // We need to initialize this so it does something but we want to set + // it back up later so we can reference `self`. This is a hack we should + // fix at some point. + self.cachedScreenContents = .init(duration: .milliseconds(500)) { "" } + // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer // can do SOMETHING. super.init(frame: NSMakeRect(0, 0, 800, 600)) + // Our cache of screen data + cachedScreenContents = .init(duration: .milliseconds(500)) { [weak self] in + guard let self else { return "" } + guard let surface = self.surface else { return "" } + var text = ghostty_text_s() + let sel = ghostty_selection_s( + top_left: ghostty_point_s( + tag: GHOSTTY_POINT_SCREEN, + coord: GHOSTTY_POINT_COORD_TOP_LEFT, + x: 0, + y: 0), + bottom_right: ghostty_point_s( + tag: GHOSTTY_POINT_SCREEN, + coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, + x: 0, + y: 0), + rectangle: false) + guard ghostty_surface_read_text(surface, sel, &text) else { return "" } + defer { ghostty_surface_free_text(surface, &text) } + return String(cString: text.text) + } + // Set a timer to show the ghost emoji after 500ms if no title is set titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in if let self = self, self.title.isEmpty { @@ -1866,7 +1896,11 @@ extension Ghostty.SurfaceView { override func accessibilityHelp() -> String? { return "Terminal content area" } - + + override func accessibilityValue() -> Any? { + return cachedScreenContents.get() + } + /// Returns the range of text that is currently selected in the terminal. /// This allows VoiceOver and other assistive technologies to understand /// what text the user has selected. @@ -1888,3 +1922,43 @@ extension Ghostty.SurfaceView { return str.isEmpty ? nil : str } } + +/// Caches a value for some period of time, evicting it automatically when that time expires. +/// We use this to cache our surface content. This probably should be extracted some day +/// to a more generic helper. +/// +// TODO: +// - Auto-expire the data so it doesn't take memory +fileprivate class CachedValue { + private var value: Value? + private let fetch: () -> T + private let duration: Duration + + struct Value { + var value: T + var expires: ContinuousClock.Instant + } + + init(duration: Duration, fetch: @escaping () -> T) { + self.duration = duration + self.fetch = fetch + } + + func get() -> T { + let now = ContinuousClock.now + if let value { + // If the value isn't expired just return it + if value.expires > now { + return value.value + } + + // Value is expired, clear it + self.value = nil + } + + // We don't have a value (or it expired). Fetch and store. + let result = fetch() + self.value = .init(value: result, expires: now + duration) + return result + } +} From e69c756c895f1cc9106e22d419037a926c3176c7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jun 2025 13:55:03 -0700 Subject: [PATCH 489/642] macos: auto-expire cached screen contents --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 046e79a23..57fa56e77 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1926,39 +1926,43 @@ extension Ghostty.SurfaceView { /// Caches a value for some period of time, evicting it automatically when that time expires. /// We use this to cache our surface content. This probably should be extracted some day /// to a more generic helper. -/// -// TODO: -// - Auto-expire the data so it doesn't take memory fileprivate class CachedValue { - private var value: Value? + private var value: T? private let fetch: () -> T private let duration: Duration - - struct Value { - var value: T - var expires: ContinuousClock.Instant - } + private var expiryTask: Task? init(duration: Duration, fetch: @escaping () -> T) { self.duration = duration self.fetch = fetch } - func get() -> T { - let now = ContinuousClock.now - if let value { - // If the value isn't expired just return it - if value.expires > now { - return value.value - } + deinit { + expiryTask?.cancel() + } - // Value is expired, clear it - self.value = nil + func get() -> T { + if let value { + return value } // We don't have a value (or it expired). Fetch and store. let result = fetch() - self.value = .init(value: result, expires: now + duration) + let now = ContinuousClock.now + let expires = now + duration + self.value = result + + // Schedule a task to clear the value + expiryTask = Task { [weak self] in + do { + try await Task.sleep(until: expires) + self?.value = nil + self?.expiryTask = nil + } catch { + // Task was cancelled, do nothing + } + } + return result } } From a2b4a2c0e42cb2bdf970598083e59e5be88f511d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 15 Jun 2025 14:00:39 -0700 Subject: [PATCH 490/642] macos: complete more ax APIs for terminal accessibility --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 57fa56e77..a47dbdaca 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1921,6 +1921,59 @@ extension Ghostty.SurfaceView { let str = String(cString: text.text) return str.isEmpty ? nil : str } + + /// Returns the number of characters in the terminal content. + /// This helps assistive technologies understand the size of the content. + override func accessibilityNumberOfCharacters() -> Int { + let content = cachedScreenContents.get() + return content.count + } + + /// Returns the visible character range for the terminal. + /// For terminals, we typically show all content as visible. + override func accessibilityVisibleCharacterRange() -> NSRange { + let content = cachedScreenContents.get() + return NSRange(location: 0, length: content.count) + } + + /// Returns the line number for a given character index. + /// This helps assistive technologies navigate by line. + override func accessibilityLine(for index: Int) -> Int { + let content = cachedScreenContents.get() + let substring = String(content.prefix(index)) + return substring.components(separatedBy: .newlines).count - 1 + } + + /// Returns a substring for the given range. + /// This allows assistive technologies to read specific portions of the content. + override func accessibilityString(for range: NSRange) -> String? { + let content = cachedScreenContents.get() + guard let swiftRange = Range(range, in: content) else { return nil } + return String(content[swiftRange]) + } + + /// Returns an attributed string for the given range. + /// + /// Note: right now this only applies font information. One day it'd be nice to extend + /// this to copy styling information as well but we need to augment Ghostty core to + /// expose that. + /// + /// This provides styling information to assistive technologies. + override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? { + guard let surface = self.surface else { return nil } + guard let plainString = accessibilityString(for: range) else { return nil } + + var attributes: [NSAttributedString.Key: Any] = [:] + + // Try to get the font from the surface + if let fontRaw = ghostty_surface_quicklook_font(surface) { + let font = Unmanaged.fromOpaque(fontRaw) + attributes[.font] = font.takeUnretainedValue() + font.release() + } + + return NSAttributedString(string: plainString, attributes: attributes) + } } /// Caches a value for some period of time, evicting it automatically when that time expires. From b629f3337a147f7a2f77f801b6fd621c90cfb4d2 Mon Sep 17 00:00:00 2001 From: Jon Parise Date: Mon, 16 Jun 2025 19:32:42 -0400 Subject: [PATCH 491/642] bash: remove dependency on $GHOSTTY_RESOURCES_DIR We were depending on $GHOSTTY_RESOURCES_DIR for two reasons: 1. To locate our script-adjacent bash-preexec.sh script 2. To restrict our script's execution to environments in which $GHOSTTY_RESOURCES_DIR is available (i.e. Ghostty-only shells) For (1), we can instead determine our directory using $BASH_SOURCE[0]. This is slightly differently than our previous behavior, where we'd always load bash-preexec.sh from the $GHOSTTY_RESOURCES_DIR hierarchy, even if ghostty.bash from source from somewhere else on the file system ... but we never relied on that behavior, even in development. For (2), there's no harm in source'ing this script outside of Ghostty, and if that does become a concern, we can restore this condition or use something more targeted based on those specific cases. Historically, I believe (2) was in place to enable (1), so addressing (1) removes the need for (2). And lastly, none of the other shell integration scripts depend on $GHOSTTY_RESOURCES_DIR. --- 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 0cfd41663..0766198f9 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. @@ -98,7 +96,7 @@ if [[ "$GHOSTTY_SHELL_FEATURES" == *"sudo"* && -n "$TERMINFO" ]]; 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 6d283c012e19f7b09eb2a07b19133f093b5270a0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 13:34:18 -0700 Subject: [PATCH 492/642] ci: build macOS releases with Xcode 26 Resolves #7591 This moves our CI to build macOS on Sequoia (macOS 15) with Xcode 26, including the new macOS 26 beta SDK. Importantly, this will make our builds on macOS 26 use the new styling. I've added a new job that ensures we can continue to build with Xcode 16 and the macOS 15 SDK, as well, although I think that might come to an end when we switch over to an IconComposer-based icon. I'll verify then. For now, we continue to support both. I've also removed our `hasLiquidGlass` check, since this will now always be true for macOS 26 builds. --- .github/workflows/release-pr.yml | 4 +- .github/workflows/release-tip.yml | 6 +- .github/workflows/test.yml | 56 ++++++++++++++----- .../Terminal/TerminalController.swift | 2 +- .../Window Styles/TerminalWindow.swift | 2 +- .../TransparentTitlebarTerminalWindow.swift | 10 ++-- macos/Sources/Helpers/AppInfo.swift | 34 ----------- 7 files changed, 56 insertions(+), 58 deletions(-) diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 3f89bd702..a1cc2af19 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -94,7 +94,7 @@ jobs: - name: Build Ghostty.app run: | cd macos - sudo xcode-select -s /Applications/Xcode_16.4.app + sudo xcode-select -s /Applications/Xcode_26.0.app xcodebuild -target Ghostty -configuration Release # We inject the "build number" as simply the number of commits since HEAD. @@ -246,7 +246,7 @@ jobs: - name: Build Ghostty.app run: | cd macos - sudo xcode-select -s /Applications/Xcode_16.4.app + sudo xcode-select -s /Applications/Xcode_26.0.app xcodebuild -target Ghostty -configuration Release # We inject the "build number" as simply the number of commits since HEAD. diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 6c6399afd..2a3277ea6 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -173,7 +173,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcode-select -s /Applications/Xcode_26.0.app # Setup Sparkle - name: Setup Sparkle @@ -388,7 +388,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcode-select -s /Applications/Xcode_26.0.app # Setup Sparkle - name: Setup Sparkle @@ -563,7 +563,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: XCode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcode-select -s /Applications/Xcode_26.0.app # Setup Sparkle - name: Setup Sparkle diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 814acec8f..2eca0a41e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,6 +18,7 @@ jobs: - build-nix - build-snap - build-macos + - build-macos-sequoia-stable - build-macos-tahoe - build-macos-matrix - build-windows @@ -270,6 +271,46 @@ jobs: ghostty-source.tar.gz build-macos: + runs-on: namespace-profile-ghostty-macos-sequoia + needs: test + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Install Nix and use that to run our tests so our environment matches exactly. + - uses: cachix/install-nix-action@v31 + with: + nix_path: nixpkgs=channel:nixos-unstable + - uses: cachix/cachix-action@v16 + with: + name: ghostty + authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" + + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_26.0.app + + - name: get the Zig deps + id: deps + run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT + + # GhosttyKit is the framework that is built from Zig for our native + # Mac app to access. + - name: Build GhosttyKit + run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} + + # The native app is built with native Xcode tooling. This also does + # codesigning. IMPORTANT: this must NOT run in a Nix environment. + # Nix breaks xcodebuild so this has to be run outside. + - name: Build Ghostty.app + run: cd macos && xcodebuild -target Ghostty + + # Build the iOS target without code signing just to verify it works. + - name: Build Ghostty iOS + run: | + cd macos + xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" + + build-macos-sequoia-stable: runs-on: namespace-profile-ghostty-macos-sequoia needs: test steps: @@ -328,17 +369,6 @@ jobs: - name: Xcode Select run: sudo xcode-select -s /Applications/Xcode_26.0.app - # TODO(tahoe): - # https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes#Interface-Builder - # We allow this step to fail because if our image already has - # the workaround in place this will fail. - - name: Xcode 26 Beta 17A5241e Metal Workaround - continue-on-error: true - run: | - xcodebuild -downloadComponent metalToolchain -exportPath /tmp/MyMetalExport/ - sed -i '' -e 's/17A5241c/17A5241e/g' /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle/ExportMetadata.plist - xcodebuild -importComponent metalToolchain -importPath /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle - - name: get the Zig deps id: deps run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT @@ -377,7 +407,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcode-select -s /Applications/Xcode_26.0.app - name: get the Zig deps id: deps @@ -695,7 +725,7 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app + run: sudo xcode-select -s /Applications/Xcode_26.0.app - name: get the Zig deps id: deps diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 49b3fea34..03a4e548e 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -16,7 +16,7 @@ class TerminalController: BaseTerminalController { case "hidden": "TerminalHiddenTitlebar" case "transparent": "TerminalTransparentTitlebar" case "tabs": - if #available(macOS 26.0, *), hasLiquidGlass() { + if #available(macOS 26.0, *) { "TerminalTabsTitlebarTahoe" } else { "TerminalTabsTitlebarVentura" diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index e24323113..f9dfb9591 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -447,7 +447,7 @@ extension TerminalWindow { // The padding from the top that the view appears. This was all just manually // measured based on the OS. var topPadding: CGFloat { - if #available(macOS 26.0, *), hasLiquidGlass() { + if #available(macOS 26.0, *) { return viewModel.hasToolbar ? 10 : 5 } else { return viewModel.hasToolbar ? 9 : 4 diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index 0d064a7f7..f6ad6e56c 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -45,11 +45,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { override func update() { super.update() - + // On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our // titlebar to be truly transparent. - if !effectViewIsHidden && !hasLiquidGlass() { - hideEffectView() + if #unavailable(macOS 26) { + if !effectViewIsHidden { + hideEffectView() + } } } @@ -65,7 +67,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // references changed (e.g. tabGroup is new). setupKVO() - if #available(macOS 26.0, *), hasLiquidGlass() { + if #available(macOS 26.0, *) { syncAppearanceTahoe(surfaceConfig) } else { syncAppearanceVentura(surfaceConfig) diff --git a/macos/Sources/Helpers/AppInfo.swift b/macos/Sources/Helpers/AppInfo.swift index cf66e332d..281bad18b 100644 --- a/macos/Sources/Helpers/AppInfo.swift +++ b/macos/Sources/Helpers/AppInfo.swift @@ -8,37 +8,3 @@ func isRunningInXcode() -> Bool { return false } - -/// True if we have liquid glass available. -func hasLiquidGlass() -> Bool { - // Can't have liquid glass unless we're in macOS 26+ - if #unavailable(macOS 26.0) { - return false - } - - // If we aren't running SDK 26.0 or later then we definitely - // do not have liquid glass. - guard let sdkName = Bundle.main.infoDictionary?["DTSDKName"] as? String else { - // If we don't have this, we assume we're built against the latest - // since we're on macOS 26+ - return true - } - - // If the SDK doesn't start with macosx then we just assume we - // have it because we already verified we're on macOS above. - guard sdkName.hasPrefix("macosx") else { - return true - } - - // The SDK version must be at least 26 - let versionString = String(sdkName.dropFirst("macosx".count)) - guard let major = if let dotIndex = versionString.firstIndex(of: ".") { - Int(String(versionString[..= 26 -} From e6c77789d341742aa80d7387cfba67bba9843b75 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 15:02:59 -0700 Subject: [PATCH 493/642] macOS: Confirm close on window close Fixes #7615 We were incorrectly closing the window without confirmation when there were no tabs. --- .../Features/Terminal/TerminalController.swift | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03a4e548e..2e4fb7363 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1001,20 +1001,14 @@ class TerminalController: BaseTerminalController { @IBAction override func closeWindow(_ sender: Any?) { guard let window = window else { return } - guard let tabGroup = window.tabGroup else { - // No tabs, no tab group, just perform a normal close. - closeWindowImmediately() - return - } - // If have one window then we just do a normal close - if tabGroup.windows.count == 1 { - closeWindowImmediately() - return - } + // We need to check all the windows in our tab group for confirmation + // if we're closing the window. If we don't have a tabgroup for any + // reason we check ourselves. + let windows: [NSWindow] = window.tabGroup?.windows ?? [window] // Check if any windows require close confirmation. - let needsConfirm = tabGroup.windows.contains { tabWindow in + let needsConfirm = windows.contains { tabWindow in guard let controller = tabWindow.windowController as? TerminalController else { return false } From 51b9fa751a13fab2884e37292159379372f4da91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 16:13:23 -0700 Subject: [PATCH 494/642] macos: disambiguate close tab vs close window for confirmation This fixes an issue where pressing the red close button in a window or the "x" button on a tab couldn't differentiate and would always close the tab or close the window (depending on tab counts). It seems like in both cases, AppKit triggers the `windowShouldClose` delegate method on the controller, but for the close window case it triggers this on ALL the windows in the group, not just the one that was clicked. I implemented a kind of silly coordinator that debounces `windowShouldClose` calls over 100ms and uses that to differentiate between the two cases. --- macos/Ghostty.xcodeproj/project.pbxproj | 6 +- .../Terminal/TerminalController.swift | 21 +-- .../Extensions/NSWindow+Extension.swift | 6 + .../Helpers/TabGroupCloseCoordinator.swift | 124 ++++++++++++++++++ 4 files changed, 148 insertions(+), 9 deletions(-) create mode 100644 macos/Sources/Helpers/TabGroupCloseCoordinator.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a5663202b..5c584709e 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -119,6 +119,7 @@ A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; }; A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; }; A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; + A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -238,6 +239,7 @@ A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = ""; }; A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; }; + A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroupCloseCoordinator.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -320,12 +322,13 @@ A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */, A5CBD0572C9F30860017A1AE /* Cursor.swift */, A5D0AF3C2B37804400D21823 /* CodableBridge.swift */, + A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */, A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */, A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, - A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */, + A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */, A5CA378D2D31D6C100931030 /* Weak.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, C1F26EE82B76CBFC00404083 /* VibrantLayer.m */, @@ -792,6 +795,7 @@ A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, + A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */, A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */, diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 03a4e548e..01ed25e63 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -5,7 +5,7 @@ import Combine import GhosttyKit /// A classic, tabbed terminal experience. -class TerminalController: BaseTerminalController { +class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller { override var windowNibName: NSNib.Name? { let defaultValue = "Terminal" @@ -882,14 +882,20 @@ class TerminalController: BaseTerminalController { ghostty.newTab(surface: surface) } - //MARK: - NSWindowDelegate + // MARK: NSWindowDelegate + + // TabGroupCloseCoordinator.Controller + lazy private(set) var tabGroupCloseCoordinator = TabGroupCloseCoordinator() override func windowShouldClose(_ sender: NSWindow) -> Bool { - // If we have tabs, then this should only close the tab. - if window?.tabGroup?.windows.count ?? 0 > 1 { - closeTab(sender) - } else { - closeWindow(sender) + tabGroupCloseCoordinator.windowShouldClose(sender) { [weak self] scope in + guard let self else { return } + switch (scope) { + case .tab: closeTab(nil) + case .window: + guard self.window?.isFirstWindowInTabGroup ?? false else { return } + closeWindow(nil) + } } // We will always explicitly close the window using the above @@ -1270,4 +1276,3 @@ extension TerminalController: NSMenuItemValidation { } } } - diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index 06a9fa4e0..f9ed364aa 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -9,4 +9,10 @@ extension NSWindow { guard windowNumber > 0 else { return nil } return CGWindowID(windowNumber) } + + /// True if this is the first window in the tab group. + var isFirstWindowInTabGroup: Bool { + guard let firstWindow = tabGroup?.windows.first else { return true } + return firstWindow === self + } } diff --git a/macos/Sources/Helpers/TabGroupCloseCoordinator.swift b/macos/Sources/Helpers/TabGroupCloseCoordinator.swift new file mode 100644 index 000000000..ca41bf89c --- /dev/null +++ b/macos/Sources/Helpers/TabGroupCloseCoordinator.swift @@ -0,0 +1,124 @@ +import AppKit + +/// Coordinates close operations for windows that are part of a tab group. +/// +/// This coordinator helps distinguish between closing a single tab versus closing +/// an entire window (with all its tabs). When macOS native tabs are used, close +/// operations can be ambiguous - this coordinator tracks close requests across +/// multiple windows in a tab group to determine the user's intent. +class TabGroupCloseCoordinator { + /// The scope of a close operation. + enum CloseScope { + case tab + case window + } + + /// Protocol that window controllers must implement to use the coordinator. + protocol Controller { + /// The tab group close coordinator instance for this controller. + var tabGroupCloseCoordinator: TabGroupCloseCoordinator { get } + } + + /// Callback type for close operations. + typealias Callback = (CloseScope) -> Void + + // We use weak vars and ObjectIdentifiers below because we don't want to + // create any strong reference cycles during coordination. + + /// The tab group being coordinated. Weak reference to avoid cycles. + private weak var tabGroup: NSWindowTabGroup? + + /// Map of window identifiers to their close callbacks. + private var closeRequests: [ObjectIdentifier: Callback] = [:] + + /// Timer used to debounce close requests and determine intent. + private var debounceTimer: Timer? + + deinit { + trigger(.tab) + } + + /// Call this from the windowShouldClose override in order to track whether + /// a window close event is from a tab or a window. If this window already + /// requested a close then only the latest will be called. + func windowShouldClose( + _ window: NSWindow, + callback: @escaping Callback + ) { + // If this window isn't part of a tab group we assume its a window + // close for the window and let our timer keep running for the rest. + guard let tabGroup = window.tabGroup else { + callback(.window) + return + } + + // Forward to the proper coordinator + if let firstController = tabGroup.windows.first?.windowController as? Controller, + firstController.tabGroupCloseCoordinator !== self { + let coordinator = firstController.tabGroupCloseCoordinator + coordinator.windowShouldClose(window, callback: callback) + return + } + + // If our tab group is nil then we either are seeing this for the first + // time or our weak ref expired and we should fire our callbacks. + if self.tabGroup == nil { + self.tabGroup = tabGroup + debounceTimer?.fire() + debounceTimer = nil + } + + // No matter what, we cancel our debounce and restart this. This opens + // us up to a DoS if close requests are looped but this would only + // happen in hostile scenarios that are self-inflicted. + debounceTimer?.invalidate() + debounceTimer = nil + + // If this tab group doesn't match then I don't really know what to + // do. This shouldn't happen. So we just assume it's a tab close + // and trigger the rest. No right answer here as far as I know. + if self.tabGroup != tabGroup { + callback(.tab) + trigger(.tab) + return + } + + // Add the request + closeRequests[ObjectIdentifier(window)] = callback + + // If close requests matches all our windows then we are done. + if closeRequests.count == tabGroup.windows.count { + let allWindows = Set(tabGroup.windows.map { ObjectIdentifier($0) }) + if Set(closeRequests.keys) == allWindows { + trigger(.window) + return + } + } + + // Setup our new timer + debounceTimer = Timer.scheduledTimer( + withTimeInterval: Duration.milliseconds(100).timeInterval, + repeats: false + ) { [weak self] _ in + self?.trigger(.tab) + } + } + + /// Triggers all pending close callbacks with the given scope. + /// + /// This method is called when the coordinator has determined the user's intent + /// (either closing a tab or the entire window). It executes all pending callbacks + /// and resets the coordinator's state. + /// + /// - Parameter scope: The determined scope of the close operation. + private func trigger(_ scope: CloseScope) { + // Reset our state + tabGroup = nil + debounceTimer?.invalidate() + debounceTimer = nil + + // Trigger all of our callbacks + closeRequests.forEach { $0.value(scope) } + closeRequests = [:] + } +} From 559fd922959905e12dabfdecf8b2a78db8ecda22 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 16:23:15 -0700 Subject: [PATCH 495/642] build: use `xcrun --sdk metal` for metal paths This wasn't working before but it just requires a restart of the machine for the changes to take effect. The namespace runners have this prebuilt so this should work now. The other workaround was flaky for unknown reasons so I'd prefer to go back to this. --- src/build/MetallibStep.zig | 33 +++++++-------------------------- 1 file changed, 7 insertions(+), 26 deletions(-) diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index bac3a72c5..b7405c496 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -22,10 +22,11 @@ step: *Step, output: LazyPath, pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { - switch (opts.target.result.os.tag) { - .macos, .ios => {}, - else => return null, // Only macOS and iOS are supported. - } + const sdk = switch (opts.target.result.os.tag) { + .macos => "macosx", + .ios => "iphoneos", + else => return null, + }; const self = b.allocator.create(MetallibStep) catch @panic("OOM"); @@ -37,31 +38,11 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { else => unreachable, }; - // Find the metal and metallib executables. The Apple docs - // at the time of writing (June 2025) say to use - // `xcrun --sdk metal` but this doesn't work with Xcode 26. - // - // I don't know if this is a bug but the xcodebuild approach also - // works with Xcode 15 so it seems safe to use this instead. - // - // Reported bug: FB17874042. - var code: u8 = undefined; - const metal_exe = std.mem.trim(u8, b.runAllowFail( - &.{ "xcodebuild", "-find-executable", "metal" }, - &code, - .Ignore, - ) catch return null, "\r\n "); - const metallib_exe = std.mem.trim(u8, b.runAllowFail( - &.{ "xcodebuild", "-find-executable", "metallib" }, - &code, - .Ignore, - ) catch return null, "\r\n "); - const run_ir = RunStep.create( b, b.fmt("metal {s}", .{opts.name}), ); - run_ir.addArgs(&.{ metal_exe, "-o" }); + run_ir.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metal", "-o" }); const output_ir = run_ir.addOutputFileArg(b.fmt("{s}.ir", .{opts.name})); run_ir.addArgs(&.{"-c"}); for (opts.sources) |source| run_ir.addFileArg(source); @@ -81,7 +62,7 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { b, b.fmt("metallib {s}", .{opts.name}), ); - run_lib.addArgs(&.{ metallib_exe, "-o" }); + run_lib.addArgs(&.{ "/usr/bin/xcrun", "-sdk", sdk, "metallib", "-o" }); const output_lib = run_lib.addOutputFileArg(b.fmt("{s}.metallib", .{opts.name})); run_lib.addFileArg(output_ir); run_lib.step.dependOn(&run_ir.step); From 7d2da23021921551e977413c87ddc675a1a7beb9 Mon Sep 17 00:00:00 2001 From: Ken VanDine Date: Wed, 18 Jun 2025 10:06:35 -0400 Subject: [PATCH 496/642] snap: vendor libgtk4-layer-shell.so --- snap/snapcraft.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index b57411a6c..d7fc63712 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -72,8 +72,6 @@ parts: build-packages: - libgtk-4-dev - libadwaita-1-dev - # TODO: Add when the Snap is updated to Ubuntu 24.10+ - # - gtk4-layer-shell - libxml2-utils - git - patchelf @@ -82,7 +80,10 @@ parts: craftctl set version=$(cat VERSION) $CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline cp -rp zig-out/* $CRAFT_PART_INSTALL/ - sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop + # Install libgtk4-layer-shell.so + mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR + cp .zig-cache/*/*/libgtk4-layer-shell.so $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/ + sed -i 's|Icon=com.mitchellh.ghostty|Icon=${SNAP}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop libs: plugin: nil From b89cb59d792ac0a6b6eec52ae517b3ae3311e66b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Fri, 20 Jun 2025 10:23:10 -0300 Subject: [PATCH 497/642] translation(pt_BR): add missing translation --- po/pt_BR.UTF-8.po | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index d2ba0e693..c7bdf4df7 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -9,15 +9,16 @@ 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" -"PO-Revision-Date: 2025-03-28 11:04-0300\n" -"Last-Translator: Gustavo Peres \n" -"Language-Team: Brazilian Portuguese \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: pt_BR\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" +"X-Generator: Poedit 3.6\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -174,8 +175,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " -"da área de transferência está sendo exibido abaixo." +"Uma aplicação está tentando ler da área de transferência. O conteúdo atual da " +"área de transferência está sendo exibido abaixo." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -189,8 +190,8 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 msgid "" -"An application is attempting to write to the clipboard. The current " -"clipboard contents are shown below." +"An application is attempting to write to the clipboard. The current clipboard " +"contents are shown below." msgstr "" "Uma aplicação está tentando escrever na área de transferência. O conteúdo " "atual da área de transferência está aparecendo abaixo." @@ -217,11 +218,10 @@ msgstr "Visualizar abas abertas" #: src/apprt/gtk/Window.zig:249 msgid "New Split" -msgstr "" +msgstr "Nova divisão" #: src/apprt/gtk/Window.zig:312 -msgid "" -"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgid "⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" "⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." From fda08a699987c2caf8585e66d4f2111c369f855b Mon Sep 17 00:00:00 2001 From: Zhaofeng Li Date: Fri, 20 Jun 2025 14:02:07 -0600 Subject: [PATCH 498/642] build: Use correct SDK for iOS Simulator shader build --- src/build/MetallibStep.zig | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/build/MetallibStep.zig b/src/build/MetallibStep.zig index b7405c496..1067e519c 100644 --- a/src/build/MetallibStep.zig +++ b/src/build/MetallibStep.zig @@ -24,9 +24,20 @@ output: LazyPath, pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { const sdk = switch (opts.target.result.os.tag) { .macos => "macosx", - .ios => "iphoneos", + .ios => switch (opts.target.result.abi) { + .simulator => "iphonesimulator", + else => "iphoneos", + }, else => return null, }; + const platform_version_arg = switch (opts.target.result.os.tag) { + .macos => "-mmacos-version-min", + .ios => switch (opts.target.result.abi) { + .simulator => "-mios-simulator-version-min", + else => "-mios-version-min", + }, + else => null, + }; const self = b.allocator.create(MetallibStep) catch @panic("OOM"); @@ -46,16 +57,11 @@ pub fn create(b: *std.Build, opts: Options) ?*MetallibStep { const output_ir = run_ir.addOutputFileArg(b.fmt("{s}.ir", .{opts.name})); run_ir.addArgs(&.{"-c"}); for (opts.sources) |source| run_ir.addFileArg(source); - switch (opts.target.result.os.tag) { - .ios => run_ir.addArgs(&.{b.fmt( - "-mios-version-min={s}", - .{min_version}, - )}), - .macos => run_ir.addArgs(&.{b.fmt( - "-mmacos-version-min={s}", - .{min_version}, - )}), - else => {}, + if (platform_version_arg) |arg| { + run_ir.addArgs(&.{b.fmt( + "{s}={s}", + .{ arg, min_version }, + )}); } const run_lib = RunStep.create( From f40cd3cae3469da4c5a70e637c3243d7a4c2f804 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 19 Jan 2025 16:47:08 -0500 Subject: [PATCH 499/642] chore: improve Metal API definitions a bit --- src/renderer/Metal.zig | 16 ++- src/renderer/metal/api.zig | 225 +++++++++++++++++++++++++++++++++++-- 2 files changed, 228 insertions(+), 13 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 99dbc838e..639ef354b 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -2337,9 +2337,11 @@ pub fn setScreenSize( desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); desc.setProperty( "usage", - @intFromEnum(mtl.MTLTextureUsage.render_target) | - @intFromEnum(mtl.MTLTextureUsage.shader_read) | - @intFromEnum(mtl.MTLTextureUsage.shader_write), + mtl.MTLTextureUsage{ + .render_target = true, + .shader_read = true, + .shader_write = true, + }, ); // If we fail to create the texture, then we just don't have a screen @@ -2377,9 +2379,11 @@ pub fn setScreenSize( desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); desc.setProperty( "usage", - @intFromEnum(mtl.MTLTextureUsage.render_target) | - @intFromEnum(mtl.MTLTextureUsage.shader_read) | - @intFromEnum(mtl.MTLTextureUsage.shader_write), + mtl.MTLTextureUsage{ + .render_target = true, + .shader_read = true, + .shader_write = true, + }, ); // If we fail to create the texture, then we just don't have a screen diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 46cb4f6bc..90a1a65ab 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -1,4 +1,10 @@ //! This file contains the definitions of the Metal API that we use. +//! +//! Because the online Apple developer docs have recently (as of January 2025) +//! been changed to hide enum values, `Metal-cpp` has been used as a reference +//! source instead. +//! +//! Ref: https://developer.apple.com/metal/cpp/ /// https://developer.apple.com/documentation/metal/mtlcommandbufferstatus?language=objc pub const MTLCommandBufferStatus = enum(c_ulong) { @@ -22,6 +28,10 @@ pub const MTLLoadAction = enum(c_ulong) { pub const MTLStoreAction = enum(c_ulong) { dont_care = 0, store = 1, + multisample_resolve = 2, + store_and_multisample_resolve = 3, + unknown = 4, + custom_sample_depth_store = 5, }; /// https://developer.apple.com/documentation/metal/mtlresourceoptions?language=objc @@ -73,16 +83,60 @@ pub const MTLIndexType = enum(c_ulong) { /// https://developer.apple.com/documentation/metal/mtlvertexformat?language=objc pub const MTLVertexFormat = enum(c_ulong) { + invalid = 0, + uchar2 = 1, + uchar3 = 2, uchar4 = 3, + char2 = 4, + char3 = 5, + char4 = 6, + uchar2normalized = 7, + uchar3normalized = 8, + uchar4normalized = 9, + char2normalized = 10, + char3normalized = 11, + char4normalized = 12, ushort2 = 13, + ushort3 = 14, + ushort4 = 15, short2 = 16, + short3 = 17, + short4 = 18, + ushort2normalized = 19, + ushort3normalized = 20, + ushort4normalized = 21, + short2normalized = 22, + short3normalized = 23, + short4normalized = 24, + half2 = 25, + half3 = 26, + half4 = 27, + float = 28, float2 = 29, + float3 = 30, float4 = 31, + int = 32, int2 = 33, + int3 = 34, + int4 = 35, uint = 36, uint2 = 37, + uint3 = 38, uint4 = 39, + int1010102normalized = 40, + uint1010102normalized = 41, + uchar4normalized_bgra = 42, uchar = 45, + char = 46, + ucharnormalized = 47, + charnormalized = 48, + ushort = 49, + short = 50, + ushortnormalized = 51, + shortnormalized = 52, + half = 53, + floatrg11b10 = 54, + floatrgb9e5 = 55, }; /// https://developer.apple.com/documentation/metal/mtlvertexstepfunction?language=objc @@ -90,20 +144,158 @@ pub const MTLVertexStepFunction = enum(c_ulong) { constant = 0, per_vertex = 1, per_instance = 2, + per_patch = 3, + per_patch_control_point = 4, }; /// https://developer.apple.com/documentation/metal/mtlpixelformat?language=objc pub const MTLPixelFormat = enum(c_ulong) { + invalid = 0, + a8unorm = 1, r8unorm = 10, + r8unorm_srgb = 11, + r8snorm = 12, + r8uint = 13, + r8sint = 14, + r16unorm = 20, + r16snorm = 22, + r16uint = 23, + r16sint = 24, + r16float = 25, + rg8unorm = 30, + rg8unorm_srgb = 31, + rg8snorm = 32, + rg8uint = 33, + rg8sint = 34, + b5g6r5unorm = 40, + a1bgr5unorm = 41, + abgr4unorm = 42, + bgr5a1unorm = 43, + r32uint = 53, + r32sint = 54, + r32float = 55, + rg16unorm = 60, + rg16snorm = 62, + rg16uint = 63, + rg16sint = 64, + rg16float = 65, rgba8unorm = 70, rgba8unorm_srgb = 71, + rgba8snorm = 72, rgba8uint = 73, + rgba8sint = 74, bgra8unorm = 80, bgra8unorm_srgb = 81, + rgb10a2unorm = 90, + rgb10a2uint = 91, + rg11b10float = 92, + rgb9e5float = 93, + bgr10a2unorm = 94, + bgr10_xr = 554, + bgr10_xr_srgb = 555, + rg32uint = 103, + rg32sint = 104, + rg32float = 105, + rgba16unorm = 110, + rgba16snorm = 112, + rgba16uint = 113, + rgba16sint = 114, + rgba16float = 115, + bgra10_xr = 552, + bgra10_xr_srgb = 553, + rgba32uint = 123, + rgba32sint = 124, + rgba32float = 125, + bc1_rgba = 130, + bc1_rgba_srgb = 131, + bc2_rgba = 132, + bc2_rgba_srgb = 133, + bc3_rgba = 134, + bc3_rgba_srgb = 135, + bc4_runorm = 140, + bc4_rsnorm = 141, + bc5_rgunorm = 142, + bc5_rgsnorm = 143, + bc6h_rgbfloat = 150, + bc6h_rgbufloat = 151, + bc7_rgbaunorm = 152, + bc7_rgbaunorm_srgb = 153, + pvrtc_rgb_2bpp = 160, + pvrtc_rgb_2bpp_srgb = 161, + pvrtc_rgb_4bpp = 162, + pvrtc_rgb_4bpp_srgb = 163, + pvrtc_rgba_2bpp = 164, + pvrtc_rgba_2bpp_srgb = 165, + pvrtc_rgba_4bpp = 166, + pvrtc_rgba_4bpp_srgb = 167, + eac_r11unorm = 170, + eac_r11snorm = 172, + eac_rg11unorm = 174, + eac_rg11snorm = 176, + eac_rgba8 = 178, + eac_rgba8_srgb = 179, + etc2_rgb8 = 180, + etc2_rgb8_srgb = 181, + etc2_rgb8a1 = 182, + etc2_rgb8a1_srgb = 183, + astc_4x4_srgb = 186, + astc_5x4_srgb = 187, + astc_5x5_srgb = 188, + astc_6x5_srgb = 189, + astc_6x6_srgb = 190, + astc_8x5_srgb = 192, + astc_8x6_srgb = 193, + astc_8x8_srgb = 194, + astc_10x5_srgb = 195, + astc_10x6_srgb = 196, + astc_10x8_srgb = 197, + astc_10x10_srgb = 198, + astc_12x10_srgb = 199, + astc_12x12_srgb = 200, + astc_4x4_ldr = 204, + astc_5x4_ldr = 205, + astc_5x5_ldr = 206, + astc_6x5_ldr = 207, + astc_6x6_ldr = 208, + astc_8x5_ldr = 210, + astc_8x6_ldr = 211, + astc_8x8_ldr = 212, + astc_10x5_ldr = 213, + astc_10x6_ldr = 214, + astc_10x8_ldr = 215, + astc_10x10_ldr = 216, + astc_12x10_ldr = 217, + astc_12x12_ldr = 218, + astc_4x4_hdr = 222, + astc_5x4_hdr = 223, + astc_5x5_hdr = 224, + astc_6x5_hdr = 225, + astc_6x6_hdr = 226, + astc_8x5_hdr = 228, + astc_8x6_hdr = 229, + astc_8x8_hdr = 230, + astc_10x5_hdr = 231, + astc_10x6_hdr = 232, + astc_10x8_hdr = 233, + astc_10x10_hdr = 234, + astc_12x10_hdr = 235, + astc_12x12_hdr = 236, + gbgr422 = 240, + bgrg422 = 241, + depth16unorm = 250, + depth32float = 252, + stencil8 = 253, + depth24unorm_stencil8 = 255, + depth32float_stencil8 = 260, + x32_stencil8 = 261, + x24_stencil8 = 262, }; /// https://developer.apple.com/documentation/metal/mtlpurgeablestate?language=objc pub const MTLPurgeableState = enum(c_ulong) { + keep_current = 1, + non_volatile = 2, + @"volatile" = 3, empty = 4, }; @@ -155,13 +347,32 @@ pub const MTLBlendOperation = enum(c_ulong) { max = 4, }; -/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc -pub const MTLTextureUsage = enum(c_ulong) { - unknown = 0, - shader_read = 1, - shader_write = 2, - render_target = 4, - pixel_format_view = 8, +/// https://developer.apple.com/documentation/metal/mtltextureusage?language=objc +pub const MTLTextureUsage = packed struct(c_ulong) { + /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderread?language=objc + shader_read: bool = false, // TextureUsageShaderRead = 1, + + /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderwrite?language=objc + shader_write: bool = false, // TextureUsageShaderWrite = 2, + + /// https://developer.apple.com/documentation/metal/mtltextureusage/rendertarget?language=objc + render_target: bool = false, // TextureUsageRenderTarget = 4, + + _reserved: u1 = 0, // The enum skips from 4 to 16, 8 has no documented use. + + /// https://developer.apple.com/documentation/metal/mtltextureusage/pixelformatview?language=objc + pixel_format_view: bool = false, // TextureUsagePixelFormatView = 16, + + /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc + shader_atomic: bool = false, // TextureUsageShaderAtomic = 32, + + __reserved: @Type(.{ .Int = .{ + .signedness = .unsigned, + .bits = @bitSizeOf(c_ulong) - 6, + } }) = 0, + + /// https://developer.apple.com/documentation/metal/mtltextureusage/unknown?language=objc + const unknown: MTLTextureUsage = @bitCast(0); // TextureUsageUnknown = 0, }; pub const MTLClearColor = extern struct { From 77c050c156945a8bc7f1e46f732e33e842a58fe0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 19 Jan 2025 18:28:36 -0500 Subject: [PATCH 500/642] refactor(Metal): make pipeline handling DRYer --- src/renderer/metal/shaders.zig | 474 +++++++++++---------------------- 1 file changed, 151 insertions(+), 323 deletions(-) diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index 8fa170bf2..ff5f1e6bd 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -106,7 +106,7 @@ pub const Image = extern struct { /// The uniforms that are passed to the terminal cell shader. pub const Uniforms = extern struct { - // Note: all of the explicit aligmnments are copied from the + // Note: all of the explicit alignments are copied from the // MSL developer reference just so that we can be sure that we got // it all exactly right. @@ -171,7 +171,7 @@ pub const Uniforms = extern struct { /// The uniforms used for custom postprocess shaders. pub const PostUniforms = extern struct { - // Note: all of the explicit aligmnments are copied from the + // Note: all of the explicit alignments are copied from the // MSL developer reference just so that we can be sure that we got // it all exactly right. resolution: [3]f32 align(16), @@ -282,65 +282,16 @@ fn initPostPipeline( }; defer post_library.msgSend(void, objc.sel("release"), .{}); - // Get our vertex and fragment functions - const func_vert = func_vert: { - const str = try macos.foundation.String.createWithBytes( - "full_screen_vertex", - .utf8, - false, - ); - defer str.release(); - - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_vert objc.Object.fromId(ptr.?); - }; - const func_frag = func_frag: { - const str = try macos.foundation.String.createWithBytes( - "main0", - .utf8, - false, - ); - defer str.release(); - - const ptr = post_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_frag objc.Object.fromId(ptr.?); - }; - defer func_vert.msgSend(void, objc.sel("release"), .{}); - defer func_frag.msgSend(void, objc.sel("release"), .{}); - - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLRenderPipelineDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer desc.msgSend(void, objc.sel("release"), .{}); - desc.setProperty("vertexFunction", func_vert); - desc.setProperty("fragmentFunction", func_frag); - - // Set our color attachment - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); - } - - // Make our state - var err: ?*anyopaque = null; - const pipeline_state = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, + return (Pipeline{ + .vertex_fn = "full_screen_vertex", + .fragment_fn = "main0", + .blending_enabled = false, + }).init( + device, + library, + post_library, + pixel_format, ); - try checkError(err); - - return pipeline_state; } /// This is a single parameter for the terminal cell shader. @@ -374,113 +325,18 @@ fn initCellTextPipeline( library: objc.Object, pixel_format: mtl.MTLPixelFormat, ) !objc.Object { - // Get our vertex and fragment functions - const func_vert = func_vert: { - const str = try macos.foundation.String.createWithBytes( - "cell_text_vertex", - .utf8, - false, - ); - defer str.release(); - - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_vert objc.Object.fromId(ptr.?); - }; - const func_frag = func_frag: { - const str = try macos.foundation.String.createWithBytes( - "cell_text_fragment", - .utf8, - false, - ); - defer str.release(); - - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_frag objc.Object.fromId(ptr.?); - }; - defer func_vert.msgSend(void, objc.sel("release"), .{}); - defer func_frag.msgSend(void, objc.sel("release"), .{}); - - // Create the vertex descriptor. The vertex descriptor describes the - // data layout of the vertex inputs. We use indexed (or "instanced") - // rendering, so this makes it so that each instance gets a single - // Cell as input. - const vertex_desc = vertex_desc: { - const desc = init: { - const Class = objc.getClass("MTLVertexDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - - // Our attributes are the fields of the input - const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes")); - autoAttribute(CellText, attrs); - - // The layout describes how and when we fetch the next vertex input. - const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); - { - const layout = layouts.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - // Access each Cell per instance, not per vertex. - layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(CellText))); - } - - break :vertex_desc desc; - }; - defer vertex_desc.msgSend(void, objc.sel("release"), .{}); - - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLRenderPipelineDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer desc.msgSend(void, objc.sel("release"), .{}); - - // Set our properties - desc.setProperty("vertexFunction", func_vert); - desc.setProperty("fragmentFunction", func_frag); - desc.setProperty("vertexDescriptor", vertex_desc); - - // Set our color attachment - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); - - // Blending. This is required so that our text we render on top - // of our drawable properly blends into the bg. - attachment.setProperty("blendingEnabled", true); - attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - } - - // Make our state - var err: ?*anyopaque = null; - const pipeline_state = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, + return (Pipeline{ + .vertex_fn = "cell_text_vertex", + .fragment_fn = "cell_text_fragment", + .Vertex = CellText, + .step_fn = .per_instance, + .blending_enabled = true, + }).init( + device, + library, + library, + pixel_format, ); - try checkError(err); - errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - - return pipeline_state; } /// This is a single parameter for the cell bg shader. @@ -492,78 +348,16 @@ fn initCellBgPipeline( library: objc.Object, pixel_format: mtl.MTLPixelFormat, ) !objc.Object { - // Get our vertex and fragment functions - const func_vert = func_vert: { - const str = try macos.foundation.String.createWithBytes( - "cell_bg_vertex", - .utf8, - false, - ); - defer str.release(); - - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_vert objc.Object.fromId(ptr.?); - }; - defer func_vert.msgSend(void, objc.sel("release"), .{}); - const func_frag = func_frag: { - const str = try macos.foundation.String.createWithBytes( - "cell_bg_fragment", - .utf8, - false, - ); - defer str.release(); - - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_frag objc.Object.fromId(ptr.?); - }; - defer func_frag.msgSend(void, objc.sel("release"), .{}); - - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLRenderPipelineDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer desc.msgSend(void, objc.sel("release"), .{}); - - // Set our properties - desc.setProperty("vertexFunction", func_vert); - desc.setProperty("fragmentFunction", func_frag); - - // Set our color attachment - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); - - // Blending. This is required so that our text we render on top - // of our drawable properly blends into the bg. - attachment.setProperty("blendingEnabled", true); - attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - } - - // Make our state - var err: ?*anyopaque = null; - const pipeline_state = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, + return (Pipeline{ + .vertex_fn = "cell_bg_vertex", + .fragment_fn = "cell_bg_fragment", + .blending_enabled = false, + }).init( + device, + library, + library, + pixel_format, ); - try checkError(err); - errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - - return pipeline_state; } /// Initialize the image render pipeline for our shader library. @@ -572,113 +366,147 @@ fn initImagePipeline( library: objc.Object, pixel_format: mtl.MTLPixelFormat, ) !objc.Object { - // Get our vertex and fragment functions - const func_vert = func_vert: { - const str = try macos.foundation.String.createWithBytes( - "image_vertex", - .utf8, - false, - ); - defer str.release(); + return (Pipeline{ + .vertex_fn = "image_vertex", + .fragment_fn = "image_fragment", + .Vertex = Image, + .step_fn = .per_instance, + .blending_enabled = true, + }).init( + device, + library, + library, + pixel_format, + ); +} - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_vert objc.Object.fromId(ptr.?); - }; - const func_frag = func_frag: { - const str = try macos.foundation.String.createWithBytes( - "image_fragment", - .utf8, - false, - ); - defer str.release(); +/// A struct with all the necessary info to initialize a pipeline. +const Pipeline = struct { + /// Name of the vertex function + vertex_fn: []const u8, + /// Name of the fragment function + fragment_fn: []const u8, - const ptr = library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - break :func_frag objc.Object.fromId(ptr.?); - }; - defer func_vert.msgSend(void, objc.sel("release"), .{}); - defer func_frag.msgSend(void, objc.sel("release"), .{}); + /// Vertex attribute struct + Vertex: ?type = null, + /// Vertex step function + step_fn: mtl.MTLVertexStepFunction = .per_vertex, - // Create the vertex descriptor. The vertex descriptor describes the - // data layout of the vertex inputs. We use indexed (or "instanced") - // rendering, so this makes it so that each instance gets a single - // Image as input. - const vertex_desc = vertex_desc: { + /// Whether blending is enabled for the color attachment + blending_enabled: bool = true, + + fn init( + self: *const Pipeline, + device: objc.Object, + vertex_library: objc.Object, + fragment_library: objc.Object, + pixel_format: mtl.MTLPixelFormat, + ) !objc.Object { + // Create our descriptor const desc = init: { - const Class = objc.getClass("MTLVertexDescriptor").?; + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); break :init id_init; }; + defer desc.msgSend(void, objc.sel("release"), .{}); - // Our attributes are the fields of the input - const attrs = objc.Object.fromId(desc.getProperty(?*anyopaque, "attributes")); - autoAttribute(Image, attrs); - - // The layout describes how and when we fetch the next vertex input. - const layouts = objc.Object.fromId(desc.getProperty(?*anyopaque, "layouts")); + // Get our vertex and fragment functions and add them to the descriptor. { - const layout = layouts.msgSend( + const str = try macos.foundation.String.createWithBytes( + self.vertex_fn, + .utf8, + false, + ); + defer str.release(); + + const ptr = vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + const func_vert = objc.Object.fromId(ptr.?); + defer func_vert.msgSend(void, objc.sel("release"), .{}); + + desc.setProperty("vertexFunction", func_vert); + } + { + const str = try macos.foundation.String.createWithBytes( + self.fragment_fn, + .utf8, + false, + ); + defer str.release(); + + const ptr = fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + const func_frag = objc.Object.fromId(ptr.?); + defer func_frag.msgSend(void, objc.sel("release"), .{}); + + desc.setProperty("fragmentFunction", func_frag); + } + + // If we have vertex attributes, create and add a vertex descriptor. + if (self.Vertex) |V| { + const vertex_desc = init: { + const Class = objc.getClass("MTLVertexDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); + + // Our attributes are the fields of the input + const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes")); + autoAttribute(V, attrs); + + // The layout describes how and when we fetch the next vertex input. + const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts")); + { + const layout = layouts.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + layout.setProperty("stepFunction", @intFromEnum(self.step_fn)); + layout.setProperty("stride", @as(c_ulong, @sizeOf(V))); + } + + desc.setProperty("vertexDescriptor", vertex_desc); + } + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + { + const attachment = attachments.msgSend( objc.Object, objc.sel("objectAtIndexedSubscript:"), .{@as(c_ulong, 0)}, ); - // Access each Image per instance, not per vertex. - layout.setProperty("stepFunction", @intFromEnum(mtl.MTLVertexStepFunction.per_instance)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(Image))); + attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); + + attachment.setProperty("blendingEnabled", self.blending_enabled); + // We always use premultiplied alpha blending for now. + if (self.blending_enabled) { + attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + } } - break :vertex_desc desc; - }; - defer vertex_desc.msgSend(void, objc.sel("release"), .{}); - - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLRenderPipelineDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer desc.msgSend(void, objc.sel("release"), .{}); - - // Set our properties - desc.setProperty("vertexFunction", func_vert); - desc.setProperty("fragmentFunction", func_frag); - desc.setProperty("vertexDescriptor", vertex_desc); - - // Set our color attachment - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = device.msgSend( objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, ); + try checkError(err); + errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); - - // Blending. This is required so that our text we render on top - // of our drawable properly blends into the bg. - attachment.setProperty("blendingEnabled", true); - attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + return pipeline_state; } - - // Make our state - var err: ?*anyopaque = null; - const pipeline_state = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, - ); - try checkError(err); - - return pipeline_state; -} +}; fn autoAttribute(T: type, attrs: objc.Object) void { inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| { From 7cfc906c607d94b19613fff17bf261c36f0fca92 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 9 Apr 2025 15:26:28 -0600 Subject: [PATCH 501/642] debug: properly set thread names on macOS --- src/crash/sentry.zig | 7 +++++++ src/os/cf_release_thread.zig | 8 ++++++++ src/os/macos.zig | 4 ++++ src/renderer/Thread.zig | 7 +++++++ src/termio/Exec.zig | 7 +++++++ src/termio/Thread.zig | 8 ++++++++ 6 files changed, 41 insertions(+) diff --git a/src/crash/sentry.zig b/src/crash/sentry.zig index c29184020..820c3e9a1 100644 --- a/src/crash/sentry.zig +++ b/src/crash/sentry.zig @@ -81,6 +81,13 @@ pub fn init(gpa: Allocator) !void { fn initThread(gpa: Allocator) !void { if (comptime !build_options.sentry) return; + // Right now, on Darwin, `std.Thread.setName` can only name the current + // thread, and we have no way to get the current thread from within it, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"sentry-init".*); + } + var arena = std.heap.ArenaAllocator.init(gpa); defer arena.deinit(); const alloc = arena.allocator(); diff --git a/src/os/cf_release_thread.zig b/src/os/cf_release_thread.zig index dbf8e6592..445dc4864 100644 --- a/src/os/cf_release_thread.zig +++ b/src/os/cf_release_thread.zig @@ -8,6 +8,7 @@ const std = @import("std"); const builtin = @import("builtin"); const macos = @import("macos"); +const internal_os = @import("../os/main.zig"); const xev = @import("../global.zig").xev; const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; @@ -119,6 +120,13 @@ pub fn threadMain(self: *Thread) void { fn threadMain_(self: *Thread) !void { defer log.debug("cf release thread exited", .{}); + // Right now, on Darwin, `std.Thread.setName` can only name the current + // thread, and we have no way to get the current thread from within it, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"cf_release".*); + } + // Start the async handlers. We start these first so that they're // registered even if anything below fails so we can drain the mailbox. self.wakeup.wait(&self.loop, &self.wakeup_c, Thread, self, wakeupCallback); diff --git a/src/os/macos.zig b/src/os/macos.zig index ca7c81a47..100d0fe44 100644 --- a/src/os/macos.zig +++ b/src/os/macos.zig @@ -88,6 +88,10 @@ extern "c" fn pthread_set_qos_class_self_np( relative_priority: c_int, ) c_int; +pub extern "c" fn pthread_setname_np( + name: [*:0]const u8, +) void; + pub const NSOperatingSystemVersion = extern struct { major: i64, minor: i64, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 1e9c29b26..52f599549 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -198,6 +198,13 @@ pub fn threadMain(self: *Thread) void { fn threadMain_(self: *Thread) !void { defer log.debug("renderer thread exited", .{}); + // Right now, on Darwin, `std.Thread.setName` can only name the current + // thread, and we have no way to get the current thread from within it, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"renderer".*); + } + // Setup our crash metadata crash.sentry.thread_state = .{ .type = .renderer, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 317ad13b4..aed7cefb6 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1364,6 +1364,13 @@ pub const ReadThread = struct { // Always close our end of the pipe when we exit. defer posix.close(quit); + // Right now, on Darwin, `std.Thread.setName` can only name the current + // thread, and we have no way to get the current thread from within it, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"io-reader".*); + } + // Setup our crash metadata crash.sentry.thread_state = .{ .type = .io, diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index d8018341d..35da3c2d2 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -16,6 +16,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const builtin = @import("builtin"); const xev = @import("../global.zig").xev; const crash = @import("../crash/main.zig"); +const internal_os = @import("../os/main.zig"); const termio = @import("../termio.zig"); const renderer = @import("../renderer.zig"); const BlockingQueue = @import("../datastruct/main.zig").BlockingQueue; @@ -202,6 +203,13 @@ pub fn threadMain(self: *Thread, io: *termio.Termio) void { fn threadMain_(self: *Thread, io: *termio.Termio) !void { defer log.debug("IO thread exited", .{}); + // Right now, on Darwin, `std.Thread.setName` can only name the current + // thread, and we have no way to get the current thread from within it, + // so instead we use this code to name the thread instead. + if (builtin.os.tag.isDarwin()) { + internal_os.macos.pthread_setname_np(&"io".*); + } + // Setup our crash metadata crash.sentry.thread_state = .{ .type = .io, From 521872442a9029031615b0c672781cc46e7fd106 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 18 May 2025 19:39:17 -0600 Subject: [PATCH 502/642] vendor: update glad to OpenGL 4.3 --- vendor/glad/include/glad/gl.h | 884 +++++++++++++++++++++++++++++++- vendor/glad/include/glad/glad.h | 1 - vendor/glad/src/gl.c | 304 +++++++++-- 3 files changed, 1130 insertions(+), 59 deletions(-) delete mode 100644 vendor/glad/include/glad/glad.h diff --git a/vendor/glad/include/glad/gl.h b/vendor/glad/include/glad/gl.h index 2f71276dc..b9b398187 100644 --- a/vendor/glad/include/glad/gl.h +++ b/vendor/glad/include/glad/gl.h @@ -1,5 +1,5 @@ /** - * Loader generated by glad 2.0.0 on Mon Oct 24 00:13:28 2022 + * Loader generated by glad 2.0.8 on Mon May 19 01:37:34 2025 * * SPDX-License-Identifier: (WTFPL OR CC0-1.0) AND Apache-2.0 * @@ -8,7 +8,7 @@ * Extensions: 0 * * APIs: - * - gl:core=3.3 + * - gl:core=4.3 * * Options: * - ALIAS = False @@ -19,10 +19,10 @@ * - ON_DEMAND = False * * Commandline: - * --api='gl:core=3.3' --extensions='' c --loader --mx + * --api='gl:core=4.3' --extensions='' c --loader --mx * * Online: - * http://glad.sh/#api=gl%3Acore%3D3.3&extensions=&generator=c&options=LOADER%2CMX + * http://glad.sh/#api=gl%3Acore%3D4.3&extensions=&generator=c&options=LOADER%2CMX * */ @@ -165,7 +165,7 @@ extern "C" { #define GLAD_VERSION_MAJOR(version) (version / 10000) #define GLAD_VERSION_MINOR(version) (version % 10000) -#define GLAD_GENERATOR_VERSION "2.0.0" +#define GLAD_GENERATOR_VERSION "2.0.8" typedef void (*GLADapiproc)(void); @@ -177,14 +177,25 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #endif /* GLAD_PLATFORM_H_ */ +#define GL_ACTIVE_ATOMIC_COUNTER_BUFFERS 0x92D9 #define GL_ACTIVE_ATTRIBUTES 0x8B89 #define GL_ACTIVE_ATTRIBUTE_MAX_LENGTH 0x8B8A +#define GL_ACTIVE_PROGRAM 0x8259 +#define GL_ACTIVE_RESOURCES 0x92F5 +#define GL_ACTIVE_SUBROUTINES 0x8DE5 +#define GL_ACTIVE_SUBROUTINE_MAX_LENGTH 0x8E48 +#define GL_ACTIVE_SUBROUTINE_UNIFORMS 0x8DE6 +#define GL_ACTIVE_SUBROUTINE_UNIFORM_LOCATIONS 0x8E47 +#define GL_ACTIVE_SUBROUTINE_UNIFORM_MAX_LENGTH 0x8E49 #define GL_ACTIVE_TEXTURE 0x84E0 #define GL_ACTIVE_UNIFORMS 0x8B86 #define GL_ACTIVE_UNIFORM_BLOCKS 0x8A36 #define GL_ACTIVE_UNIFORM_BLOCK_MAX_NAME_LENGTH 0x8A35 #define GL_ACTIVE_UNIFORM_MAX_LENGTH 0x8B87 +#define GL_ACTIVE_VARIABLES 0x9305 #define GL_ALIASED_LINE_WIDTH_RANGE 0x846E +#define GL_ALL_BARRIER_BITS 0xFFFFFFFF +#define GL_ALL_SHADER_BITS 0xFFFFFFFF #define GL_ALPHA 0x1906 #define GL_ALREADY_SIGNALED 0x911A #define GL_ALWAYS 0x0207 @@ -192,9 +203,28 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_AND_INVERTED 0x1504 #define GL_AND_REVERSE 0x1502 #define GL_ANY_SAMPLES_PASSED 0x8C2F +#define GL_ANY_SAMPLES_PASSED_CONSERVATIVE 0x8D6A #define GL_ARRAY_BUFFER 0x8892 #define GL_ARRAY_BUFFER_BINDING 0x8894 +#define GL_ARRAY_SIZE 0x92FB +#define GL_ARRAY_STRIDE 0x92FE +#define GL_ATOMIC_COUNTER_BARRIER_BIT 0x00001000 +#define GL_ATOMIC_COUNTER_BUFFER 0x92C0 +#define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTERS 0x92C5 +#define GL_ATOMIC_COUNTER_BUFFER_ACTIVE_ATOMIC_COUNTER_INDICES 0x92C6 +#define GL_ATOMIC_COUNTER_BUFFER_BINDING 0x92C1 +#define GL_ATOMIC_COUNTER_BUFFER_DATA_SIZE 0x92C4 +#define GL_ATOMIC_COUNTER_BUFFER_INDEX 0x9301 +#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_COMPUTE_SHADER 0x90ED +#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_FRAGMENT_SHADER 0x92CB +#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_GEOMETRY_SHADER 0x92CA +#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_CONTROL_SHADER 0x92C8 +#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_TESS_EVALUATION_SHADER 0x92C9 +#define GL_ATOMIC_COUNTER_BUFFER_REFERENCED_BY_VERTEX_SHADER 0x92C7 +#define GL_ATOMIC_COUNTER_BUFFER_SIZE 0x92C3 +#define GL_ATOMIC_COUNTER_BUFFER_START 0x92C2 #define GL_ATTACHED_SHADERS 0x8B85 +#define GL_AUTO_GENERATE_MIPMAP 0x8295 #define GL_BACK 0x0405 #define GL_BACK_LEFT 0x0402 #define GL_BACK_RIGHT 0x0403 @@ -213,26 +243,34 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_BLEND_SRC 0x0BE1 #define GL_BLEND_SRC_ALPHA 0x80CB #define GL_BLEND_SRC_RGB 0x80C9 +#define GL_BLOCK_INDEX 0x92FD #define GL_BLUE 0x1905 #define GL_BLUE_INTEGER 0x8D96 #define GL_BOOL 0x8B56 #define GL_BOOL_VEC2 0x8B57 #define GL_BOOL_VEC3 0x8B58 #define GL_BOOL_VEC4 0x8B59 +#define GL_BUFFER 0x82E0 #define GL_BUFFER_ACCESS 0x88BB #define GL_BUFFER_ACCESS_FLAGS 0x911F +#define GL_BUFFER_BINDING 0x9302 +#define GL_BUFFER_DATA_SIZE 0x9303 #define GL_BUFFER_MAPPED 0x88BC #define GL_BUFFER_MAP_LENGTH 0x9120 #define GL_BUFFER_MAP_OFFSET 0x9121 #define GL_BUFFER_MAP_POINTER 0x88BD #define GL_BUFFER_SIZE 0x8764 +#define GL_BUFFER_UPDATE_BARRIER_BIT 0x00000200 #define GL_BUFFER_USAGE 0x8765 +#define GL_BUFFER_VARIABLE 0x92E5 #define GL_BYTE 0x1400 +#define GL_CAVEAT_SUPPORT 0x82B8 #define GL_CCW 0x0901 #define GL_CLAMP_READ_COLOR 0x891C #define GL_CLAMP_TO_BORDER 0x812D #define GL_CLAMP_TO_EDGE 0x812F #define GL_CLEAR 0x1500 +#define GL_CLEAR_BUFFER 0x82B4 #define GL_CLIP_DISTANCE0 0x3000 #define GL_CLIP_DISTANCE1 0x3001 #define GL_CLIP_DISTANCE2 0x3002 @@ -276,39 +314,93 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_COLOR_ATTACHMENT9 0x8CE9 #define GL_COLOR_BUFFER_BIT 0x00004000 #define GL_COLOR_CLEAR_VALUE 0x0C22 +#define GL_COLOR_COMPONENTS 0x8283 +#define GL_COLOR_ENCODING 0x8296 #define GL_COLOR_LOGIC_OP 0x0BF2 +#define GL_COLOR_RENDERABLE 0x8286 #define GL_COLOR_WRITEMASK 0x0C23 +#define GL_COMMAND_BARRIER_BIT 0x00000040 #define GL_COMPARE_REF_TO_TEXTURE 0x884E +#define GL_COMPATIBLE_SUBROUTINES 0x8E4B #define GL_COMPILE_STATUS 0x8B81 +#define GL_COMPRESSED_R11_EAC 0x9270 #define GL_COMPRESSED_RED 0x8225 #define GL_COMPRESSED_RED_RGTC1 0x8DBB #define GL_COMPRESSED_RG 0x8226 +#define GL_COMPRESSED_RG11_EAC 0x9272 #define GL_COMPRESSED_RGB 0x84ED +#define GL_COMPRESSED_RGB8_ETC2 0x9274 +#define GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9276 #define GL_COMPRESSED_RGBA 0x84EE +#define GL_COMPRESSED_RGBA8_ETC2_EAC 0x9278 +#define GL_COMPRESSED_RGBA_BPTC_UNORM 0x8E8C +#define GL_COMPRESSED_RGB_BPTC_SIGNED_FLOAT 0x8E8E +#define GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT 0x8E8F #define GL_COMPRESSED_RG_RGTC2 0x8DBD +#define GL_COMPRESSED_SIGNED_R11_EAC 0x9271 #define GL_COMPRESSED_SIGNED_RED_RGTC1 0x8DBC +#define GL_COMPRESSED_SIGNED_RG11_EAC 0x9273 #define GL_COMPRESSED_SIGNED_RG_RGTC2 0x8DBE #define GL_COMPRESSED_SRGB 0x8C48 +#define GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC 0x9279 +#define GL_COMPRESSED_SRGB8_ETC2 0x9275 +#define GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 0x9277 #define GL_COMPRESSED_SRGB_ALPHA 0x8C49 +#define GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM 0x8E8D #define GL_COMPRESSED_TEXTURE_FORMATS 0x86A3 +#define GL_COMPUTE_SHADER 0x91B9 +#define GL_COMPUTE_SHADER_BIT 0x00000020 +#define GL_COMPUTE_SUBROUTINE 0x92ED +#define GL_COMPUTE_SUBROUTINE_UNIFORM 0x92F3 +#define GL_COMPUTE_TEXTURE 0x82A0 +#define GL_COMPUTE_WORK_GROUP_SIZE 0x8267 #define GL_CONDITION_SATISFIED 0x911C #define GL_CONSTANT_ALPHA 0x8003 #define GL_CONSTANT_COLOR 0x8001 #define GL_CONTEXT_COMPATIBILITY_PROFILE_BIT 0x00000002 #define GL_CONTEXT_CORE_PROFILE_BIT 0x00000001 #define GL_CONTEXT_FLAGS 0x821E +#define GL_CONTEXT_FLAG_DEBUG_BIT 0x00000002 #define GL_CONTEXT_FLAG_FORWARD_COMPATIBLE_BIT 0x00000001 #define GL_CONTEXT_PROFILE_MASK 0x9126 #define GL_COPY 0x1503 #define GL_COPY_INVERTED 0x150C #define GL_COPY_READ_BUFFER 0x8F36 +#define GL_COPY_READ_BUFFER_BINDING 0x8F36 #define GL_COPY_WRITE_BUFFER 0x8F37 +#define GL_COPY_WRITE_BUFFER_BINDING 0x8F37 #define GL_CULL_FACE 0x0B44 #define GL_CULL_FACE_MODE 0x0B45 #define GL_CURRENT_PROGRAM 0x8B8D #define GL_CURRENT_QUERY 0x8865 #define GL_CURRENT_VERTEX_ATTRIB 0x8626 #define GL_CW 0x0900 +#define GL_DEBUG_CALLBACK_FUNCTION 0x8244 +#define GL_DEBUG_CALLBACK_USER_PARAM 0x8245 +#define GL_DEBUG_GROUP_STACK_DEPTH 0x826D +#define GL_DEBUG_LOGGED_MESSAGES 0x9145 +#define GL_DEBUG_NEXT_LOGGED_MESSAGE_LENGTH 0x8243 +#define GL_DEBUG_OUTPUT 0x92E0 +#define GL_DEBUG_OUTPUT_SYNCHRONOUS 0x8242 +#define GL_DEBUG_SEVERITY_HIGH 0x9146 +#define GL_DEBUG_SEVERITY_LOW 0x9148 +#define GL_DEBUG_SEVERITY_MEDIUM 0x9147 +#define GL_DEBUG_SEVERITY_NOTIFICATION 0x826B +#define GL_DEBUG_SOURCE_API 0x8246 +#define GL_DEBUG_SOURCE_APPLICATION 0x824A +#define GL_DEBUG_SOURCE_OTHER 0x824B +#define GL_DEBUG_SOURCE_SHADER_COMPILER 0x8248 +#define GL_DEBUG_SOURCE_THIRD_PARTY 0x8249 +#define GL_DEBUG_SOURCE_WINDOW_SYSTEM 0x8247 +#define GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR 0x824D +#define GL_DEBUG_TYPE_ERROR 0x824C +#define GL_DEBUG_TYPE_MARKER 0x8268 +#define GL_DEBUG_TYPE_OTHER 0x8251 +#define GL_DEBUG_TYPE_PERFORMANCE 0x8250 +#define GL_DEBUG_TYPE_POP_GROUP 0x826A +#define GL_DEBUG_TYPE_PORTABILITY 0x824F +#define GL_DEBUG_TYPE_PUSH_GROUP 0x8269 +#define GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR 0x824E #define GL_DECR 0x1E03 #define GL_DECR_WRAP 0x8508 #define GL_DELETE_STATUS 0x8B80 @@ -324,16 +416,33 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_DEPTH_COMPONENT24 0x81A6 #define GL_DEPTH_COMPONENT32 0x81A7 #define GL_DEPTH_COMPONENT32F 0x8CAC +#define GL_DEPTH_COMPONENTS 0x8284 #define GL_DEPTH_FUNC 0x0B74 #define GL_DEPTH_RANGE 0x0B70 +#define GL_DEPTH_RENDERABLE 0x8287 #define GL_DEPTH_STENCIL 0x84F9 #define GL_DEPTH_STENCIL_ATTACHMENT 0x821A +#define GL_DEPTH_STENCIL_TEXTURE_MODE 0x90EA #define GL_DEPTH_TEST 0x0B71 #define GL_DEPTH_WRITEMASK 0x0B72 +#define GL_DISPATCH_INDIRECT_BUFFER 0x90EE +#define GL_DISPATCH_INDIRECT_BUFFER_BINDING 0x90EF #define GL_DITHER 0x0BD0 #define GL_DONT_CARE 0x1100 #define GL_DOUBLE 0x140A #define GL_DOUBLEBUFFER 0x0C32 +#define GL_DOUBLE_MAT2 0x8F46 +#define GL_DOUBLE_MAT2x3 0x8F49 +#define GL_DOUBLE_MAT2x4 0x8F4A +#define GL_DOUBLE_MAT3 0x8F47 +#define GL_DOUBLE_MAT3x2 0x8F4B +#define GL_DOUBLE_MAT3x4 0x8F4C +#define GL_DOUBLE_MAT4 0x8F48 +#define GL_DOUBLE_MAT4x2 0x8F4D +#define GL_DOUBLE_MAT4x3 0x8F4E +#define GL_DOUBLE_VEC2 0x8FFC +#define GL_DOUBLE_VEC3 0x8FFD +#define GL_DOUBLE_VEC4 0x8FFE #define GL_DRAW_BUFFER 0x0C01 #define GL_DRAW_BUFFER0 0x8825 #define GL_DRAW_BUFFER1 0x8826 @@ -353,11 +462,14 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_DRAW_BUFFER9 0x882E #define GL_DRAW_FRAMEBUFFER 0x8CA9 #define GL_DRAW_FRAMEBUFFER_BINDING 0x8CA6 +#define GL_DRAW_INDIRECT_BUFFER 0x8F3F +#define GL_DRAW_INDIRECT_BUFFER_BINDING 0x8F43 #define GL_DST_ALPHA 0x0304 #define GL_DST_COLOR 0x0306 #define GL_DYNAMIC_COPY 0x88EA #define GL_DYNAMIC_DRAW 0x88E8 #define GL_DYNAMIC_READ 0x88E9 +#define GL_ELEMENT_ARRAY_BARRIER_BIT 0x00000002 #define GL_ELEMENT_ARRAY_BUFFER 0x8893 #define GL_ELEMENT_ARRAY_BUFFER_BINDING 0x8895 #define GL_EQUAL 0x0202 @@ -366,7 +478,9 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_FALSE 0 #define GL_FASTEST 0x1101 #define GL_FILL 0x1B02 +#define GL_FILTER 0x829A #define GL_FIRST_VERTEX_CONVENTION 0x8E4D +#define GL_FIXED 0x140C #define GL_FIXED_ONLY 0x891D #define GL_FLOAT 0x1406 #define GL_FLOAT_32_UNSIGNED_INT_24_8_REV 0x8DAD @@ -382,8 +496,15 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_FLOAT_VEC2 0x8B50 #define GL_FLOAT_VEC3 0x8B51 #define GL_FLOAT_VEC4 0x8B52 +#define GL_FRACTIONAL_EVEN 0x8E7C +#define GL_FRACTIONAL_ODD 0x8E7B +#define GL_FRAGMENT_INTERPOLATION_OFFSET_BITS 0x8E5D #define GL_FRAGMENT_SHADER 0x8B30 +#define GL_FRAGMENT_SHADER_BIT 0x00000002 #define GL_FRAGMENT_SHADER_DERIVATIVE_HINT 0x8B8B +#define GL_FRAGMENT_SUBROUTINE 0x92EC +#define GL_FRAGMENT_SUBROUTINE_UNIFORM 0x92F2 +#define GL_FRAGMENT_TEXTURE 0x829F #define GL_FRAMEBUFFER 0x8D40 #define GL_FRAMEBUFFER_ATTACHMENT_ALPHA_SIZE 0x8215 #define GL_FRAMEBUFFER_ATTACHMENT_BLUE_SIZE 0x8214 @@ -399,15 +520,24 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE 0x8CD3 #define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LAYER 0x8CD4 #define GL_FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL 0x8CD2 +#define GL_FRAMEBUFFER_BARRIER_BIT 0x00000400 #define GL_FRAMEBUFFER_BINDING 0x8CA6 +#define GL_FRAMEBUFFER_BLEND 0x828B #define GL_FRAMEBUFFER_COMPLETE 0x8CD5 #define GL_FRAMEBUFFER_DEFAULT 0x8218 +#define GL_FRAMEBUFFER_DEFAULT_FIXED_SAMPLE_LOCATIONS 0x9314 +#define GL_FRAMEBUFFER_DEFAULT_HEIGHT 0x9311 +#define GL_FRAMEBUFFER_DEFAULT_LAYERS 0x9312 +#define GL_FRAMEBUFFER_DEFAULT_SAMPLES 0x9313 +#define GL_FRAMEBUFFER_DEFAULT_WIDTH 0x9310 #define GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT 0x8CD6 #define GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER 0x8CDB #define GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS 0x8DA8 #define GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT 0x8CD7 #define GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE 0x8D56 #define GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER 0x8CDC +#define GL_FRAMEBUFFER_RENDERABLE 0x8289 +#define GL_FRAMEBUFFER_RENDERABLE_LAYERED 0x828A #define GL_FRAMEBUFFER_SRGB 0x8DB9 #define GL_FRAMEBUFFER_UNDEFINED 0x8219 #define GL_FRAMEBUFFER_UNSUPPORTED 0x8CDD @@ -416,24 +546,97 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_FRONT_FACE 0x0B46 #define GL_FRONT_LEFT 0x0400 #define GL_FRONT_RIGHT 0x0401 +#define GL_FULL_SUPPORT 0x82B7 #define GL_FUNC_ADD 0x8006 #define GL_FUNC_REVERSE_SUBTRACT 0x800B #define GL_FUNC_SUBTRACT 0x800A #define GL_GEOMETRY_INPUT_TYPE 0x8917 #define GL_GEOMETRY_OUTPUT_TYPE 0x8918 #define GL_GEOMETRY_SHADER 0x8DD9 +#define GL_GEOMETRY_SHADER_BIT 0x00000004 +#define GL_GEOMETRY_SHADER_INVOCATIONS 0x887F +#define GL_GEOMETRY_SUBROUTINE 0x92EB +#define GL_GEOMETRY_SUBROUTINE_UNIFORM 0x92F1 +#define GL_GEOMETRY_TEXTURE 0x829E #define GL_GEOMETRY_VERTICES_OUT 0x8916 #define GL_GEQUAL 0x0206 +#define GL_GET_TEXTURE_IMAGE_FORMAT 0x8291 +#define GL_GET_TEXTURE_IMAGE_TYPE 0x8292 #define GL_GREATER 0x0204 #define GL_GREEN 0x1904 #define GL_GREEN_INTEGER 0x8D95 #define GL_HALF_FLOAT 0x140B +#define GL_HIGH_FLOAT 0x8DF2 +#define GL_HIGH_INT 0x8DF5 +#define GL_IMAGE_1D 0x904C +#define GL_IMAGE_1D_ARRAY 0x9052 +#define GL_IMAGE_2D 0x904D +#define GL_IMAGE_2D_ARRAY 0x9053 +#define GL_IMAGE_2D_MULTISAMPLE 0x9055 +#define GL_IMAGE_2D_MULTISAMPLE_ARRAY 0x9056 +#define GL_IMAGE_2D_RECT 0x904F +#define GL_IMAGE_3D 0x904E +#define GL_IMAGE_BINDING_ACCESS 0x8F3E +#define GL_IMAGE_BINDING_FORMAT 0x906E +#define GL_IMAGE_BINDING_LAYER 0x8F3D +#define GL_IMAGE_BINDING_LAYERED 0x8F3C +#define GL_IMAGE_BINDING_LEVEL 0x8F3B +#define GL_IMAGE_BINDING_NAME 0x8F3A +#define GL_IMAGE_BUFFER 0x9051 +#define GL_IMAGE_CLASS_10_10_10_2 0x82C3 +#define GL_IMAGE_CLASS_11_11_10 0x82C2 +#define GL_IMAGE_CLASS_1_X_16 0x82BE +#define GL_IMAGE_CLASS_1_X_32 0x82BB +#define GL_IMAGE_CLASS_1_X_8 0x82C1 +#define GL_IMAGE_CLASS_2_X_16 0x82BD +#define GL_IMAGE_CLASS_2_X_32 0x82BA +#define GL_IMAGE_CLASS_2_X_8 0x82C0 +#define GL_IMAGE_CLASS_4_X_16 0x82BC +#define GL_IMAGE_CLASS_4_X_32 0x82B9 +#define GL_IMAGE_CLASS_4_X_8 0x82BF +#define GL_IMAGE_COMPATIBILITY_CLASS 0x82A8 +#define GL_IMAGE_CUBE 0x9050 +#define GL_IMAGE_CUBE_MAP_ARRAY 0x9054 +#define GL_IMAGE_FORMAT_COMPATIBILITY_BY_CLASS 0x90C9 +#define GL_IMAGE_FORMAT_COMPATIBILITY_BY_SIZE 0x90C8 +#define GL_IMAGE_FORMAT_COMPATIBILITY_TYPE 0x90C7 +#define GL_IMAGE_PIXEL_FORMAT 0x82A9 +#define GL_IMAGE_PIXEL_TYPE 0x82AA +#define GL_IMAGE_TEXEL_SIZE 0x82A7 +#define GL_IMPLEMENTATION_COLOR_READ_FORMAT 0x8B9B +#define GL_IMPLEMENTATION_COLOR_READ_TYPE 0x8B9A #define GL_INCR 0x1E02 #define GL_INCR_WRAP 0x8507 #define GL_INFO_LOG_LENGTH 0x8B84 #define GL_INT 0x1404 #define GL_INTERLEAVED_ATTRIBS 0x8C8C +#define GL_INTERNALFORMAT_ALPHA_SIZE 0x8274 +#define GL_INTERNALFORMAT_ALPHA_TYPE 0x827B +#define GL_INTERNALFORMAT_BLUE_SIZE 0x8273 +#define GL_INTERNALFORMAT_BLUE_TYPE 0x827A +#define GL_INTERNALFORMAT_DEPTH_SIZE 0x8275 +#define GL_INTERNALFORMAT_DEPTH_TYPE 0x827C +#define GL_INTERNALFORMAT_GREEN_SIZE 0x8272 +#define GL_INTERNALFORMAT_GREEN_TYPE 0x8279 +#define GL_INTERNALFORMAT_PREFERRED 0x8270 +#define GL_INTERNALFORMAT_RED_SIZE 0x8271 +#define GL_INTERNALFORMAT_RED_TYPE 0x8278 +#define GL_INTERNALFORMAT_SHARED_SIZE 0x8277 +#define GL_INTERNALFORMAT_STENCIL_SIZE 0x8276 +#define GL_INTERNALFORMAT_STENCIL_TYPE 0x827D +#define GL_INTERNALFORMAT_SUPPORTED 0x826F #define GL_INT_2_10_10_10_REV 0x8D9F +#define GL_INT_IMAGE_1D 0x9057 +#define GL_INT_IMAGE_1D_ARRAY 0x905D +#define GL_INT_IMAGE_2D 0x9058 +#define GL_INT_IMAGE_2D_ARRAY 0x905E +#define GL_INT_IMAGE_2D_MULTISAMPLE 0x9060 +#define GL_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x9061 +#define GL_INT_IMAGE_2D_RECT 0x905A +#define GL_INT_IMAGE_3D 0x9059 +#define GL_INT_IMAGE_BUFFER 0x905C +#define GL_INT_IMAGE_CUBE 0x905B +#define GL_INT_IMAGE_CUBE_MAP_ARRAY 0x905F #define GL_INT_SAMPLER_1D 0x8DC9 #define GL_INT_SAMPLER_1D_ARRAY 0x8DCE #define GL_INT_SAMPLER_2D 0x8DCA @@ -444,6 +647,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_INT_SAMPLER_3D 0x8DCB #define GL_INT_SAMPLER_BUFFER 0x8DD0 #define GL_INT_SAMPLER_CUBE 0x8DCC +#define GL_INT_SAMPLER_CUBE_MAP_ARRAY 0x900E #define GL_INT_VEC2 0x8B53 #define GL_INT_VEC3 0x8B54 #define GL_INT_VEC4 0x8B55 @@ -453,8 +657,12 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_INVALID_OPERATION 0x0502 #define GL_INVALID_VALUE 0x0501 #define GL_INVERT 0x150A +#define GL_ISOLINES 0x8E7A +#define GL_IS_PER_PATCH 0x92E7 +#define GL_IS_ROW_MAJOR 0x9300 #define GL_KEEP 0x1E00 #define GL_LAST_VERTEX_CONVENTION 0x8E4E +#define GL_LAYER_PROVOKING_VERTEX 0x825E #define GL_LEFT 0x0406 #define GL_LEQUAL 0x0203 #define GL_LESS 0x0201 @@ -473,71 +681,176 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_LINE_WIDTH_GRANULARITY 0x0B23 #define GL_LINE_WIDTH_RANGE 0x0B22 #define GL_LINK_STATUS 0x8B82 +#define GL_LOCATION 0x930E +#define GL_LOCATION_INDEX 0x930F #define GL_LOGIC_OP_MODE 0x0BF0 #define GL_LOWER_LEFT 0x8CA1 +#define GL_LOW_FLOAT 0x8DF0 +#define GL_LOW_INT 0x8DF3 #define GL_MAJOR_VERSION 0x821B +#define GL_MANUAL_GENERATE_MIPMAP 0x8294 #define GL_MAP_FLUSH_EXPLICIT_BIT 0x0010 #define GL_MAP_INVALIDATE_BUFFER_BIT 0x0008 #define GL_MAP_INVALIDATE_RANGE_BIT 0x0004 #define GL_MAP_READ_BIT 0x0001 #define GL_MAP_UNSYNCHRONIZED_BIT 0x0020 #define GL_MAP_WRITE_BIT 0x0002 +#define GL_MATRIX_STRIDE 0x92FF #define GL_MAX 0x8008 #define GL_MAX_3D_TEXTURE_SIZE 0x8073 #define GL_MAX_ARRAY_TEXTURE_LAYERS 0x88FF +#define GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS 0x92DC +#define GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE 0x92D8 #define GL_MAX_CLIP_DISTANCES 0x0D32 #define GL_MAX_COLOR_ATTACHMENTS 0x8CDF #define GL_MAX_COLOR_TEXTURE_SAMPLES 0x910E +#define GL_MAX_COMBINED_ATOMIC_COUNTERS 0x92D7 +#define GL_MAX_COMBINED_ATOMIC_COUNTER_BUFFERS 0x92D1 +#define GL_MAX_COMBINED_COMPUTE_UNIFORM_COMPONENTS 0x8266 +#define GL_MAX_COMBINED_DIMENSIONS 0x8282 #define GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS 0x8A33 #define GL_MAX_COMBINED_GEOMETRY_UNIFORM_COMPONENTS 0x8A32 +#define GL_MAX_COMBINED_IMAGE_UNIFORMS 0x90CF +#define GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS 0x8F39 +#define GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES 0x8F39 +#define GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS 0x90DC +#define GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E1E +#define GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E1F #define GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 0x8B4D #define GL_MAX_COMBINED_UNIFORM_BLOCKS 0x8A2E #define GL_MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS 0x8A31 +#define GL_MAX_COMPUTE_ATOMIC_COUNTERS 0x8265 +#define GL_MAX_COMPUTE_ATOMIC_COUNTER_BUFFERS 0x8264 +#define GL_MAX_COMPUTE_IMAGE_UNIFORMS 0x91BD +#define GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS 0x90DB +#define GL_MAX_COMPUTE_SHARED_MEMORY_SIZE 0x8262 +#define GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS 0x91BC +#define GL_MAX_COMPUTE_UNIFORM_BLOCKS 0x91BB +#define GL_MAX_COMPUTE_UNIFORM_COMPONENTS 0x8263 +#define GL_MAX_COMPUTE_WORK_GROUP_COUNT 0x91BE +#define GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 0x90EB +#define GL_MAX_COMPUTE_WORK_GROUP_SIZE 0x91BF #define GL_MAX_CUBE_MAP_TEXTURE_SIZE 0x851C +#define GL_MAX_DEBUG_GROUP_STACK_DEPTH 0x826C +#define GL_MAX_DEBUG_LOGGED_MESSAGES 0x9144 +#define GL_MAX_DEBUG_MESSAGE_LENGTH 0x9143 +#define GL_MAX_DEPTH 0x8280 #define GL_MAX_DEPTH_TEXTURE_SAMPLES 0x910F #define GL_MAX_DRAW_BUFFERS 0x8824 #define GL_MAX_DUAL_SOURCE_DRAW_BUFFERS 0x88FC #define GL_MAX_ELEMENTS_INDICES 0x80E9 #define GL_MAX_ELEMENTS_VERTICES 0x80E8 +#define GL_MAX_ELEMENT_INDEX 0x8D6B +#define GL_MAX_FRAGMENT_ATOMIC_COUNTERS 0x92D6 +#define GL_MAX_FRAGMENT_ATOMIC_COUNTER_BUFFERS 0x92D0 +#define GL_MAX_FRAGMENT_IMAGE_UNIFORMS 0x90CE #define GL_MAX_FRAGMENT_INPUT_COMPONENTS 0x9125 +#define GL_MAX_FRAGMENT_INTERPOLATION_OFFSET 0x8E5C +#define GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS 0x90DA #define GL_MAX_FRAGMENT_UNIFORM_BLOCKS 0x8A2D #define GL_MAX_FRAGMENT_UNIFORM_COMPONENTS 0x8B49 +#define GL_MAX_FRAGMENT_UNIFORM_VECTORS 0x8DFD +#define GL_MAX_FRAMEBUFFER_HEIGHT 0x9316 +#define GL_MAX_FRAMEBUFFER_LAYERS 0x9317 +#define GL_MAX_FRAMEBUFFER_SAMPLES 0x9318 +#define GL_MAX_FRAMEBUFFER_WIDTH 0x9315 +#define GL_MAX_GEOMETRY_ATOMIC_COUNTERS 0x92D5 +#define GL_MAX_GEOMETRY_ATOMIC_COUNTER_BUFFERS 0x92CF +#define GL_MAX_GEOMETRY_IMAGE_UNIFORMS 0x90CD #define GL_MAX_GEOMETRY_INPUT_COMPONENTS 0x9123 #define GL_MAX_GEOMETRY_OUTPUT_COMPONENTS 0x9124 #define GL_MAX_GEOMETRY_OUTPUT_VERTICES 0x8DE0 +#define GL_MAX_GEOMETRY_SHADER_INVOCATIONS 0x8E5A +#define GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS 0x90D7 #define GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS 0x8C29 #define GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS 0x8DE1 #define GL_MAX_GEOMETRY_UNIFORM_BLOCKS 0x8A2C #define GL_MAX_GEOMETRY_UNIFORM_COMPONENTS 0x8DDF +#define GL_MAX_HEIGHT 0x827F +#define GL_MAX_IMAGE_SAMPLES 0x906D +#define GL_MAX_IMAGE_UNITS 0x8F38 #define GL_MAX_INTEGER_SAMPLES 0x9110 +#define GL_MAX_LABEL_LENGTH 0x82E8 +#define GL_MAX_LAYERS 0x8281 +#define GL_MAX_NAME_LENGTH 0x92F6 +#define GL_MAX_NUM_ACTIVE_VARIABLES 0x92F7 +#define GL_MAX_NUM_COMPATIBLE_SUBROUTINES 0x92F8 +#define GL_MAX_PATCH_VERTICES 0x8E7D #define GL_MAX_PROGRAM_TEXEL_OFFSET 0x8905 +#define GL_MAX_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5F #define GL_MAX_RECTANGLE_TEXTURE_SIZE 0x84F8 #define GL_MAX_RENDERBUFFER_SIZE 0x84E8 #define GL_MAX_SAMPLES 0x8D57 #define GL_MAX_SAMPLE_MASK_WORDS 0x8E59 #define GL_MAX_SERVER_WAIT_TIMEOUT 0x9111 +#define GL_MAX_SHADER_STORAGE_BLOCK_SIZE 0x90DE +#define GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS 0x90DD +#define GL_MAX_SUBROUTINES 0x8DE7 +#define GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS 0x8DE8 +#define GL_MAX_TESS_CONTROL_ATOMIC_COUNTERS 0x92D3 +#define GL_MAX_TESS_CONTROL_ATOMIC_COUNTER_BUFFERS 0x92CD +#define GL_MAX_TESS_CONTROL_IMAGE_UNIFORMS 0x90CB +#define GL_MAX_TESS_CONTROL_INPUT_COMPONENTS 0x886C +#define GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS 0x8E83 +#define GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS 0x90D8 +#define GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS 0x8E81 +#define GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS 0x8E85 +#define GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS 0x8E89 +#define GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS 0x8E7F +#define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTERS 0x92D4 +#define GL_MAX_TESS_EVALUATION_ATOMIC_COUNTER_BUFFERS 0x92CE +#define GL_MAX_TESS_EVALUATION_IMAGE_UNIFORMS 0x90CC +#define GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS 0x886D +#define GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS 0x8E86 +#define GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS 0x90D9 +#define GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS 0x8E82 +#define GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS 0x8E8A +#define GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS 0x8E80 +#define GL_MAX_TESS_GEN_LEVEL 0x8E7E +#define GL_MAX_TESS_PATCH_COMPONENTS 0x8E84 #define GL_MAX_TEXTURE_BUFFER_SIZE 0x8C2B #define GL_MAX_TEXTURE_IMAGE_UNITS 0x8872 #define GL_MAX_TEXTURE_LOD_BIAS 0x84FD #define GL_MAX_TEXTURE_SIZE 0x0D33 +#define GL_MAX_TRANSFORM_FEEDBACK_BUFFERS 0x8E70 #define GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS 0x8C8A #define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS 0x8C8B #define GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS 0x8C80 #define GL_MAX_UNIFORM_BLOCK_SIZE 0x8A30 #define GL_MAX_UNIFORM_BUFFER_BINDINGS 0x8A2F +#define GL_MAX_UNIFORM_LOCATIONS 0x826E #define GL_MAX_VARYING_COMPONENTS 0x8B4B #define GL_MAX_VARYING_FLOATS 0x8B4B +#define GL_MAX_VARYING_VECTORS 0x8DFC +#define GL_MAX_VERTEX_ATOMIC_COUNTERS 0x92D2 +#define GL_MAX_VERTEX_ATOMIC_COUNTER_BUFFERS 0x92CC #define GL_MAX_VERTEX_ATTRIBS 0x8869 +#define GL_MAX_VERTEX_ATTRIB_BINDINGS 0x82DA +#define GL_MAX_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D9 +#define GL_MAX_VERTEX_IMAGE_UNIFORMS 0x90CA #define GL_MAX_VERTEX_OUTPUT_COMPONENTS 0x9122 +#define GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS 0x90D6 +#define GL_MAX_VERTEX_STREAMS 0x8E71 #define GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS 0x8B4C #define GL_MAX_VERTEX_UNIFORM_BLOCKS 0x8A2B #define GL_MAX_VERTEX_UNIFORM_COMPONENTS 0x8B4A +#define GL_MAX_VERTEX_UNIFORM_VECTORS 0x8DFB +#define GL_MAX_VIEWPORTS 0x825B #define GL_MAX_VIEWPORT_DIMS 0x0D3A +#define GL_MAX_WIDTH 0x827E +#define GL_MEDIUM_FLOAT 0x8DF1 +#define GL_MEDIUM_INT 0x8DF4 #define GL_MIN 0x8007 #define GL_MINOR_VERSION 0x821C +#define GL_MIN_FRAGMENT_INTERPOLATION_OFFSET 0x8E5B +#define GL_MIN_MAP_BUFFER_ALIGNMENT 0x90BC #define GL_MIN_PROGRAM_TEXEL_OFFSET 0x8904 +#define GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET 0x8E5E +#define GL_MIN_SAMPLE_SHADING_VALUE 0x8C37 +#define GL_MIPMAP 0x8293 #define GL_MIRRORED_REPEAT 0x8370 #define GL_MULTISAMPLE 0x809D +#define GL_NAME_LENGTH 0x92F9 #define GL_NAND 0x150E #define GL_NEAREST 0x2600 #define GL_NEAREST_MIPMAP_LINEAR 0x2702 @@ -549,9 +862,16 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_NOR 0x1508 #define GL_NOTEQUAL 0x0205 #define GL_NO_ERROR 0 +#define GL_NUM_ACTIVE_VARIABLES 0x9304 +#define GL_NUM_COMPATIBLE_SUBROUTINES 0x8E4A #define GL_NUM_COMPRESSED_TEXTURE_FORMATS 0x86A2 #define GL_NUM_EXTENSIONS 0x821D +#define GL_NUM_PROGRAM_BINARY_FORMATS 0x87FE +#define GL_NUM_SAMPLE_COUNTS 0x9380 +#define GL_NUM_SHADER_BINARY_FORMATS 0x8DF9 +#define GL_NUM_SHADING_LANGUAGE_VERSIONS 0x82E9 #define GL_OBJECT_TYPE 0x9112 +#define GL_OFFSET 0x92FC #define GL_ONE 1 #define GL_ONE_MINUS_CONSTANT_ALPHA 0x8004 #define GL_ONE_MINUS_CONSTANT_COLOR 0x8002 @@ -566,6 +886,10 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_OR_REVERSE 0x150B #define GL_OUT_OF_MEMORY 0x0505 #define GL_PACK_ALIGNMENT 0x0D05 +#define GL_PACK_COMPRESSED_BLOCK_DEPTH 0x912D +#define GL_PACK_COMPRESSED_BLOCK_HEIGHT 0x912C +#define GL_PACK_COMPRESSED_BLOCK_SIZE 0x912E +#define GL_PACK_COMPRESSED_BLOCK_WIDTH 0x912B #define GL_PACK_IMAGE_HEIGHT 0x806C #define GL_PACK_LSB_FIRST 0x0D01 #define GL_PACK_ROW_LENGTH 0x0D02 @@ -573,6 +897,11 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_PACK_SKIP_PIXELS 0x0D04 #define GL_PACK_SKIP_ROWS 0x0D03 #define GL_PACK_SWAP_BYTES 0x0D00 +#define GL_PATCHES 0x000E +#define GL_PATCH_DEFAULT_INNER_LEVEL 0x8E73 +#define GL_PATCH_DEFAULT_OUTER_LEVEL 0x8E74 +#define GL_PATCH_VERTICES 0x8E72 +#define GL_PIXEL_BUFFER_BARRIER_BIT 0x00000080 #define GL_PIXEL_PACK_BUFFER 0x88EB #define GL_PIXEL_PACK_BUFFER_BINDING 0x88ED #define GL_PIXEL_UNPACK_BUFFER 0x88EC @@ -594,8 +923,18 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_POLYGON_SMOOTH_HINT 0x0C53 #define GL_PRIMITIVES_GENERATED 0x8C87 #define GL_PRIMITIVE_RESTART 0x8F9D +#define GL_PRIMITIVE_RESTART_FIXED_INDEX 0x8D69 #define GL_PRIMITIVE_RESTART_INDEX 0x8F9E +#define GL_PROGRAM 0x82E2 +#define GL_PROGRAM_BINARY_FORMATS 0x87FF +#define GL_PROGRAM_BINARY_LENGTH 0x8741 +#define GL_PROGRAM_BINARY_RETRIEVABLE_HINT 0x8257 +#define GL_PROGRAM_INPUT 0x92E3 +#define GL_PROGRAM_OUTPUT 0x92E4 +#define GL_PROGRAM_PIPELINE 0x82E4 +#define GL_PROGRAM_PIPELINE_BINDING 0x825A #define GL_PROGRAM_POINT_SIZE 0x8642 +#define GL_PROGRAM_SEPARABLE 0x8258 #define GL_PROVOKING_VERTEX 0x8E4F #define GL_PROXY_TEXTURE_1D 0x8063 #define GL_PROXY_TEXTURE_1D_ARRAY 0x8C19 @@ -605,8 +944,11 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY 0x9103 #define GL_PROXY_TEXTURE_3D 0x8070 #define GL_PROXY_TEXTURE_CUBE_MAP 0x851B +#define GL_PROXY_TEXTURE_CUBE_MAP_ARRAY 0x900B #define GL_PROXY_TEXTURE_RECTANGLE 0x84F7 +#define GL_QUADS 0x0007 #define GL_QUADS_FOLLOW_PROVOKING_VERTEX_CONVENTION 0x8E4C +#define GL_QUERY 0x82E3 #define GL_QUERY_BY_REGION_NO_WAIT 0x8E16 #define GL_QUERY_BY_REGION_WAIT 0x8E15 #define GL_QUERY_COUNTER_BITS 0x8864 @@ -633,9 +975,18 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_READ_FRAMEBUFFER 0x8CA8 #define GL_READ_FRAMEBUFFER_BINDING 0x8CAA #define GL_READ_ONLY 0x88B8 +#define GL_READ_PIXELS 0x828C +#define GL_READ_PIXELS_FORMAT 0x828D +#define GL_READ_PIXELS_TYPE 0x828E #define GL_READ_WRITE 0x88BA #define GL_RED 0x1903 #define GL_RED_INTEGER 0x8D94 +#define GL_REFERENCED_BY_COMPUTE_SHADER 0x930B +#define GL_REFERENCED_BY_FRAGMENT_SHADER 0x930A +#define GL_REFERENCED_BY_GEOMETRY_SHADER 0x9309 +#define GL_REFERENCED_BY_TESS_CONTROL_SHADER 0x9307 +#define GL_REFERENCED_BY_TESS_EVALUATION_SHADER 0x9308 +#define GL_REFERENCED_BY_VERTEX_SHADER 0x9306 #define GL_RENDERBUFFER 0x8D41 #define GL_RENDERBUFFER_ALPHA_SIZE 0x8D53 #define GL_RENDERBUFFER_BINDING 0x8CA7 @@ -679,6 +1030,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_RGB32UI 0x8D71 #define GL_RGB4 0x804F #define GL_RGB5 0x8050 +#define GL_RGB565 0x8D62 #define GL_RGB5_A1 0x8057 #define GL_RGB8 0x8051 #define GL_RGB8I 0x8D8F @@ -705,6 +1057,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_RGB_INTEGER 0x8D98 #define GL_RG_INTEGER 0x8228 #define GL_RIGHT 0x0407 +#define GL_SAMPLER 0x82E6 #define GL_SAMPLER_1D 0x8B5D #define GL_SAMPLER_1D_ARRAY 0x8DC0 #define GL_SAMPLER_1D_ARRAY_SHADOW 0x8DC3 @@ -721,6 +1074,8 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_SAMPLER_BINDING 0x8919 #define GL_SAMPLER_BUFFER 0x8DC2 #define GL_SAMPLER_CUBE 0x8B60 +#define GL_SAMPLER_CUBE_MAP_ARRAY 0x900C +#define GL_SAMPLER_CUBE_MAP_ARRAY_SHADOW 0x900D #define GL_SAMPLER_CUBE_SHADOW 0x8DC5 #define GL_SAMPLES 0x80A9 #define GL_SAMPLES_PASSED 0x8914 @@ -733,16 +1088,35 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_SAMPLE_MASK 0x8E51 #define GL_SAMPLE_MASK_VALUE 0x8E52 #define GL_SAMPLE_POSITION 0x8E50 +#define GL_SAMPLE_SHADING 0x8C36 #define GL_SCISSOR_BOX 0x0C10 #define GL_SCISSOR_TEST 0x0C11 #define GL_SEPARATE_ATTRIBS 0x8C8D #define GL_SET 0x150F +#define GL_SHADER 0x82E1 +#define GL_SHADER_BINARY_FORMATS 0x8DF8 +#define GL_SHADER_COMPILER 0x8DFA +#define GL_SHADER_IMAGE_ACCESS_BARRIER_BIT 0x00000020 +#define GL_SHADER_IMAGE_ATOMIC 0x82A6 +#define GL_SHADER_IMAGE_LOAD 0x82A4 +#define GL_SHADER_IMAGE_STORE 0x82A5 #define GL_SHADER_SOURCE_LENGTH 0x8B88 +#define GL_SHADER_STORAGE_BARRIER_BIT 0x00002000 +#define GL_SHADER_STORAGE_BLOCK 0x92E6 +#define GL_SHADER_STORAGE_BUFFER 0x90D2 +#define GL_SHADER_STORAGE_BUFFER_BINDING 0x90D3 +#define GL_SHADER_STORAGE_BUFFER_OFFSET_ALIGNMENT 0x90DF +#define GL_SHADER_STORAGE_BUFFER_SIZE 0x90D5 +#define GL_SHADER_STORAGE_BUFFER_START 0x90D4 #define GL_SHADER_TYPE 0x8B4F #define GL_SHADING_LANGUAGE_VERSION 0x8B8C #define GL_SHORT 0x1402 #define GL_SIGNALED 0x9119 #define GL_SIGNED_NORMALIZED 0x8F9C +#define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_TEST 0x82AC +#define GL_SIMULTANEOUS_TEXTURE_AND_DEPTH_WRITE 0x82AE +#define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_TEST 0x82AD +#define GL_SIMULTANEOUS_TEXTURE_AND_STENCIL_WRITE 0x82AF #define GL_SMOOTH_LINE_WIDTH_GRANULARITY 0x0B23 #define GL_SMOOTH_LINE_WIDTH_RANGE 0x0B22 #define GL_SMOOTH_POINT_SIZE_GRANULARITY 0x0B13 @@ -756,6 +1130,10 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_SRGB8 0x8C41 #define GL_SRGB8_ALPHA8 0x8C43 #define GL_SRGB_ALPHA 0x8C42 +#define GL_SRGB_READ 0x8297 +#define GL_SRGB_WRITE 0x8298 +#define GL_STACK_OVERFLOW 0x0503 +#define GL_STACK_UNDERFLOW 0x0504 #define GL_STATIC_COPY 0x88E6 #define GL_STATIC_DRAW 0x88E4 #define GL_STATIC_READ 0x88E5 @@ -770,6 +1148,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_STENCIL_BACK_WRITEMASK 0x8CA5 #define GL_STENCIL_BUFFER_BIT 0x00000400 #define GL_STENCIL_CLEAR_VALUE 0x0B91 +#define GL_STENCIL_COMPONENTS 0x8285 #define GL_STENCIL_FAIL 0x0B94 #define GL_STENCIL_FUNC 0x0B92 #define GL_STENCIL_INDEX 0x1901 @@ -780,6 +1159,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_STENCIL_PASS_DEPTH_FAIL 0x0B95 #define GL_STENCIL_PASS_DEPTH_PASS 0x0B96 #define GL_STENCIL_REF 0x0B97 +#define GL_STENCIL_RENDERABLE 0x8288 #define GL_STENCIL_TEST 0x0B90 #define GL_STENCIL_VALUE_MASK 0x0B93 #define GL_STENCIL_WRITEMASK 0x0B98 @@ -794,6 +1174,21 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_SYNC_FLUSH_COMMANDS_BIT 0x00000001 #define GL_SYNC_GPU_COMMANDS_COMPLETE 0x9117 #define GL_SYNC_STATUS 0x9114 +#define GL_TESS_CONTROL_OUTPUT_VERTICES 0x8E75 +#define GL_TESS_CONTROL_SHADER 0x8E88 +#define GL_TESS_CONTROL_SHADER_BIT 0x00000008 +#define GL_TESS_CONTROL_SUBROUTINE 0x92E9 +#define GL_TESS_CONTROL_SUBROUTINE_UNIFORM 0x92EF +#define GL_TESS_CONTROL_TEXTURE 0x829C +#define GL_TESS_EVALUATION_SHADER 0x8E87 +#define GL_TESS_EVALUATION_SHADER_BIT 0x00000010 +#define GL_TESS_EVALUATION_SUBROUTINE 0x92EA +#define GL_TESS_EVALUATION_SUBROUTINE_UNIFORM 0x92F0 +#define GL_TESS_EVALUATION_TEXTURE 0x829D +#define GL_TESS_GEN_MODE 0x8E76 +#define GL_TESS_GEN_POINT_MODE 0x8E79 +#define GL_TESS_GEN_SPACING 0x8E77 +#define GL_TESS_GEN_VERTEX_ORDER 0x8E78 #define GL_TEXTURE 0x1702 #define GL_TEXTURE0 0x84C0 #define GL_TEXTURE1 0x84C1 @@ -846,18 +1241,26 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_TEXTURE_BINDING_3D 0x806A #define GL_TEXTURE_BINDING_BUFFER 0x8C2C #define GL_TEXTURE_BINDING_CUBE_MAP 0x8514 +#define GL_TEXTURE_BINDING_CUBE_MAP_ARRAY 0x900A #define GL_TEXTURE_BINDING_RECTANGLE 0x84F6 #define GL_TEXTURE_BLUE_SIZE 0x805E #define GL_TEXTURE_BLUE_TYPE 0x8C12 #define GL_TEXTURE_BORDER_COLOR 0x1004 #define GL_TEXTURE_BUFFER 0x8C2A #define GL_TEXTURE_BUFFER_DATA_STORE_BINDING 0x8C2D +#define GL_TEXTURE_BUFFER_OFFSET 0x919D +#define GL_TEXTURE_BUFFER_OFFSET_ALIGNMENT 0x919F +#define GL_TEXTURE_BUFFER_SIZE 0x919E #define GL_TEXTURE_COMPARE_FUNC 0x884D #define GL_TEXTURE_COMPARE_MODE 0x884C #define GL_TEXTURE_COMPRESSED 0x86A1 +#define GL_TEXTURE_COMPRESSED_BLOCK_HEIGHT 0x82B2 +#define GL_TEXTURE_COMPRESSED_BLOCK_SIZE 0x82B3 +#define GL_TEXTURE_COMPRESSED_BLOCK_WIDTH 0x82B1 #define GL_TEXTURE_COMPRESSED_IMAGE_SIZE 0x86A0 #define GL_TEXTURE_COMPRESSION_HINT 0x84EF #define GL_TEXTURE_CUBE_MAP 0x8513 +#define GL_TEXTURE_CUBE_MAP_ARRAY 0x9009 #define GL_TEXTURE_CUBE_MAP_NEGATIVE_X 0x8516 #define GL_TEXTURE_CUBE_MAP_NEGATIVE_Y 0x8518 #define GL_TEXTURE_CUBE_MAP_NEGATIVE_Z 0x851A @@ -868,10 +1271,17 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_TEXTURE_DEPTH 0x8071 #define GL_TEXTURE_DEPTH_SIZE 0x884A #define GL_TEXTURE_DEPTH_TYPE 0x8C16 +#define GL_TEXTURE_FETCH_BARRIER_BIT 0x00000008 #define GL_TEXTURE_FIXED_SAMPLE_LOCATIONS 0x9107 +#define GL_TEXTURE_GATHER 0x82A2 +#define GL_TEXTURE_GATHER_SHADOW 0x82A3 #define GL_TEXTURE_GREEN_SIZE 0x805D #define GL_TEXTURE_GREEN_TYPE 0x8C11 #define GL_TEXTURE_HEIGHT 0x1001 +#define GL_TEXTURE_IMAGE_FORMAT 0x828F +#define GL_TEXTURE_IMAGE_TYPE 0x8290 +#define GL_TEXTURE_IMMUTABLE_FORMAT 0x912F +#define GL_TEXTURE_IMMUTABLE_LEVELS 0x82DF #define GL_TEXTURE_INTERNAL_FORMAT 0x1003 #define GL_TEXTURE_LOD_BIAS 0x8501 #define GL_TEXTURE_MAG_FILTER 0x2800 @@ -883,6 +1293,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_TEXTURE_RED_SIZE 0x805C #define GL_TEXTURE_RED_TYPE 0x8C10 #define GL_TEXTURE_SAMPLES 0x9106 +#define GL_TEXTURE_SHADOW 0x82A1 #define GL_TEXTURE_SHARED_SIZE 0x8C3F #define GL_TEXTURE_STENCIL_SIZE 0x88F1 #define GL_TEXTURE_SWIZZLE_A 0x8E45 @@ -890,6 +1301,12 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_TEXTURE_SWIZZLE_G 0x8E43 #define GL_TEXTURE_SWIZZLE_R 0x8E42 #define GL_TEXTURE_SWIZZLE_RGBA 0x8E46 +#define GL_TEXTURE_UPDATE_BARRIER_BIT 0x00000100 +#define GL_TEXTURE_VIEW 0x82B5 +#define GL_TEXTURE_VIEW_MIN_LAYER 0x82DD +#define GL_TEXTURE_VIEW_MIN_LEVEL 0x82DB +#define GL_TEXTURE_VIEW_NUM_LAYERS 0x82DE +#define GL_TEXTURE_VIEW_NUM_LEVELS 0x82DC #define GL_TEXTURE_WIDTH 0x1000 #define GL_TEXTURE_WRAP_R 0x8072 #define GL_TEXTURE_WRAP_S 0x2802 @@ -898,12 +1315,22 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_TIMEOUT_IGNORED 0xFFFFFFFFFFFFFFFF #define GL_TIMESTAMP 0x8E28 #define GL_TIME_ELAPSED 0x88BF +#define GL_TOP_LEVEL_ARRAY_SIZE 0x930C +#define GL_TOP_LEVEL_ARRAY_STRIDE 0x930D +#define GL_TRANSFORM_FEEDBACK 0x8E22 +#define GL_TRANSFORM_FEEDBACK_ACTIVE 0x8E24 +#define GL_TRANSFORM_FEEDBACK_BARRIER_BIT 0x00000800 +#define GL_TRANSFORM_FEEDBACK_BINDING 0x8E25 #define GL_TRANSFORM_FEEDBACK_BUFFER 0x8C8E +#define GL_TRANSFORM_FEEDBACK_BUFFER_ACTIVE 0x8E24 #define GL_TRANSFORM_FEEDBACK_BUFFER_BINDING 0x8C8F #define GL_TRANSFORM_FEEDBACK_BUFFER_MODE 0x8C7F +#define GL_TRANSFORM_FEEDBACK_BUFFER_PAUSED 0x8E23 #define GL_TRANSFORM_FEEDBACK_BUFFER_SIZE 0x8C85 #define GL_TRANSFORM_FEEDBACK_BUFFER_START 0x8C84 +#define GL_TRANSFORM_FEEDBACK_PAUSED 0x8E23 #define GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN 0x8C88 +#define GL_TRANSFORM_FEEDBACK_VARYING 0x92F4 #define GL_TRANSFORM_FEEDBACK_VARYINGS 0x8C83 #define GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH 0x8C76 #define GL_TRIANGLES 0x0004 @@ -912,15 +1339,24 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_TRIANGLE_STRIP 0x0005 #define GL_TRIANGLE_STRIP_ADJACENCY 0x000D #define GL_TRUE 1 +#define GL_TYPE 0x92FA +#define GL_UNDEFINED_VERTEX 0x8260 +#define GL_UNIFORM 0x92E1 #define GL_UNIFORM_ARRAY_STRIDE 0x8A3C +#define GL_UNIFORM_ATOMIC_COUNTER_BUFFER_INDEX 0x92DA +#define GL_UNIFORM_BARRIER_BIT 0x00000004 +#define GL_UNIFORM_BLOCK 0x92E2 #define GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS 0x8A42 #define GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES 0x8A43 #define GL_UNIFORM_BLOCK_BINDING 0x8A3F #define GL_UNIFORM_BLOCK_DATA_SIZE 0x8A40 #define GL_UNIFORM_BLOCK_INDEX 0x8A3A #define GL_UNIFORM_BLOCK_NAME_LENGTH 0x8A41 +#define GL_UNIFORM_BLOCK_REFERENCED_BY_COMPUTE_SHADER 0x90EC #define GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER 0x8A46 #define GL_UNIFORM_BLOCK_REFERENCED_BY_GEOMETRY_SHADER 0x8A45 +#define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_CONTROL_SHADER 0x84F0 +#define GL_UNIFORM_BLOCK_REFERENCED_BY_TESS_EVALUATION_SHADER 0x84F1 #define GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER 0x8A44 #define GL_UNIFORM_BUFFER 0x8A11 #define GL_UNIFORM_BUFFER_BINDING 0x8A28 @@ -934,6 +1370,10 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_UNIFORM_SIZE 0x8A38 #define GL_UNIFORM_TYPE 0x8A37 #define GL_UNPACK_ALIGNMENT 0x0CF5 +#define GL_UNPACK_COMPRESSED_BLOCK_DEPTH 0x9129 +#define GL_UNPACK_COMPRESSED_BLOCK_HEIGHT 0x9128 +#define GL_UNPACK_COMPRESSED_BLOCK_SIZE 0x912A +#define GL_UNPACK_COMPRESSED_BLOCK_WIDTH 0x9127 #define GL_UNPACK_IMAGE_HEIGHT 0x806E #define GL_UNPACK_LSB_FIRST 0x0CF1 #define GL_UNPACK_ROW_LENGTH 0x0CF2 @@ -953,6 +1393,18 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_UNSIGNED_INT_5_9_9_9_REV 0x8C3E #define GL_UNSIGNED_INT_8_8_8_8 0x8035 #define GL_UNSIGNED_INT_8_8_8_8_REV 0x8367 +#define GL_UNSIGNED_INT_ATOMIC_COUNTER 0x92DB +#define GL_UNSIGNED_INT_IMAGE_1D 0x9062 +#define GL_UNSIGNED_INT_IMAGE_1D_ARRAY 0x9068 +#define GL_UNSIGNED_INT_IMAGE_2D 0x9063 +#define GL_UNSIGNED_INT_IMAGE_2D_ARRAY 0x9069 +#define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE 0x906B +#define GL_UNSIGNED_INT_IMAGE_2D_MULTISAMPLE_ARRAY 0x906C +#define GL_UNSIGNED_INT_IMAGE_2D_RECT 0x9065 +#define GL_UNSIGNED_INT_IMAGE_3D 0x9064 +#define GL_UNSIGNED_INT_IMAGE_BUFFER 0x9067 +#define GL_UNSIGNED_INT_IMAGE_CUBE 0x9066 +#define GL_UNSIGNED_INT_IMAGE_CUBE_MAP_ARRAY 0x906A #define GL_UNSIGNED_INT_SAMPLER_1D 0x8DD1 #define GL_UNSIGNED_INT_SAMPLER_1D_ARRAY 0x8DD6 #define GL_UNSIGNED_INT_SAMPLER_2D 0x8DD2 @@ -963,6 +1415,7 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_UNSIGNED_INT_SAMPLER_3D 0x8DD3 #define GL_UNSIGNED_INT_SAMPLER_BUFFER 0x8DD8 #define GL_UNSIGNED_INT_SAMPLER_CUBE 0x8DD4 +#define GL_UNSIGNED_INT_SAMPLER_CUBE_MAP_ARRAY 0x900F #define GL_UNSIGNED_INT_VEC2 0x8DC6 #define GL_UNSIGNED_INT_VEC3 0x8DC7 #define GL_UNSIGNED_INT_VEC4 0x8DC8 @@ -978,19 +1431,52 @@ typedef void (*GLADpostcallback)(void *ret, const char *name, GLADapiproc apipro #define GL_VALIDATE_STATUS 0x8B83 #define GL_VENDOR 0x1F00 #define GL_VERSION 0x1F02 +#define GL_VERTEX_ARRAY 0x8074 #define GL_VERTEX_ARRAY_BINDING 0x85B5 +#define GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT 0x00000001 #define GL_VERTEX_ATTRIB_ARRAY_BUFFER_BINDING 0x889F #define GL_VERTEX_ATTRIB_ARRAY_DIVISOR 0x88FE #define GL_VERTEX_ATTRIB_ARRAY_ENABLED 0x8622 #define GL_VERTEX_ATTRIB_ARRAY_INTEGER 0x88FD +#define GL_VERTEX_ATTRIB_ARRAY_LONG 0x874E #define GL_VERTEX_ATTRIB_ARRAY_NORMALIZED 0x886A #define GL_VERTEX_ATTRIB_ARRAY_POINTER 0x8645 #define GL_VERTEX_ATTRIB_ARRAY_SIZE 0x8623 #define GL_VERTEX_ATTRIB_ARRAY_STRIDE 0x8624 #define GL_VERTEX_ATTRIB_ARRAY_TYPE 0x8625 +#define GL_VERTEX_ATTRIB_BINDING 0x82D4 +#define GL_VERTEX_ATTRIB_RELATIVE_OFFSET 0x82D5 +#define GL_VERTEX_BINDING_BUFFER 0x8F4F +#define GL_VERTEX_BINDING_DIVISOR 0x82D6 +#define GL_VERTEX_BINDING_OFFSET 0x82D7 +#define GL_VERTEX_BINDING_STRIDE 0x82D8 #define GL_VERTEX_PROGRAM_POINT_SIZE 0x8642 #define GL_VERTEX_SHADER 0x8B31 +#define GL_VERTEX_SHADER_BIT 0x00000001 +#define GL_VERTEX_SUBROUTINE 0x92E8 +#define GL_VERTEX_SUBROUTINE_UNIFORM 0x92EE +#define GL_VERTEX_TEXTURE 0x829B #define GL_VIEWPORT 0x0BA2 +#define GL_VIEWPORT_BOUNDS_RANGE 0x825D +#define GL_VIEWPORT_INDEX_PROVOKING_VERTEX 0x825F +#define GL_VIEWPORT_SUBPIXEL_BITS 0x825C +#define GL_VIEW_CLASS_128_BITS 0x82C4 +#define GL_VIEW_CLASS_16_BITS 0x82CA +#define GL_VIEW_CLASS_24_BITS 0x82C9 +#define GL_VIEW_CLASS_32_BITS 0x82C8 +#define GL_VIEW_CLASS_48_BITS 0x82C7 +#define GL_VIEW_CLASS_64_BITS 0x82C6 +#define GL_VIEW_CLASS_8_BITS 0x82CB +#define GL_VIEW_CLASS_96_BITS 0x82C5 +#define GL_VIEW_CLASS_BPTC_FLOAT 0x82D3 +#define GL_VIEW_CLASS_BPTC_UNORM 0x82D2 +#define GL_VIEW_CLASS_RGTC1_RED 0x82D0 +#define GL_VIEW_CLASS_RGTC2_RG 0x82D1 +#define GL_VIEW_CLASS_S3TC_DXT1_RGB 0x82CC +#define GL_VIEW_CLASS_S3TC_DXT1_RGBA 0x82CD +#define GL_VIEW_CLASS_S3TC_DXT3_RGBA 0x82CE +#define GL_VIEW_CLASS_S3TC_DXT5_RGBA 0x82CF +#define GL_VIEW_COMPATIBILITY_CLASS 0x82B6 #define GL_WAIT_FAILED 0x911D #define GL_WRITE_ONLY 0x88B9 #define GL_XOR 0x1506 @@ -1074,12 +1560,18 @@ typedef void (GLAD_API_PTR *GLVULKANPROCNV)(void); #define GL_VERSION_3_1 1 #define GL_VERSION_3_2 1 #define GL_VERSION_3_3 1 +#define GL_VERSION_4_0 1 +#define GL_VERSION_4_1 1 +#define GL_VERSION_4_2 1 +#define GL_VERSION_4_3 1 +typedef void (GLAD_API_PTR *PFNGLACTIVESHADERPROGRAMPROC)(GLuint pipeline, GLuint program); typedef void (GLAD_API_PTR *PFNGLACTIVETEXTUREPROC)(GLenum texture); typedef void (GLAD_API_PTR *PFNGLATTACHSHADERPROC)(GLuint program, GLuint shader); typedef void (GLAD_API_PTR *PFNGLBEGINCONDITIONALRENDERPROC)(GLuint id, GLenum mode); typedef void (GLAD_API_PTR *PFNGLBEGINQUERYPROC)(GLenum target, GLuint id); +typedef void (GLAD_API_PTR *PFNGLBEGINQUERYINDEXEDPROC)(GLenum target, GLuint index, GLuint id); typedef void (GLAD_API_PTR *PFNGLBEGINTRANSFORMFEEDBACKPROC)(GLenum primitiveMode); typedef void (GLAD_API_PTR *PFNGLBINDATTRIBLOCATIONPROC)(GLuint program, GLuint index, const GLchar * name); typedef void (GLAD_API_PTR *PFNGLBINDBUFFERPROC)(GLenum target, GLuint buffer); @@ -1088,27 +1580,38 @@ typedef void (GLAD_API_PTR *PFNGLBINDBUFFERRANGEPROC)(GLenum target, GLuint inde typedef void (GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONPROC)(GLuint program, GLuint color, const GLchar * name); typedef void (GLAD_API_PTR *PFNGLBINDFRAGDATALOCATIONINDEXEDPROC)(GLuint program, GLuint colorNumber, GLuint index, const GLchar * name); typedef void (GLAD_API_PTR *PFNGLBINDFRAMEBUFFERPROC)(GLenum target, GLuint framebuffer); +typedef void (GLAD_API_PTR *PFNGLBINDIMAGETEXTUREPROC)(GLuint unit, GLuint texture, GLint level, GLboolean layered, GLint layer, GLenum access, GLenum format); +typedef void (GLAD_API_PTR *PFNGLBINDPROGRAMPIPELINEPROC)(GLuint pipeline); typedef void (GLAD_API_PTR *PFNGLBINDRENDERBUFFERPROC)(GLenum target, GLuint renderbuffer); typedef void (GLAD_API_PTR *PFNGLBINDSAMPLERPROC)(GLuint unit, GLuint sampler); typedef void (GLAD_API_PTR *PFNGLBINDTEXTUREPROC)(GLenum target, GLuint texture); +typedef void (GLAD_API_PTR *PFNGLBINDTRANSFORMFEEDBACKPROC)(GLenum target, GLuint id); typedef void (GLAD_API_PTR *PFNGLBINDVERTEXARRAYPROC)(GLuint array); +typedef void (GLAD_API_PTR *PFNGLBINDVERTEXBUFFERPROC)(GLuint bindingindex, GLuint buffer, GLintptr offset, GLsizei stride); typedef void (GLAD_API_PTR *PFNGLBLENDCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONPROC)(GLenum mode); typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEPROC)(GLenum modeRGB, GLenum modeAlpha); +typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONSEPARATEIPROC)(GLuint buf, GLenum modeRGB, GLenum modeAlpha); +typedef void (GLAD_API_PTR *PFNGLBLENDEQUATIONIPROC)(GLuint buf, GLenum mode); typedef void (GLAD_API_PTR *PFNGLBLENDFUNCPROC)(GLenum sfactor, GLenum dfactor); typedef void (GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEPROC)(GLenum sfactorRGB, GLenum dfactorRGB, GLenum sfactorAlpha, GLenum dfactorAlpha); +typedef void (GLAD_API_PTR *PFNGLBLENDFUNCSEPARATEIPROC)(GLuint buf, GLenum srcRGB, GLenum dstRGB, GLenum srcAlpha, GLenum dstAlpha); +typedef void (GLAD_API_PTR *PFNGLBLENDFUNCIPROC)(GLuint buf, GLenum src, GLenum dst); typedef void (GLAD_API_PTR *PFNGLBLITFRAMEBUFFERPROC)(GLint srcX0, GLint srcY0, GLint srcX1, GLint srcY1, GLint dstX0, GLint dstY0, GLint dstX1, GLint dstY1, GLbitfield mask, GLenum filter); typedef void (GLAD_API_PTR *PFNGLBUFFERDATAPROC)(GLenum target, GLsizeiptr size, const void * data, GLenum usage); typedef void (GLAD_API_PTR *PFNGLBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, const void * data); typedef GLenum (GLAD_API_PTR *PFNGLCHECKFRAMEBUFFERSTATUSPROC)(GLenum target); typedef void (GLAD_API_PTR *PFNGLCLAMPCOLORPROC)(GLenum target, GLenum clamp); typedef void (GLAD_API_PTR *PFNGLCLEARPROC)(GLbitfield mask); +typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERDATAPROC)(GLenum target, GLenum internalformat, GLenum format, GLenum type, const void * data); +typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERSUBDATAPROC)(GLenum target, GLenum internalformat, GLintptr offset, GLsizeiptr size, GLenum format, GLenum type, const void * data); typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERFIPROC)(GLenum buffer, GLint drawbuffer, GLfloat depth, GLint stencil); typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERFVPROC)(GLenum buffer, GLint drawbuffer, const GLfloat * value); typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERIVPROC)(GLenum buffer, GLint drawbuffer, const GLint * value); typedef void (GLAD_API_PTR *PFNGLCLEARBUFFERUIVPROC)(GLenum buffer, GLint drawbuffer, const GLuint * value); typedef void (GLAD_API_PTR *PFNGLCLEARCOLORPROC)(GLfloat red, GLfloat green, GLfloat blue, GLfloat alpha); typedef void (GLAD_API_PTR *PFNGLCLEARDEPTHPROC)(GLdouble depth); +typedef void (GLAD_API_PTR *PFNGLCLEARDEPTHFPROC)(GLfloat d); typedef void (GLAD_API_PTR *PFNGLCLEARSTENCILPROC)(GLint s); typedef GLenum (GLAD_API_PTR *PFNGLCLIENTWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout); typedef void (GLAD_API_PTR *PFNGLCOLORMASKPROC)(GLboolean red, GLboolean green, GLboolean blue, GLboolean alpha); @@ -1121,6 +1624,7 @@ typedef void (GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE1DPROC)(GLenum target, GLi typedef void (GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLsizei imageSize, const void * data); typedef void (GLAD_API_PTR *PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLsizei imageSize, const void * data); typedef void (GLAD_API_PTR *PFNGLCOPYBUFFERSUBDATAPROC)(GLenum readTarget, GLenum writeTarget, GLintptr readOffset, GLintptr writeOffset, GLsizeiptr size); +typedef void (GLAD_API_PTR *PFNGLCOPYIMAGESUBDATAPROC)(GLuint srcName, GLenum srcTarget, GLint srcLevel, GLint srcX, GLint srcY, GLint srcZ, GLuint dstName, GLenum dstTarget, GLint dstLevel, GLint dstX, GLint dstY, GLint dstZ, GLsizei srcWidth, GLsizei srcHeight, GLsizei srcDepth); typedef void (GLAD_API_PTR *PFNGLCOPYTEXIMAGE1DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLint border); typedef void (GLAD_API_PTR *PFNGLCOPYTEXIMAGE2DPROC)(GLenum target, GLint level, GLenum internalformat, GLint x, GLint y, GLsizei width, GLsizei height, GLint border); typedef void (GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLint x, GLint y, GLsizei width); @@ -1128,44 +1632,66 @@ typedef void (GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE2DPROC)(GLenum target, GLint lev typedef void (GLAD_API_PTR *PFNGLCOPYTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLint x, GLint y, GLsizei width, GLsizei height); typedef GLuint (GLAD_API_PTR *PFNGLCREATEPROGRAMPROC)(void); typedef GLuint (GLAD_API_PTR *PFNGLCREATESHADERPROC)(GLenum type); +typedef GLuint (GLAD_API_PTR *PFNGLCREATESHADERPROGRAMVPROC)(GLenum type, GLsizei count, const GLchar *const* strings); typedef void (GLAD_API_PTR *PFNGLCULLFACEPROC)(GLenum mode); +typedef void (GLAD_API_PTR *PFNGLDEBUGMESSAGECALLBACKPROC)(GLDEBUGPROC callback, const void * userParam); +typedef void (GLAD_API_PTR *PFNGLDEBUGMESSAGECONTROLPROC)(GLenum source, GLenum type, GLenum severity, GLsizei count, const GLuint * ids, GLboolean enabled); +typedef void (GLAD_API_PTR *PFNGLDEBUGMESSAGEINSERTPROC)(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar * buf); typedef void (GLAD_API_PTR *PFNGLDELETEBUFFERSPROC)(GLsizei n, const GLuint * buffers); typedef void (GLAD_API_PTR *PFNGLDELETEFRAMEBUFFERSPROC)(GLsizei n, const GLuint * framebuffers); typedef void (GLAD_API_PTR *PFNGLDELETEPROGRAMPROC)(GLuint program); +typedef void (GLAD_API_PTR *PFNGLDELETEPROGRAMPIPELINESPROC)(GLsizei n, const GLuint * pipelines); typedef void (GLAD_API_PTR *PFNGLDELETEQUERIESPROC)(GLsizei n, const GLuint * ids); typedef void (GLAD_API_PTR *PFNGLDELETERENDERBUFFERSPROC)(GLsizei n, const GLuint * renderbuffers); typedef void (GLAD_API_PTR *PFNGLDELETESAMPLERSPROC)(GLsizei count, const GLuint * samplers); typedef void (GLAD_API_PTR *PFNGLDELETESHADERPROC)(GLuint shader); typedef void (GLAD_API_PTR *PFNGLDELETESYNCPROC)(GLsync sync); typedef void (GLAD_API_PTR *PFNGLDELETETEXTURESPROC)(GLsizei n, const GLuint * textures); +typedef void (GLAD_API_PTR *PFNGLDELETETRANSFORMFEEDBACKSPROC)(GLsizei n, const GLuint * ids); typedef void (GLAD_API_PTR *PFNGLDELETEVERTEXARRAYSPROC)(GLsizei n, const GLuint * arrays); typedef void (GLAD_API_PTR *PFNGLDEPTHFUNCPROC)(GLenum func); typedef void (GLAD_API_PTR *PFNGLDEPTHMASKPROC)(GLboolean flag); typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEPROC)(GLdouble n, GLdouble f); +typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEARRAYVPROC)(GLuint first, GLsizei count, const GLdouble * v); +typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEINDEXEDPROC)(GLuint index, GLdouble n, GLdouble f); +typedef void (GLAD_API_PTR *PFNGLDEPTHRANGEFPROC)(GLfloat n, GLfloat f); typedef void (GLAD_API_PTR *PFNGLDETACHSHADERPROC)(GLuint program, GLuint shader); typedef void (GLAD_API_PTR *PFNGLDISABLEPROC)(GLenum cap); typedef void (GLAD_API_PTR *PFNGLDISABLEVERTEXATTRIBARRAYPROC)(GLuint index); typedef void (GLAD_API_PTR *PFNGLDISABLEIPROC)(GLenum target, GLuint index); +typedef void (GLAD_API_PTR *PFNGLDISPATCHCOMPUTEPROC)(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z); +typedef void (GLAD_API_PTR *PFNGLDISPATCHCOMPUTEINDIRECTPROC)(GLintptr indirect); typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSPROC)(GLenum mode, GLint first, GLsizei count); +typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSINDIRECTPROC)(GLenum mode, const void * indirect); typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount); +typedef void (GLAD_API_PTR *PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance); typedef void (GLAD_API_PTR *PFNGLDRAWBUFFERPROC)(GLenum buf); typedef void (GLAD_API_PTR *PFNGLDRAWBUFFERSPROC)(GLsizei n, const GLenum * bufs); typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices); typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLint basevertex); +typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void * indirect); typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount); +typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount, GLuint baseinstance); typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount, GLint basevertex); +typedef void (GLAD_API_PTR *PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC)(GLenum mode, GLsizei count, GLenum type, const void * indices, GLsizei instancecount, GLint basevertex, GLuint baseinstance); typedef void (GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void * indices); typedef void (GLAD_API_PTR *PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC)(GLenum mode, GLuint start, GLuint end, GLsizei count, GLenum type, const void * indices, GLint basevertex); +typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKPROC)(GLenum mode, GLuint id); +typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC)(GLenum mode, GLuint id, GLsizei instancecount); +typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC)(GLenum mode, GLuint id, GLuint stream); +typedef void (GLAD_API_PTR *PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC)(GLenum mode, GLuint id, GLuint stream, GLsizei instancecount); typedef void (GLAD_API_PTR *PFNGLENABLEPROC)(GLenum cap); typedef void (GLAD_API_PTR *PFNGLENABLEVERTEXATTRIBARRAYPROC)(GLuint index); typedef void (GLAD_API_PTR *PFNGLENABLEIPROC)(GLenum target, GLuint index); typedef void (GLAD_API_PTR *PFNGLENDCONDITIONALRENDERPROC)(void); typedef void (GLAD_API_PTR *PFNGLENDQUERYPROC)(GLenum target); +typedef void (GLAD_API_PTR *PFNGLENDQUERYINDEXEDPROC)(GLenum target, GLuint index); typedef void (GLAD_API_PTR *PFNGLENDTRANSFORMFEEDBACKPROC)(void); typedef GLsync (GLAD_API_PTR *PFNGLFENCESYNCPROC)(GLenum condition, GLbitfield flags); typedef void (GLAD_API_PTR *PFNGLFINISHPROC)(void); typedef void (GLAD_API_PTR *PFNGLFLUSHPROC)(void); typedef void (GLAD_API_PTR *PFNGLFLUSHMAPPEDBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length); +typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERPARAMETERIPROC)(GLenum target, GLenum pname, GLint param); typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERRENDERBUFFERPROC)(GLenum target, GLenum attachment, GLenum renderbuffertarget, GLuint renderbuffer); typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTUREPROC)(GLenum target, GLenum attachment, GLuint texture, GLint level); typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURE1DPROC)(GLenum target, GLenum attachment, GLenum textarget, GLuint texture, GLint level); @@ -1175,13 +1701,19 @@ typedef void (GLAD_API_PTR *PFNGLFRAMEBUFFERTEXTURELAYERPROC)(GLenum target, GLe typedef void (GLAD_API_PTR *PFNGLFRONTFACEPROC)(GLenum mode); typedef void (GLAD_API_PTR *PFNGLGENBUFFERSPROC)(GLsizei n, GLuint * buffers); typedef void (GLAD_API_PTR *PFNGLGENFRAMEBUFFERSPROC)(GLsizei n, GLuint * framebuffers); +typedef void (GLAD_API_PTR *PFNGLGENPROGRAMPIPELINESPROC)(GLsizei n, GLuint * pipelines); typedef void (GLAD_API_PTR *PFNGLGENQUERIESPROC)(GLsizei n, GLuint * ids); typedef void (GLAD_API_PTR *PFNGLGENRENDERBUFFERSPROC)(GLsizei n, GLuint * renderbuffers); typedef void (GLAD_API_PTR *PFNGLGENSAMPLERSPROC)(GLsizei count, GLuint * samplers); typedef void (GLAD_API_PTR *PFNGLGENTEXTURESPROC)(GLsizei n, GLuint * textures); +typedef void (GLAD_API_PTR *PFNGLGENTRANSFORMFEEDBACKSPROC)(GLsizei n, GLuint * ids); typedef void (GLAD_API_PTR *PFNGLGENVERTEXARRAYSPROC)(GLsizei n, GLuint * arrays); typedef void (GLAD_API_PTR *PFNGLGENERATEMIPMAPPROC)(GLenum target); +typedef void (GLAD_API_PTR *PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC)(GLuint program, GLuint bufferIndex, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETACTIVEATTRIBPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei * length, GLint * size, GLenum * type, GLchar * name); +typedef void (GLAD_API_PTR *PFNGLGETACTIVESUBROUTINENAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei * length, GLchar * name); +typedef void (GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC)(GLuint program, GLenum shadertype, GLuint index, GLsizei bufSize, GLsizei * length, GLchar * name); +typedef void (GLAD_API_PTR *PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC)(GLuint program, GLenum shadertype, GLuint index, GLenum pname, GLint * values); typedef void (GLAD_API_PTR *PFNGLGETACTIVEUNIFORMPROC)(GLuint program, GLuint index, GLsizei bufSize, GLsizei * length, GLint * size, GLenum * type, GLchar * name); typedef void (GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC)(GLuint program, GLuint uniformBlockIndex, GLsizei bufSize, GLsizei * length, GLchar * uniformBlockName); typedef void (GLAD_API_PTR *PFNGLGETACTIVEUNIFORMBLOCKIVPROC)(GLuint program, GLuint uniformBlockIndex, GLenum pname, GLint * params); @@ -1196,19 +1728,39 @@ typedef void (GLAD_API_PTR *PFNGLGETBUFFERPARAMETERIVPROC)(GLenum target, GLenum typedef void (GLAD_API_PTR *PFNGLGETBUFFERPOINTERVPROC)(GLenum target, GLenum pname, void ** params); typedef void (GLAD_API_PTR *PFNGLGETBUFFERSUBDATAPROC)(GLenum target, GLintptr offset, GLsizeiptr size, void * data); typedef void (GLAD_API_PTR *PFNGLGETCOMPRESSEDTEXIMAGEPROC)(GLenum target, GLint level, void * img); +typedef GLuint (GLAD_API_PTR *PFNGLGETDEBUGMESSAGELOGPROC)(GLuint count, GLsizei bufSize, GLenum * sources, GLenum * types, GLuint * ids, GLenum * severities, GLsizei * lengths, GLchar * messageLog); +typedef void (GLAD_API_PTR *PFNGLGETDOUBLEI_VPROC)(GLenum target, GLuint index, GLdouble * data); typedef void (GLAD_API_PTR *PFNGLGETDOUBLEVPROC)(GLenum pname, GLdouble * data); typedef GLenum (GLAD_API_PTR *PFNGLGETERRORPROC)(void); +typedef void (GLAD_API_PTR *PFNGLGETFLOATI_VPROC)(GLenum target, GLuint index, GLfloat * data); typedef void (GLAD_API_PTR *PFNGLGETFLOATVPROC)(GLenum pname, GLfloat * data); typedef GLint (GLAD_API_PTR *PFNGLGETFRAGDATAINDEXPROC)(GLuint program, const GLchar * name); typedef GLint (GLAD_API_PTR *PFNGLGETFRAGDATALOCATIONPROC)(GLuint program, const GLchar * name); typedef void (GLAD_API_PTR *PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC)(GLenum target, GLenum attachment, GLenum pname, GLint * params); +typedef void (GLAD_API_PTR *PFNGLGETFRAMEBUFFERPARAMETERIVPROC)(GLenum target, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETINTEGER64I_VPROC)(GLenum target, GLuint index, GLint64 * data); typedef void (GLAD_API_PTR *PFNGLGETINTEGER64VPROC)(GLenum pname, GLint64 * data); typedef void (GLAD_API_PTR *PFNGLGETINTEGERI_VPROC)(GLenum target, GLuint index, GLint * data); typedef void (GLAD_API_PTR *PFNGLGETINTEGERVPROC)(GLenum pname, GLint * data); +typedef void (GLAD_API_PTR *PFNGLGETINTERNALFORMATI64VPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint64 * params); +typedef void (GLAD_API_PTR *PFNGLGETINTERNALFORMATIVPROC)(GLenum target, GLenum internalformat, GLenum pname, GLsizei count, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETMULTISAMPLEFVPROC)(GLenum pname, GLuint index, GLfloat * val); +typedef void (GLAD_API_PTR *PFNGLGETOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei bufSize, GLsizei * length, GLchar * label); +typedef void (GLAD_API_PTR *PFNGLGETOBJECTPTRLABELPROC)(const void * ptr, GLsizei bufSize, GLsizei * length, GLchar * label); +typedef void (GLAD_API_PTR *PFNGLGETPOINTERVPROC)(GLenum pname, void ** params); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMBINARYPROC)(GLuint program, GLsizei bufSize, GLsizei * length, GLenum * binaryFormat, void * binary); typedef void (GLAD_API_PTR *PFNGLGETPROGRAMINFOLOGPROC)(GLuint program, GLsizei bufSize, GLsizei * length, GLchar * infoLog); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMINTERFACEIVPROC)(GLuint program, GLenum programInterface, GLenum pname, GLint * params); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEINFOLOGPROC)(GLuint pipeline, GLsizei bufSize, GLsizei * length, GLchar * infoLog); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMPIPELINEIVPROC)(GLuint pipeline, GLenum pname, GLint * params); +typedef GLuint (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEINDEXPROC)(GLuint program, GLenum programInterface, const GLchar * name); +typedef GLint (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONPROC)(GLuint program, GLenum programInterface, const GLchar * name); +typedef GLint (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC)(GLuint program, GLenum programInterface, const GLchar * name); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCENAMEPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei bufSize, GLsizei * length, GLchar * name); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMRESOURCEIVPROC)(GLuint program, GLenum programInterface, GLuint index, GLsizei propCount, const GLenum * props, GLsizei count, GLsizei * length, GLint * params); +typedef void (GLAD_API_PTR *PFNGLGETPROGRAMSTAGEIVPROC)(GLuint program, GLenum shadertype, GLenum pname, GLint * values); typedef void (GLAD_API_PTR *PFNGLGETPROGRAMIVPROC)(GLuint program, GLenum pname, GLint * params); +typedef void (GLAD_API_PTR *PFNGLGETQUERYINDEXEDIVPROC)(GLenum target, GLuint index, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETQUERYOBJECTI64VPROC)(GLuint id, GLenum pname, GLint64 * params); typedef void (GLAD_API_PTR *PFNGLGETQUERYOBJECTIVPROC)(GLuint id, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETQUERYOBJECTUI64VPROC)(GLuint id, GLenum pname, GLuint64 * params); @@ -1220,10 +1772,13 @@ typedef void (GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIUIVPROC)(GLuint sampler, GL typedef void (GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum pname, GLfloat * params); typedef void (GLAD_API_PTR *PFNGLGETSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETSHADERINFOLOGPROC)(GLuint shader, GLsizei bufSize, GLsizei * length, GLchar * infoLog); +typedef void (GLAD_API_PTR *PFNGLGETSHADERPRECISIONFORMATPROC)(GLenum shadertype, GLenum precisiontype, GLint * range, GLint * precision); typedef void (GLAD_API_PTR *PFNGLGETSHADERSOURCEPROC)(GLuint shader, GLsizei bufSize, GLsizei * length, GLchar * source); typedef void (GLAD_API_PTR *PFNGLGETSHADERIVPROC)(GLuint shader, GLenum pname, GLint * params); typedef const GLubyte * (GLAD_API_PTR *PFNGLGETSTRINGPROC)(GLenum name); typedef const GLubyte * (GLAD_API_PTR *PFNGLGETSTRINGIPROC)(GLenum name, GLuint index); +typedef GLuint (GLAD_API_PTR *PFNGLGETSUBROUTINEINDEXPROC)(GLuint program, GLenum shadertype, const GLchar * name); +typedef GLint (GLAD_API_PTR *PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC)(GLuint program, GLenum shadertype, const GLchar * name); typedef void (GLAD_API_PTR *PFNGLGETSYNCIVPROC)(GLsync sync, GLenum pname, GLsizei count, GLsizei * length, GLint * values); typedef void (GLAD_API_PTR *PFNGLGETTEXIMAGEPROC)(GLenum target, GLint level, GLenum format, GLenum type, void * pixels); typedef void (GLAD_API_PTR *PFNGLGETTEXLEVELPARAMETERFVPROC)(GLenum target, GLint level, GLenum pname, GLfloat * params); @@ -1236,36 +1791,56 @@ typedef void (GLAD_API_PTR *PFNGLGETTRANSFORMFEEDBACKVARYINGPROC)(GLuint program typedef GLuint (GLAD_API_PTR *PFNGLGETUNIFORMBLOCKINDEXPROC)(GLuint program, const GLchar * uniformBlockName); typedef void (GLAD_API_PTR *PFNGLGETUNIFORMINDICESPROC)(GLuint program, GLsizei uniformCount, const GLchar *const* uniformNames, GLuint * uniformIndices); typedef GLint (GLAD_API_PTR *PFNGLGETUNIFORMLOCATIONPROC)(GLuint program, const GLchar * name); +typedef void (GLAD_API_PTR *PFNGLGETUNIFORMSUBROUTINEUIVPROC)(GLenum shadertype, GLint location, GLuint * params); +typedef void (GLAD_API_PTR *PFNGLGETUNIFORMDVPROC)(GLuint program, GLint location, GLdouble * params); typedef void (GLAD_API_PTR *PFNGLGETUNIFORMFVPROC)(GLuint program, GLint location, GLfloat * params); typedef void (GLAD_API_PTR *PFNGLGETUNIFORMIVPROC)(GLuint program, GLint location, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETUNIFORMUIVPROC)(GLuint program, GLint location, GLuint * params); typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBIIVPROC)(GLuint index, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBIUIVPROC)(GLuint index, GLenum pname, GLuint * params); +typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBLDVPROC)(GLuint index, GLenum pname, GLdouble * params); typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBPOINTERVPROC)(GLuint index, GLenum pname, void ** pointer); typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBDVPROC)(GLuint index, GLenum pname, GLdouble * params); typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBFVPROC)(GLuint index, GLenum pname, GLfloat * params); typedef void (GLAD_API_PTR *PFNGLGETVERTEXATTRIBIVPROC)(GLuint index, GLenum pname, GLint * params); typedef void (GLAD_API_PTR *PFNGLHINTPROC)(GLenum target, GLenum mode); +typedef void (GLAD_API_PTR *PFNGLINVALIDATEBUFFERDATAPROC)(GLuint buffer); +typedef void (GLAD_API_PTR *PFNGLINVALIDATEBUFFERSUBDATAPROC)(GLuint buffer, GLintptr offset, GLsizeiptr length); +typedef void (GLAD_API_PTR *PFNGLINVALIDATEFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum * attachments); +typedef void (GLAD_API_PTR *PFNGLINVALIDATESUBFRAMEBUFFERPROC)(GLenum target, GLsizei numAttachments, const GLenum * attachments, GLint x, GLint y, GLsizei width, GLsizei height); +typedef void (GLAD_API_PTR *PFNGLINVALIDATETEXIMAGEPROC)(GLuint texture, GLint level); +typedef void (GLAD_API_PTR *PFNGLINVALIDATETEXSUBIMAGEPROC)(GLuint texture, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth); typedef GLboolean (GLAD_API_PTR *PFNGLISBUFFERPROC)(GLuint buffer); typedef GLboolean (GLAD_API_PTR *PFNGLISENABLEDPROC)(GLenum cap); typedef GLboolean (GLAD_API_PTR *PFNGLISENABLEDIPROC)(GLenum target, GLuint index); typedef GLboolean (GLAD_API_PTR *PFNGLISFRAMEBUFFERPROC)(GLuint framebuffer); typedef GLboolean (GLAD_API_PTR *PFNGLISPROGRAMPROC)(GLuint program); +typedef GLboolean (GLAD_API_PTR *PFNGLISPROGRAMPIPELINEPROC)(GLuint pipeline); typedef GLboolean (GLAD_API_PTR *PFNGLISQUERYPROC)(GLuint id); typedef GLboolean (GLAD_API_PTR *PFNGLISRENDERBUFFERPROC)(GLuint renderbuffer); typedef GLboolean (GLAD_API_PTR *PFNGLISSAMPLERPROC)(GLuint sampler); typedef GLboolean (GLAD_API_PTR *PFNGLISSHADERPROC)(GLuint shader); typedef GLboolean (GLAD_API_PTR *PFNGLISSYNCPROC)(GLsync sync); typedef GLboolean (GLAD_API_PTR *PFNGLISTEXTUREPROC)(GLuint texture); +typedef GLboolean (GLAD_API_PTR *PFNGLISTRANSFORMFEEDBACKPROC)(GLuint id); typedef GLboolean (GLAD_API_PTR *PFNGLISVERTEXARRAYPROC)(GLuint array); typedef void (GLAD_API_PTR *PFNGLLINEWIDTHPROC)(GLfloat width); typedef void (GLAD_API_PTR *PFNGLLINKPROGRAMPROC)(GLuint program); typedef void (GLAD_API_PTR *PFNGLLOGICOPPROC)(GLenum opcode); typedef void * (GLAD_API_PTR *PFNGLMAPBUFFERPROC)(GLenum target, GLenum access); typedef void * (GLAD_API_PTR *PFNGLMAPBUFFERRANGEPROC)(GLenum target, GLintptr offset, GLsizeiptr length, GLbitfield access); +typedef void (GLAD_API_PTR *PFNGLMEMORYBARRIERPROC)(GLbitfield barriers); +typedef void (GLAD_API_PTR *PFNGLMINSAMPLESHADINGPROC)(GLfloat value); typedef void (GLAD_API_PTR *PFNGLMULTIDRAWARRAYSPROC)(GLenum mode, const GLint * first, const GLsizei * count, GLsizei drawcount); +typedef void (GLAD_API_PTR *PFNGLMULTIDRAWARRAYSINDIRECTPROC)(GLenum mode, const void * indirect, GLsizei drawcount, GLsizei stride); typedef void (GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSPROC)(GLenum mode, const GLsizei * count, GLenum type, const void *const* indices, GLsizei drawcount); typedef void (GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC)(GLenum mode, const GLsizei * count, GLenum type, const void *const* indices, GLsizei drawcount, const GLint * basevertex); +typedef void (GLAD_API_PTR *PFNGLMULTIDRAWELEMENTSINDIRECTPROC)(GLenum mode, GLenum type, const void * indirect, GLsizei drawcount, GLsizei stride); +typedef void (GLAD_API_PTR *PFNGLOBJECTLABELPROC)(GLenum identifier, GLuint name, GLsizei length, const GLchar * label); +typedef void (GLAD_API_PTR *PFNGLOBJECTPTRLABELPROC)(const void * ptr, GLsizei length, const GLchar * label); +typedef void (GLAD_API_PTR *PFNGLPATCHPARAMETERFVPROC)(GLenum pname, const GLfloat * values); +typedef void (GLAD_API_PTR *PFNGLPATCHPARAMETERIPROC)(GLenum pname, GLint value); +typedef void (GLAD_API_PTR *PFNGLPAUSETRANSFORMFEEDBACKPROC)(void); typedef void (GLAD_API_PTR *PFNGLPIXELSTOREFPROC)(GLenum pname, GLfloat param); typedef void (GLAD_API_PTR *PFNGLPIXELSTOREIPROC)(GLenum pname, GLint param); typedef void (GLAD_API_PTR *PFNGLPOINTPARAMETERFPROC)(GLenum pname, GLfloat param); @@ -1275,13 +1850,69 @@ typedef void (GLAD_API_PTR *PFNGLPOINTPARAMETERIVPROC)(GLenum pname, const GLint typedef void (GLAD_API_PTR *PFNGLPOINTSIZEPROC)(GLfloat size); typedef void (GLAD_API_PTR *PFNGLPOLYGONMODEPROC)(GLenum face, GLenum mode); typedef void (GLAD_API_PTR *PFNGLPOLYGONOFFSETPROC)(GLfloat factor, GLfloat units); +typedef void (GLAD_API_PTR *PFNGLPOPDEBUGGROUPPROC)(void); typedef void (GLAD_API_PTR *PFNGLPRIMITIVERESTARTINDEXPROC)(GLuint index); +typedef void (GLAD_API_PTR *PFNGLPROGRAMBINARYPROC)(GLuint program, GLenum binaryFormat, const void * binary, GLsizei length); +typedef void (GLAD_API_PTR *PFNGLPROGRAMPARAMETERIPROC)(GLuint program, GLenum pname, GLint value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DPROC)(GLuint program, GLint location, GLdouble v0); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FPROC)(GLuint program, GLint location, GLfloat v0); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IPROC)(GLuint program, GLint location, GLint v0); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIPROC)(GLuint program, GLint location, GLuint v0); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM1UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IPROC)(GLuint program, GLint location, GLint v0, GLint v1); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM2UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM3UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DPROC)(GLuint program, GLint location, GLdouble v0, GLdouble v1, GLdouble v2, GLdouble v3); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4DVPROC)(GLuint program, GLint location, GLsizei count, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FPROC)(GLuint program, GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4FVPROC)(GLuint program, GLint location, GLsizei count, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IPROC)(GLuint program, GLint location, GLint v0, GLint v1, GLint v2, GLint v3); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4IVPROC)(GLuint program, GLint location, GLsizei count, const GLint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIPROC)(GLuint program, GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORM4UIVPROC)(GLuint program, GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); +typedef void (GLAD_API_PTR *PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC)(GLuint program, GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); typedef void (GLAD_API_PTR *PFNGLPROVOKINGVERTEXPROC)(GLenum mode); +typedef void (GLAD_API_PTR *PFNGLPUSHDEBUGGROUPPROC)(GLenum source, GLuint id, GLsizei length, const GLchar * message); typedef void (GLAD_API_PTR *PFNGLQUERYCOUNTERPROC)(GLuint id, GLenum target); typedef void (GLAD_API_PTR *PFNGLREADBUFFERPROC)(GLenum src); typedef void (GLAD_API_PTR *PFNGLREADPIXELSPROC)(GLint x, GLint y, GLsizei width, GLsizei height, GLenum format, GLenum type, void * pixels); +typedef void (GLAD_API_PTR *PFNGLRELEASESHADERCOMPILERPROC)(void); typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEPROC)(GLenum target, GLenum internalformat, GLsizei width, GLsizei height); typedef void (GLAD_API_PTR *PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height); +typedef void (GLAD_API_PTR *PFNGLRESUMETRANSFORMFEEDBACKPROC)(void); typedef void (GLAD_API_PTR *PFNGLSAMPLECOVERAGEPROC)(GLfloat value, GLboolean invert); typedef void (GLAD_API_PTR *PFNGLSAMPLEMASKIPROC)(GLuint maskNumber, GLbitfield mask); typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERIIVPROC)(GLuint sampler, GLenum pname, const GLint * param); @@ -1291,7 +1922,12 @@ typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERFVPROC)(GLuint sampler, GLenum typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERIPROC)(GLuint sampler, GLenum pname, GLint param); typedef void (GLAD_API_PTR *PFNGLSAMPLERPARAMETERIVPROC)(GLuint sampler, GLenum pname, const GLint * param); typedef void (GLAD_API_PTR *PFNGLSCISSORPROC)(GLint x, GLint y, GLsizei width, GLsizei height); +typedef void (GLAD_API_PTR *PFNGLSCISSORARRAYVPROC)(GLuint first, GLsizei count, const GLint * v); +typedef void (GLAD_API_PTR *PFNGLSCISSORINDEXEDPROC)(GLuint index, GLint left, GLint bottom, GLsizei width, GLsizei height); +typedef void (GLAD_API_PTR *PFNGLSCISSORINDEXEDVPROC)(GLuint index, const GLint * v); +typedef void (GLAD_API_PTR *PFNGLSHADERBINARYPROC)(GLsizei count, const GLuint * shaders, GLenum binaryFormat, const void * binary, GLsizei length); typedef void (GLAD_API_PTR *PFNGLSHADERSOURCEPROC)(GLuint shader, GLsizei count, const GLchar *const* string, const GLint * length); +typedef void (GLAD_API_PTR *PFNGLSHADERSTORAGEBLOCKBINDINGPROC)(GLuint program, GLuint storageBlockIndex, GLuint storageBlockBinding); typedef void (GLAD_API_PTR *PFNGLSTENCILFUNCPROC)(GLenum func, GLint ref, GLuint mask); typedef void (GLAD_API_PTR *PFNGLSTENCILFUNCSEPARATEPROC)(GLenum face, GLenum func, GLint ref, GLuint mask); typedef void (GLAD_API_PTR *PFNGLSTENCILMASKPROC)(GLuint mask); @@ -1299,6 +1935,7 @@ typedef void (GLAD_API_PTR *PFNGLSTENCILMASKSEPARATEPROC)(GLenum face, GLuint ma typedef void (GLAD_API_PTR *PFNGLSTENCILOPPROC)(GLenum fail, GLenum zfail, GLenum zpass); typedef void (GLAD_API_PTR *PFNGLSTENCILOPSEPARATEPROC)(GLenum face, GLenum sfail, GLenum dpfail, GLenum dppass); typedef void (GLAD_API_PTR *PFNGLTEXBUFFERPROC)(GLenum target, GLenum internalformat, GLuint buffer); +typedef void (GLAD_API_PTR *PFNGLTEXBUFFERRANGEPROC)(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size); typedef void (GLAD_API_PTR *PFNGLTEXIMAGE1DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLint border, GLenum format, GLenum type, const void * pixels); typedef void (GLAD_API_PTR *PFNGLTEXIMAGE2DPROC)(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void * pixels); typedef void (GLAD_API_PTR *PFNGLTEXIMAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations); @@ -1310,28 +1947,42 @@ typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERFPROC)(GLenum target, GLenum pname, typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERFVPROC)(GLenum target, GLenum pname, const GLfloat * params); typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERIPROC)(GLenum target, GLenum pname, GLint param); typedef void (GLAD_API_PTR *PFNGLTEXPARAMETERIVPROC)(GLenum target, GLenum pname, const GLint * params); +typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE1DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width); +typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE2DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height); +typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE2DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLboolean fixedsamplelocations); +typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE3DPROC)(GLenum target, GLsizei levels, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth); +typedef void (GLAD_API_PTR *PFNGLTEXSTORAGE3DMULTISAMPLEPROC)(GLenum target, GLsizei samples, GLenum internalformat, GLsizei width, GLsizei height, GLsizei depth, GLboolean fixedsamplelocations); typedef void (GLAD_API_PTR *PFNGLTEXSUBIMAGE1DPROC)(GLenum target, GLint level, GLint xoffset, GLsizei width, GLenum format, GLenum type, const void * pixels); typedef void (GLAD_API_PTR *PFNGLTEXSUBIMAGE2DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const void * pixels); typedef void (GLAD_API_PTR *PFNGLTEXSUBIMAGE3DPROC)(GLenum target, GLint level, GLint xoffset, GLint yoffset, GLint zoffset, GLsizei width, GLsizei height, GLsizei depth, GLenum format, GLenum type, const void * pixels); +typedef void (GLAD_API_PTR *PFNGLTEXTUREVIEWPROC)(GLuint texture, GLenum target, GLuint origtexture, GLenum internalformat, GLuint minlevel, GLuint numlevels, GLuint minlayer, GLuint numlayers); typedef void (GLAD_API_PTR *PFNGLTRANSFORMFEEDBACKVARYINGSPROC)(GLuint program, GLsizei count, const GLchar *const* varyings, GLenum bufferMode); +typedef void (GLAD_API_PTR *PFNGLUNIFORM1DPROC)(GLint location, GLdouble x); +typedef void (GLAD_API_PTR *PFNGLUNIFORM1DVPROC)(GLint location, GLsizei count, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM1FPROC)(GLint location, GLfloat v0); typedef void (GLAD_API_PTR *PFNGLUNIFORM1FVPROC)(GLint location, GLsizei count, const GLfloat * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM1IPROC)(GLint location, GLint v0); typedef void (GLAD_API_PTR *PFNGLUNIFORM1IVPROC)(GLint location, GLsizei count, const GLint * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM1UIPROC)(GLint location, GLuint v0); typedef void (GLAD_API_PTR *PFNGLUNIFORM1UIVPROC)(GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORM2DPROC)(GLint location, GLdouble x, GLdouble y); +typedef void (GLAD_API_PTR *PFNGLUNIFORM2DVPROC)(GLint location, GLsizei count, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM2FPROC)(GLint location, GLfloat v0, GLfloat v1); typedef void (GLAD_API_PTR *PFNGLUNIFORM2FVPROC)(GLint location, GLsizei count, const GLfloat * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM2IPROC)(GLint location, GLint v0, GLint v1); typedef void (GLAD_API_PTR *PFNGLUNIFORM2IVPROC)(GLint location, GLsizei count, const GLint * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM2UIPROC)(GLint location, GLuint v0, GLuint v1); typedef void (GLAD_API_PTR *PFNGLUNIFORM2UIVPROC)(GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORM3DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z); +typedef void (GLAD_API_PTR *PFNGLUNIFORM3DVPROC)(GLint location, GLsizei count, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM3FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2); typedef void (GLAD_API_PTR *PFNGLUNIFORM3FVPROC)(GLint location, GLsizei count, const GLfloat * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM3IPROC)(GLint location, GLint v0, GLint v1, GLint v2); typedef void (GLAD_API_PTR *PFNGLUNIFORM3IVPROC)(GLint location, GLsizei count, const GLint * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM3UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2); typedef void (GLAD_API_PTR *PFNGLUNIFORM3UIVPROC)(GLint location, GLsizei count, const GLuint * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORM4DPROC)(GLint location, GLdouble x, GLdouble y, GLdouble z, GLdouble w); +typedef void (GLAD_API_PTR *PFNGLUNIFORM4DVPROC)(GLint location, GLsizei count, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM4FPROC)(GLint location, GLfloat v0, GLfloat v1, GLfloat v2, GLfloat v3); typedef void (GLAD_API_PTR *PFNGLUNIFORM4FVPROC)(GLint location, GLsizei count, const GLfloat * value); typedef void (GLAD_API_PTR *PFNGLUNIFORM4IPROC)(GLint location, GLint v0, GLint v1, GLint v2, GLint v3); @@ -1339,18 +1990,30 @@ typedef void (GLAD_API_PTR *PFNGLUNIFORM4IVPROC)(GLint location, GLsizei count, typedef void (GLAD_API_PTR *PFNGLUNIFORM4UIPROC)(GLint location, GLuint v0, GLuint v1, GLuint v2, GLuint v3); typedef void (GLAD_API_PTR *PFNGLUNIFORM4UIVPROC)(GLint location, GLsizei count, const GLuint * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMBLOCKBINDINGPROC)(GLuint program, GLuint uniformBlockIndex, GLuint uniformBlockBinding); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX2X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX3X4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X2FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3DVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLdouble * value); typedef void (GLAD_API_PTR *PFNGLUNIFORMMATRIX4X3FVPROC)(GLint location, GLsizei count, GLboolean transpose, const GLfloat * value); +typedef void (GLAD_API_PTR *PFNGLUNIFORMSUBROUTINESUIVPROC)(GLenum shadertype, GLsizei count, const GLuint * indices); typedef GLboolean (GLAD_API_PTR *PFNGLUNMAPBUFFERPROC)(GLenum target); typedef void (GLAD_API_PTR *PFNGLUSEPROGRAMPROC)(GLuint program); +typedef void (GLAD_API_PTR *PFNGLUSEPROGRAMSTAGESPROC)(GLuint pipeline, GLbitfield stages, GLuint program); typedef void (GLAD_API_PTR *PFNGLVALIDATEPROGRAMPROC)(GLuint program); +typedef void (GLAD_API_PTR *PFNGLVALIDATEPROGRAMPIPELINEPROC)(GLuint pipeline); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB1DPROC)(GLuint index, GLdouble x); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB1DVPROC)(GLuint index, const GLdouble * v); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB1FPROC)(GLuint index, GLfloat x); @@ -1387,7 +2050,9 @@ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4SVPROC)(GLuint index, const GLshor typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4UBVPROC)(GLuint index, const GLubyte * v); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4UIVPROC)(GLuint index, const GLuint * v); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIB4USVPROC)(GLuint index, const GLushort * v); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBBINDINGPROC)(GLuint attribindex, GLuint bindingindex); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBDIVISORPROC)(GLuint index, GLuint divisor); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLboolean normalized, GLuint relativeoffset); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI1IPROC)(GLuint index, GLint x); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI1IVPROC)(GLuint index, const GLint * v); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI1UIPROC)(GLuint index, GLuint x); @@ -1408,7 +2073,18 @@ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4UBVPROC)(GLuint index, const GLub typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIPROC)(GLuint index, GLuint x, GLuint y, GLuint z, GLuint w); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4UIVPROC)(GLuint index, const GLuint * v); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBI4USVPROC)(GLuint index, const GLushort * v); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBIFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBIPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void * pointer); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL1DPROC)(GLuint index, GLdouble x); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL1DVPROC)(GLuint index, const GLdouble * v); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL2DPROC)(GLuint index, GLdouble x, GLdouble y); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL2DVPROC)(GLuint index, const GLdouble * v); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL3DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL3DVPROC)(GLuint index, const GLdouble * v); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL4DPROC)(GLuint index, GLdouble x, GLdouble y, GLdouble z, GLdouble w); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBL4DVPROC)(GLuint index, const GLdouble * v); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBLFORMATPROC)(GLuint attribindex, GLint size, GLenum type, GLuint relativeoffset); +typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBLPOINTERPROC)(GLuint index, GLint size, GLenum type, GLsizei stride, const void * pointer); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP1UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint * value); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP2UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); @@ -1418,7 +2094,11 @@ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP3UIVPROC)(GLuint index, GLenum typ typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIPROC)(GLuint index, GLenum type, GLboolean normalized, GLuint value); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBP4UIVPROC)(GLuint index, GLenum type, GLboolean normalized, const GLuint * value); typedef void (GLAD_API_PTR *PFNGLVERTEXATTRIBPOINTERPROC)(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const void * pointer); +typedef void (GLAD_API_PTR *PFNGLVERTEXBINDINGDIVISORPROC)(GLuint bindingindex, GLuint divisor); typedef void (GLAD_API_PTR *PFNGLVIEWPORTPROC)(GLint x, GLint y, GLsizei width, GLsizei height); +typedef void (GLAD_API_PTR *PFNGLVIEWPORTARRAYVPROC)(GLuint first, GLsizei count, const GLfloat * v); +typedef void (GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFPROC)(GLuint index, GLfloat x, GLfloat y, GLfloat w, GLfloat h); +typedef void (GLAD_API_PTR *PFNGLVIEWPORTINDEXEDFVPROC)(GLuint index, const GLfloat * v); typedef void (GLAD_API_PTR *PFNGLWAITSYNCPROC)(GLsync sync, GLbitfield flags, GLuint64 timeout); typedef struct GladGLContext { @@ -1436,11 +2116,17 @@ typedef struct GladGLContext { int VERSION_3_1; int VERSION_3_2; int VERSION_3_3; + int VERSION_4_0; + int VERSION_4_1; + int VERSION_4_2; + int VERSION_4_3; + PFNGLACTIVESHADERPROGRAMPROC ActiveShaderProgram; PFNGLACTIVETEXTUREPROC ActiveTexture; PFNGLATTACHSHADERPROC AttachShader; PFNGLBEGINCONDITIONALRENDERPROC BeginConditionalRender; PFNGLBEGINQUERYPROC BeginQuery; + PFNGLBEGINQUERYINDEXEDPROC BeginQueryIndexed; PFNGLBEGINTRANSFORMFEEDBACKPROC BeginTransformFeedback; PFNGLBINDATTRIBLOCATIONPROC BindAttribLocation; PFNGLBINDBUFFERPROC BindBuffer; @@ -1449,27 +2135,38 @@ typedef struct GladGLContext { PFNGLBINDFRAGDATALOCATIONPROC BindFragDataLocation; PFNGLBINDFRAGDATALOCATIONINDEXEDPROC BindFragDataLocationIndexed; PFNGLBINDFRAMEBUFFERPROC BindFramebuffer; + PFNGLBINDIMAGETEXTUREPROC BindImageTexture; + PFNGLBINDPROGRAMPIPELINEPROC BindProgramPipeline; PFNGLBINDRENDERBUFFERPROC BindRenderbuffer; PFNGLBINDSAMPLERPROC BindSampler; PFNGLBINDTEXTUREPROC BindTexture; + PFNGLBINDTRANSFORMFEEDBACKPROC BindTransformFeedback; PFNGLBINDVERTEXARRAYPROC BindVertexArray; + PFNGLBINDVERTEXBUFFERPROC BindVertexBuffer; PFNGLBLENDCOLORPROC BlendColor; PFNGLBLENDEQUATIONPROC BlendEquation; PFNGLBLENDEQUATIONSEPARATEPROC BlendEquationSeparate; + PFNGLBLENDEQUATIONSEPARATEIPROC BlendEquationSeparatei; + PFNGLBLENDEQUATIONIPROC BlendEquationi; PFNGLBLENDFUNCPROC BlendFunc; PFNGLBLENDFUNCSEPARATEPROC BlendFuncSeparate; + PFNGLBLENDFUNCSEPARATEIPROC BlendFuncSeparatei; + PFNGLBLENDFUNCIPROC BlendFunci; PFNGLBLITFRAMEBUFFERPROC BlitFramebuffer; PFNGLBUFFERDATAPROC BufferData; PFNGLBUFFERSUBDATAPROC BufferSubData; PFNGLCHECKFRAMEBUFFERSTATUSPROC CheckFramebufferStatus; PFNGLCLAMPCOLORPROC ClampColor; PFNGLCLEARPROC Clear; + PFNGLCLEARBUFFERDATAPROC ClearBufferData; + PFNGLCLEARBUFFERSUBDATAPROC ClearBufferSubData; PFNGLCLEARBUFFERFIPROC ClearBufferfi; PFNGLCLEARBUFFERFVPROC ClearBufferfv; PFNGLCLEARBUFFERIVPROC ClearBufferiv; PFNGLCLEARBUFFERUIVPROC ClearBufferuiv; PFNGLCLEARCOLORPROC ClearColor; PFNGLCLEARDEPTHPROC ClearDepth; + PFNGLCLEARDEPTHFPROC ClearDepthf; PFNGLCLEARSTENCILPROC ClearStencil; PFNGLCLIENTWAITSYNCPROC ClientWaitSync; PFNGLCOLORMASKPROC ColorMask; @@ -1482,6 +2179,7 @@ typedef struct GladGLContext { PFNGLCOMPRESSEDTEXSUBIMAGE2DPROC CompressedTexSubImage2D; PFNGLCOMPRESSEDTEXSUBIMAGE3DPROC CompressedTexSubImage3D; PFNGLCOPYBUFFERSUBDATAPROC CopyBufferSubData; + PFNGLCOPYIMAGESUBDATAPROC CopyImageSubData; PFNGLCOPYTEXIMAGE1DPROC CopyTexImage1D; PFNGLCOPYTEXIMAGE2DPROC CopyTexImage2D; PFNGLCOPYTEXSUBIMAGE1DPROC CopyTexSubImage1D; @@ -1489,44 +2187,66 @@ typedef struct GladGLContext { PFNGLCOPYTEXSUBIMAGE3DPROC CopyTexSubImage3D; PFNGLCREATEPROGRAMPROC CreateProgram; PFNGLCREATESHADERPROC CreateShader; + PFNGLCREATESHADERPROGRAMVPROC CreateShaderProgramv; PFNGLCULLFACEPROC CullFace; + PFNGLDEBUGMESSAGECALLBACKPROC DebugMessageCallback; + PFNGLDEBUGMESSAGECONTROLPROC DebugMessageControl; + PFNGLDEBUGMESSAGEINSERTPROC DebugMessageInsert; PFNGLDELETEBUFFERSPROC DeleteBuffers; PFNGLDELETEFRAMEBUFFERSPROC DeleteFramebuffers; PFNGLDELETEPROGRAMPROC DeleteProgram; + PFNGLDELETEPROGRAMPIPELINESPROC DeleteProgramPipelines; PFNGLDELETEQUERIESPROC DeleteQueries; PFNGLDELETERENDERBUFFERSPROC DeleteRenderbuffers; PFNGLDELETESAMPLERSPROC DeleteSamplers; PFNGLDELETESHADERPROC DeleteShader; PFNGLDELETESYNCPROC DeleteSync; PFNGLDELETETEXTURESPROC DeleteTextures; + PFNGLDELETETRANSFORMFEEDBACKSPROC DeleteTransformFeedbacks; PFNGLDELETEVERTEXARRAYSPROC DeleteVertexArrays; PFNGLDEPTHFUNCPROC DepthFunc; PFNGLDEPTHMASKPROC DepthMask; PFNGLDEPTHRANGEPROC DepthRange; + PFNGLDEPTHRANGEARRAYVPROC DepthRangeArrayv; + PFNGLDEPTHRANGEINDEXEDPROC DepthRangeIndexed; + PFNGLDEPTHRANGEFPROC DepthRangef; PFNGLDETACHSHADERPROC DetachShader; PFNGLDISABLEPROC Disable; PFNGLDISABLEVERTEXATTRIBARRAYPROC DisableVertexAttribArray; PFNGLDISABLEIPROC Disablei; + PFNGLDISPATCHCOMPUTEPROC DispatchCompute; + PFNGLDISPATCHCOMPUTEINDIRECTPROC DispatchComputeIndirect; PFNGLDRAWARRAYSPROC DrawArrays; + PFNGLDRAWARRAYSINDIRECTPROC DrawArraysIndirect; PFNGLDRAWARRAYSINSTANCEDPROC DrawArraysInstanced; + PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC DrawArraysInstancedBaseInstance; PFNGLDRAWBUFFERPROC DrawBuffer; PFNGLDRAWBUFFERSPROC DrawBuffers; PFNGLDRAWELEMENTSPROC DrawElements; PFNGLDRAWELEMENTSBASEVERTEXPROC DrawElementsBaseVertex; + PFNGLDRAWELEMENTSINDIRECTPROC DrawElementsIndirect; PFNGLDRAWELEMENTSINSTANCEDPROC DrawElementsInstanced; + PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC DrawElementsInstancedBaseInstance; PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXPROC DrawElementsInstancedBaseVertex; + PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC DrawElementsInstancedBaseVertexBaseInstance; PFNGLDRAWRANGEELEMENTSPROC DrawRangeElements; PFNGLDRAWRANGEELEMENTSBASEVERTEXPROC DrawRangeElementsBaseVertex; + PFNGLDRAWTRANSFORMFEEDBACKPROC DrawTransformFeedback; + PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC DrawTransformFeedbackInstanced; + PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC DrawTransformFeedbackStream; + PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC DrawTransformFeedbackStreamInstanced; PFNGLENABLEPROC Enable; PFNGLENABLEVERTEXATTRIBARRAYPROC EnableVertexAttribArray; PFNGLENABLEIPROC Enablei; PFNGLENDCONDITIONALRENDERPROC EndConditionalRender; PFNGLENDQUERYPROC EndQuery; + PFNGLENDQUERYINDEXEDPROC EndQueryIndexed; PFNGLENDTRANSFORMFEEDBACKPROC EndTransformFeedback; PFNGLFENCESYNCPROC FenceSync; PFNGLFINISHPROC Finish; PFNGLFLUSHPROC Flush; PFNGLFLUSHMAPPEDBUFFERRANGEPROC FlushMappedBufferRange; + PFNGLFRAMEBUFFERPARAMETERIPROC FramebufferParameteri; PFNGLFRAMEBUFFERRENDERBUFFERPROC FramebufferRenderbuffer; PFNGLFRAMEBUFFERTEXTUREPROC FramebufferTexture; PFNGLFRAMEBUFFERTEXTURE1DPROC FramebufferTexture1D; @@ -1536,13 +2256,19 @@ typedef struct GladGLContext { PFNGLFRONTFACEPROC FrontFace; PFNGLGENBUFFERSPROC GenBuffers; PFNGLGENFRAMEBUFFERSPROC GenFramebuffers; + PFNGLGENPROGRAMPIPELINESPROC GenProgramPipelines; PFNGLGENQUERIESPROC GenQueries; PFNGLGENRENDERBUFFERSPROC GenRenderbuffers; PFNGLGENSAMPLERSPROC GenSamplers; PFNGLGENTEXTURESPROC GenTextures; + PFNGLGENTRANSFORMFEEDBACKSPROC GenTransformFeedbacks; PFNGLGENVERTEXARRAYSPROC GenVertexArrays; PFNGLGENERATEMIPMAPPROC GenerateMipmap; + PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC GetActiveAtomicCounterBufferiv; PFNGLGETACTIVEATTRIBPROC GetActiveAttrib; + PFNGLGETACTIVESUBROUTINENAMEPROC GetActiveSubroutineName; + PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC GetActiveSubroutineUniformName; + PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC GetActiveSubroutineUniformiv; PFNGLGETACTIVEUNIFORMPROC GetActiveUniform; PFNGLGETACTIVEUNIFORMBLOCKNAMEPROC GetActiveUniformBlockName; PFNGLGETACTIVEUNIFORMBLOCKIVPROC GetActiveUniformBlockiv; @@ -1557,19 +2283,39 @@ typedef struct GladGLContext { PFNGLGETBUFFERPOINTERVPROC GetBufferPointerv; PFNGLGETBUFFERSUBDATAPROC GetBufferSubData; PFNGLGETCOMPRESSEDTEXIMAGEPROC GetCompressedTexImage; + PFNGLGETDEBUGMESSAGELOGPROC GetDebugMessageLog; + PFNGLGETDOUBLEI_VPROC GetDoublei_v; PFNGLGETDOUBLEVPROC GetDoublev; PFNGLGETERRORPROC GetError; + PFNGLGETFLOATI_VPROC GetFloati_v; PFNGLGETFLOATVPROC GetFloatv; PFNGLGETFRAGDATAINDEXPROC GetFragDataIndex; PFNGLGETFRAGDATALOCATIONPROC GetFragDataLocation; PFNGLGETFRAMEBUFFERATTACHMENTPARAMETERIVPROC GetFramebufferAttachmentParameteriv; + PFNGLGETFRAMEBUFFERPARAMETERIVPROC GetFramebufferParameteriv; PFNGLGETINTEGER64I_VPROC GetInteger64i_v; PFNGLGETINTEGER64VPROC GetInteger64v; PFNGLGETINTEGERI_VPROC GetIntegeri_v; PFNGLGETINTEGERVPROC GetIntegerv; + PFNGLGETINTERNALFORMATI64VPROC GetInternalformati64v; + PFNGLGETINTERNALFORMATIVPROC GetInternalformativ; PFNGLGETMULTISAMPLEFVPROC GetMultisamplefv; + PFNGLGETOBJECTLABELPROC GetObjectLabel; + PFNGLGETOBJECTPTRLABELPROC GetObjectPtrLabel; + PFNGLGETPOINTERVPROC GetPointerv; + PFNGLGETPROGRAMBINARYPROC GetProgramBinary; PFNGLGETPROGRAMINFOLOGPROC GetProgramInfoLog; + PFNGLGETPROGRAMINTERFACEIVPROC GetProgramInterfaceiv; + PFNGLGETPROGRAMPIPELINEINFOLOGPROC GetProgramPipelineInfoLog; + PFNGLGETPROGRAMPIPELINEIVPROC GetProgramPipelineiv; + PFNGLGETPROGRAMRESOURCEINDEXPROC GetProgramResourceIndex; + PFNGLGETPROGRAMRESOURCELOCATIONPROC GetProgramResourceLocation; + PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC GetProgramResourceLocationIndex; + PFNGLGETPROGRAMRESOURCENAMEPROC GetProgramResourceName; + PFNGLGETPROGRAMRESOURCEIVPROC GetProgramResourceiv; + PFNGLGETPROGRAMSTAGEIVPROC GetProgramStageiv; PFNGLGETPROGRAMIVPROC GetProgramiv; + PFNGLGETQUERYINDEXEDIVPROC GetQueryIndexediv; PFNGLGETQUERYOBJECTI64VPROC GetQueryObjecti64v; PFNGLGETQUERYOBJECTIVPROC GetQueryObjectiv; PFNGLGETQUERYOBJECTUI64VPROC GetQueryObjectui64v; @@ -1581,10 +2327,13 @@ typedef struct GladGLContext { PFNGLGETSAMPLERPARAMETERFVPROC GetSamplerParameterfv; PFNGLGETSAMPLERPARAMETERIVPROC GetSamplerParameteriv; PFNGLGETSHADERINFOLOGPROC GetShaderInfoLog; + PFNGLGETSHADERPRECISIONFORMATPROC GetShaderPrecisionFormat; PFNGLGETSHADERSOURCEPROC GetShaderSource; PFNGLGETSHADERIVPROC GetShaderiv; PFNGLGETSTRINGPROC GetString; PFNGLGETSTRINGIPROC GetStringi; + PFNGLGETSUBROUTINEINDEXPROC GetSubroutineIndex; + PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC GetSubroutineUniformLocation; PFNGLGETSYNCIVPROC GetSynciv; PFNGLGETTEXIMAGEPROC GetTexImage; PFNGLGETTEXLEVELPARAMETERFVPROC GetTexLevelParameterfv; @@ -1597,36 +2346,56 @@ typedef struct GladGLContext { PFNGLGETUNIFORMBLOCKINDEXPROC GetUniformBlockIndex; PFNGLGETUNIFORMINDICESPROC GetUniformIndices; PFNGLGETUNIFORMLOCATIONPROC GetUniformLocation; + PFNGLGETUNIFORMSUBROUTINEUIVPROC GetUniformSubroutineuiv; + PFNGLGETUNIFORMDVPROC GetUniformdv; PFNGLGETUNIFORMFVPROC GetUniformfv; PFNGLGETUNIFORMIVPROC GetUniformiv; PFNGLGETUNIFORMUIVPROC GetUniformuiv; PFNGLGETVERTEXATTRIBIIVPROC GetVertexAttribIiv; PFNGLGETVERTEXATTRIBIUIVPROC GetVertexAttribIuiv; + PFNGLGETVERTEXATTRIBLDVPROC GetVertexAttribLdv; PFNGLGETVERTEXATTRIBPOINTERVPROC GetVertexAttribPointerv; PFNGLGETVERTEXATTRIBDVPROC GetVertexAttribdv; PFNGLGETVERTEXATTRIBFVPROC GetVertexAttribfv; PFNGLGETVERTEXATTRIBIVPROC GetVertexAttribiv; PFNGLHINTPROC Hint; + PFNGLINVALIDATEBUFFERDATAPROC InvalidateBufferData; + PFNGLINVALIDATEBUFFERSUBDATAPROC InvalidateBufferSubData; + PFNGLINVALIDATEFRAMEBUFFERPROC InvalidateFramebuffer; + PFNGLINVALIDATESUBFRAMEBUFFERPROC InvalidateSubFramebuffer; + PFNGLINVALIDATETEXIMAGEPROC InvalidateTexImage; + PFNGLINVALIDATETEXSUBIMAGEPROC InvalidateTexSubImage; PFNGLISBUFFERPROC IsBuffer; PFNGLISENABLEDPROC IsEnabled; PFNGLISENABLEDIPROC IsEnabledi; PFNGLISFRAMEBUFFERPROC IsFramebuffer; PFNGLISPROGRAMPROC IsProgram; + PFNGLISPROGRAMPIPELINEPROC IsProgramPipeline; PFNGLISQUERYPROC IsQuery; PFNGLISRENDERBUFFERPROC IsRenderbuffer; PFNGLISSAMPLERPROC IsSampler; PFNGLISSHADERPROC IsShader; PFNGLISSYNCPROC IsSync; PFNGLISTEXTUREPROC IsTexture; + PFNGLISTRANSFORMFEEDBACKPROC IsTransformFeedback; PFNGLISVERTEXARRAYPROC IsVertexArray; PFNGLLINEWIDTHPROC LineWidth; PFNGLLINKPROGRAMPROC LinkProgram; PFNGLLOGICOPPROC LogicOp; PFNGLMAPBUFFERPROC MapBuffer; PFNGLMAPBUFFERRANGEPROC MapBufferRange; + PFNGLMEMORYBARRIERPROC MemoryBarrier; + PFNGLMINSAMPLESHADINGPROC MinSampleShading; PFNGLMULTIDRAWARRAYSPROC MultiDrawArrays; + PFNGLMULTIDRAWARRAYSINDIRECTPROC MultiDrawArraysIndirect; PFNGLMULTIDRAWELEMENTSPROC MultiDrawElements; PFNGLMULTIDRAWELEMENTSBASEVERTEXPROC MultiDrawElementsBaseVertex; + PFNGLMULTIDRAWELEMENTSINDIRECTPROC MultiDrawElementsIndirect; + PFNGLOBJECTLABELPROC ObjectLabel; + PFNGLOBJECTPTRLABELPROC ObjectPtrLabel; + PFNGLPATCHPARAMETERFVPROC PatchParameterfv; + PFNGLPATCHPARAMETERIPROC PatchParameteri; + PFNGLPAUSETRANSFORMFEEDBACKPROC PauseTransformFeedback; PFNGLPIXELSTOREFPROC PixelStoref; PFNGLPIXELSTOREIPROC PixelStorei; PFNGLPOINTPARAMETERFPROC PointParameterf; @@ -1636,13 +2405,69 @@ typedef struct GladGLContext { PFNGLPOINTSIZEPROC PointSize; PFNGLPOLYGONMODEPROC PolygonMode; PFNGLPOLYGONOFFSETPROC PolygonOffset; + PFNGLPOPDEBUGGROUPPROC PopDebugGroup; PFNGLPRIMITIVERESTARTINDEXPROC PrimitiveRestartIndex; + PFNGLPROGRAMBINARYPROC ProgramBinary; + PFNGLPROGRAMPARAMETERIPROC ProgramParameteri; + PFNGLPROGRAMUNIFORM1DPROC ProgramUniform1d; + PFNGLPROGRAMUNIFORM1DVPROC ProgramUniform1dv; + PFNGLPROGRAMUNIFORM1FPROC ProgramUniform1f; + PFNGLPROGRAMUNIFORM1FVPROC ProgramUniform1fv; + PFNGLPROGRAMUNIFORM1IPROC ProgramUniform1i; + PFNGLPROGRAMUNIFORM1IVPROC ProgramUniform1iv; + PFNGLPROGRAMUNIFORM1UIPROC ProgramUniform1ui; + PFNGLPROGRAMUNIFORM1UIVPROC ProgramUniform1uiv; + PFNGLPROGRAMUNIFORM2DPROC ProgramUniform2d; + PFNGLPROGRAMUNIFORM2DVPROC ProgramUniform2dv; + PFNGLPROGRAMUNIFORM2FPROC ProgramUniform2f; + PFNGLPROGRAMUNIFORM2FVPROC ProgramUniform2fv; + PFNGLPROGRAMUNIFORM2IPROC ProgramUniform2i; + PFNGLPROGRAMUNIFORM2IVPROC ProgramUniform2iv; + PFNGLPROGRAMUNIFORM2UIPROC ProgramUniform2ui; + PFNGLPROGRAMUNIFORM2UIVPROC ProgramUniform2uiv; + PFNGLPROGRAMUNIFORM3DPROC ProgramUniform3d; + PFNGLPROGRAMUNIFORM3DVPROC ProgramUniform3dv; + PFNGLPROGRAMUNIFORM3FPROC ProgramUniform3f; + PFNGLPROGRAMUNIFORM3FVPROC ProgramUniform3fv; + PFNGLPROGRAMUNIFORM3IPROC ProgramUniform3i; + PFNGLPROGRAMUNIFORM3IVPROC ProgramUniform3iv; + PFNGLPROGRAMUNIFORM3UIPROC ProgramUniform3ui; + PFNGLPROGRAMUNIFORM3UIVPROC ProgramUniform3uiv; + PFNGLPROGRAMUNIFORM4DPROC ProgramUniform4d; + PFNGLPROGRAMUNIFORM4DVPROC ProgramUniform4dv; + PFNGLPROGRAMUNIFORM4FPROC ProgramUniform4f; + PFNGLPROGRAMUNIFORM4FVPROC ProgramUniform4fv; + PFNGLPROGRAMUNIFORM4IPROC ProgramUniform4i; + PFNGLPROGRAMUNIFORM4IVPROC ProgramUniform4iv; + PFNGLPROGRAMUNIFORM4UIPROC ProgramUniform4ui; + PFNGLPROGRAMUNIFORM4UIVPROC ProgramUniform4uiv; + PFNGLPROGRAMUNIFORMMATRIX2DVPROC ProgramUniformMatrix2dv; + PFNGLPROGRAMUNIFORMMATRIX2FVPROC ProgramUniformMatrix2fv; + PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC ProgramUniformMatrix2x3dv; + PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC ProgramUniformMatrix2x3fv; + PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC ProgramUniformMatrix2x4dv; + PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC ProgramUniformMatrix2x4fv; + PFNGLPROGRAMUNIFORMMATRIX3DVPROC ProgramUniformMatrix3dv; + PFNGLPROGRAMUNIFORMMATRIX3FVPROC ProgramUniformMatrix3fv; + PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC ProgramUniformMatrix3x2dv; + PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC ProgramUniformMatrix3x2fv; + PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC ProgramUniformMatrix3x4dv; + PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC ProgramUniformMatrix3x4fv; + PFNGLPROGRAMUNIFORMMATRIX4DVPROC ProgramUniformMatrix4dv; + PFNGLPROGRAMUNIFORMMATRIX4FVPROC ProgramUniformMatrix4fv; + PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC ProgramUniformMatrix4x2dv; + PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC ProgramUniformMatrix4x2fv; + PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC ProgramUniformMatrix4x3dv; + PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC ProgramUniformMatrix4x3fv; PFNGLPROVOKINGVERTEXPROC ProvokingVertex; + PFNGLPUSHDEBUGGROUPPROC PushDebugGroup; PFNGLQUERYCOUNTERPROC QueryCounter; PFNGLREADBUFFERPROC ReadBuffer; PFNGLREADPIXELSPROC ReadPixels; + PFNGLRELEASESHADERCOMPILERPROC ReleaseShaderCompiler; PFNGLRENDERBUFFERSTORAGEPROC RenderbufferStorage; PFNGLRENDERBUFFERSTORAGEMULTISAMPLEPROC RenderbufferStorageMultisample; + PFNGLRESUMETRANSFORMFEEDBACKPROC ResumeTransformFeedback; PFNGLSAMPLECOVERAGEPROC SampleCoverage; PFNGLSAMPLEMASKIPROC SampleMaski; PFNGLSAMPLERPARAMETERIIVPROC SamplerParameterIiv; @@ -1652,7 +2477,12 @@ typedef struct GladGLContext { PFNGLSAMPLERPARAMETERIPROC SamplerParameteri; PFNGLSAMPLERPARAMETERIVPROC SamplerParameteriv; PFNGLSCISSORPROC Scissor; + PFNGLSCISSORARRAYVPROC ScissorArrayv; + PFNGLSCISSORINDEXEDPROC ScissorIndexed; + PFNGLSCISSORINDEXEDVPROC ScissorIndexedv; + PFNGLSHADERBINARYPROC ShaderBinary; PFNGLSHADERSOURCEPROC ShaderSource; + PFNGLSHADERSTORAGEBLOCKBINDINGPROC ShaderStorageBlockBinding; PFNGLSTENCILFUNCPROC StencilFunc; PFNGLSTENCILFUNCSEPARATEPROC StencilFuncSeparate; PFNGLSTENCILMASKPROC StencilMask; @@ -1660,6 +2490,7 @@ typedef struct GladGLContext { PFNGLSTENCILOPPROC StencilOp; PFNGLSTENCILOPSEPARATEPROC StencilOpSeparate; PFNGLTEXBUFFERPROC TexBuffer; + PFNGLTEXBUFFERRANGEPROC TexBufferRange; PFNGLTEXIMAGE1DPROC TexImage1D; PFNGLTEXIMAGE2DPROC TexImage2D; PFNGLTEXIMAGE2DMULTISAMPLEPROC TexImage2DMultisample; @@ -1671,28 +2502,42 @@ typedef struct GladGLContext { PFNGLTEXPARAMETERFVPROC TexParameterfv; PFNGLTEXPARAMETERIPROC TexParameteri; PFNGLTEXPARAMETERIVPROC TexParameteriv; + PFNGLTEXSTORAGE1DPROC TexStorage1D; + PFNGLTEXSTORAGE2DPROC TexStorage2D; + PFNGLTEXSTORAGE2DMULTISAMPLEPROC TexStorage2DMultisample; + PFNGLTEXSTORAGE3DPROC TexStorage3D; + PFNGLTEXSTORAGE3DMULTISAMPLEPROC TexStorage3DMultisample; PFNGLTEXSUBIMAGE1DPROC TexSubImage1D; PFNGLTEXSUBIMAGE2DPROC TexSubImage2D; PFNGLTEXSUBIMAGE3DPROC TexSubImage3D; + PFNGLTEXTUREVIEWPROC TextureView; PFNGLTRANSFORMFEEDBACKVARYINGSPROC TransformFeedbackVaryings; + PFNGLUNIFORM1DPROC Uniform1d; + PFNGLUNIFORM1DVPROC Uniform1dv; PFNGLUNIFORM1FPROC Uniform1f; PFNGLUNIFORM1FVPROC Uniform1fv; PFNGLUNIFORM1IPROC Uniform1i; PFNGLUNIFORM1IVPROC Uniform1iv; PFNGLUNIFORM1UIPROC Uniform1ui; PFNGLUNIFORM1UIVPROC Uniform1uiv; + PFNGLUNIFORM2DPROC Uniform2d; + PFNGLUNIFORM2DVPROC Uniform2dv; PFNGLUNIFORM2FPROC Uniform2f; PFNGLUNIFORM2FVPROC Uniform2fv; PFNGLUNIFORM2IPROC Uniform2i; PFNGLUNIFORM2IVPROC Uniform2iv; PFNGLUNIFORM2UIPROC Uniform2ui; PFNGLUNIFORM2UIVPROC Uniform2uiv; + PFNGLUNIFORM3DPROC Uniform3d; + PFNGLUNIFORM3DVPROC Uniform3dv; PFNGLUNIFORM3FPROC Uniform3f; PFNGLUNIFORM3FVPROC Uniform3fv; PFNGLUNIFORM3IPROC Uniform3i; PFNGLUNIFORM3IVPROC Uniform3iv; PFNGLUNIFORM3UIPROC Uniform3ui; PFNGLUNIFORM3UIVPROC Uniform3uiv; + PFNGLUNIFORM4DPROC Uniform4d; + PFNGLUNIFORM4DVPROC Uniform4dv; PFNGLUNIFORM4FPROC Uniform4f; PFNGLUNIFORM4FVPROC Uniform4fv; PFNGLUNIFORM4IPROC Uniform4i; @@ -1700,18 +2545,30 @@ typedef struct GladGLContext { PFNGLUNIFORM4UIPROC Uniform4ui; PFNGLUNIFORM4UIVPROC Uniform4uiv; PFNGLUNIFORMBLOCKBINDINGPROC UniformBlockBinding; + PFNGLUNIFORMMATRIX2DVPROC UniformMatrix2dv; PFNGLUNIFORMMATRIX2FVPROC UniformMatrix2fv; + PFNGLUNIFORMMATRIX2X3DVPROC UniformMatrix2x3dv; PFNGLUNIFORMMATRIX2X3FVPROC UniformMatrix2x3fv; + PFNGLUNIFORMMATRIX2X4DVPROC UniformMatrix2x4dv; PFNGLUNIFORMMATRIX2X4FVPROC UniformMatrix2x4fv; + PFNGLUNIFORMMATRIX3DVPROC UniformMatrix3dv; PFNGLUNIFORMMATRIX3FVPROC UniformMatrix3fv; + PFNGLUNIFORMMATRIX3X2DVPROC UniformMatrix3x2dv; PFNGLUNIFORMMATRIX3X2FVPROC UniformMatrix3x2fv; + PFNGLUNIFORMMATRIX3X4DVPROC UniformMatrix3x4dv; PFNGLUNIFORMMATRIX3X4FVPROC UniformMatrix3x4fv; + PFNGLUNIFORMMATRIX4DVPROC UniformMatrix4dv; PFNGLUNIFORMMATRIX4FVPROC UniformMatrix4fv; + PFNGLUNIFORMMATRIX4X2DVPROC UniformMatrix4x2dv; PFNGLUNIFORMMATRIX4X2FVPROC UniformMatrix4x2fv; + PFNGLUNIFORMMATRIX4X3DVPROC UniformMatrix4x3dv; PFNGLUNIFORMMATRIX4X3FVPROC UniformMatrix4x3fv; + PFNGLUNIFORMSUBROUTINESUIVPROC UniformSubroutinesuiv; PFNGLUNMAPBUFFERPROC UnmapBuffer; PFNGLUSEPROGRAMPROC UseProgram; + PFNGLUSEPROGRAMSTAGESPROC UseProgramStages; PFNGLVALIDATEPROGRAMPROC ValidateProgram; + PFNGLVALIDATEPROGRAMPIPELINEPROC ValidateProgramPipeline; PFNGLVERTEXATTRIB1DPROC VertexAttrib1d; PFNGLVERTEXATTRIB1DVPROC VertexAttrib1dv; PFNGLVERTEXATTRIB1FPROC VertexAttrib1f; @@ -1748,7 +2605,9 @@ typedef struct GladGLContext { PFNGLVERTEXATTRIB4UBVPROC VertexAttrib4ubv; PFNGLVERTEXATTRIB4UIVPROC VertexAttrib4uiv; PFNGLVERTEXATTRIB4USVPROC VertexAttrib4usv; + PFNGLVERTEXATTRIBBINDINGPROC VertexAttribBinding; PFNGLVERTEXATTRIBDIVISORPROC VertexAttribDivisor; + PFNGLVERTEXATTRIBFORMATPROC VertexAttribFormat; PFNGLVERTEXATTRIBI1IPROC VertexAttribI1i; PFNGLVERTEXATTRIBI1IVPROC VertexAttribI1iv; PFNGLVERTEXATTRIBI1UIPROC VertexAttribI1ui; @@ -1769,7 +2628,18 @@ typedef struct GladGLContext { PFNGLVERTEXATTRIBI4UIPROC VertexAttribI4ui; PFNGLVERTEXATTRIBI4UIVPROC VertexAttribI4uiv; PFNGLVERTEXATTRIBI4USVPROC VertexAttribI4usv; + PFNGLVERTEXATTRIBIFORMATPROC VertexAttribIFormat; PFNGLVERTEXATTRIBIPOINTERPROC VertexAttribIPointer; + PFNGLVERTEXATTRIBL1DPROC VertexAttribL1d; + PFNGLVERTEXATTRIBL1DVPROC VertexAttribL1dv; + PFNGLVERTEXATTRIBL2DPROC VertexAttribL2d; + PFNGLVERTEXATTRIBL2DVPROC VertexAttribL2dv; + PFNGLVERTEXATTRIBL3DPROC VertexAttribL3d; + PFNGLVERTEXATTRIBL3DVPROC VertexAttribL3dv; + PFNGLVERTEXATTRIBL4DPROC VertexAttribL4d; + PFNGLVERTEXATTRIBL4DVPROC VertexAttribL4dv; + PFNGLVERTEXATTRIBLFORMATPROC VertexAttribLFormat; + PFNGLVERTEXATTRIBLPOINTERPROC VertexAttribLPointer; PFNGLVERTEXATTRIBP1UIPROC VertexAttribP1ui; PFNGLVERTEXATTRIBP1UIVPROC VertexAttribP1uiv; PFNGLVERTEXATTRIBP2UIPROC VertexAttribP2ui; @@ -1779,7 +2649,11 @@ typedef struct GladGLContext { PFNGLVERTEXATTRIBP4UIPROC VertexAttribP4ui; PFNGLVERTEXATTRIBP4UIVPROC VertexAttribP4uiv; PFNGLVERTEXATTRIBPOINTERPROC VertexAttribPointer; + PFNGLVERTEXBINDINGDIVISORPROC VertexBindingDivisor; PFNGLVIEWPORTPROC Viewport; + PFNGLVIEWPORTARRAYVPROC ViewportArrayv; + PFNGLVIEWPORTINDEXEDFPROC ViewportIndexedf; + PFNGLVIEWPORTINDEXEDFVPROC ViewportIndexedfv; PFNGLWAITSYNCPROC WaitSync; void* glad_loader_handle; diff --git a/vendor/glad/include/glad/glad.h b/vendor/glad/include/glad/glad.h deleted file mode 100644 index f70d5b73f..000000000 --- a/vendor/glad/include/glad/glad.h +++ /dev/null @@ -1 +0,0 @@ -#include diff --git a/vendor/glad/src/gl.c b/vendor/glad/src/gl.c index ad49f387a..3eaf35450 100644 --- a/vendor/glad/src/gl.c +++ b/vendor/glad/src/gl.c @@ -90,6 +90,7 @@ static void glad_gl_load_GL_VERSION_1_1(GladGLContext *context, GLADuserptrloadf context->DrawArrays = (PFNGLDRAWARRAYSPROC) load(userptr, "glDrawArrays"); context->DrawElements = (PFNGLDRAWELEMENTSPROC) load(userptr, "glDrawElements"); context->GenTextures = (PFNGLGENTEXTURESPROC) load(userptr, "glGenTextures"); + context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv"); context->IsTexture = (PFNGLISTEXTUREPROC) load(userptr, "glIsTexture"); context->PolygonOffset = (PFNGLPOLYGONOFFSETPROC) load(userptr, "glPolygonOffset"); context->TexSubImage1D = (PFNGLTEXSUBIMAGE1DPROC) load(userptr, "glTexSubImage1D"); @@ -411,39 +412,229 @@ static void glad_gl_load_GL_VERSION_3_3(GladGLContext *context, GLADuserptrloadf context->VertexAttribP4ui = (PFNGLVERTEXATTRIBP4UIPROC) load(userptr, "glVertexAttribP4ui"); context->VertexAttribP4uiv = (PFNGLVERTEXATTRIBP4UIVPROC) load(userptr, "glVertexAttribP4uiv"); } +static void glad_gl_load_GL_VERSION_4_0(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { + if(!context->VERSION_4_0) return; + context->BeginQueryIndexed = (PFNGLBEGINQUERYINDEXEDPROC) load(userptr, "glBeginQueryIndexed"); + context->BindTransformFeedback = (PFNGLBINDTRANSFORMFEEDBACKPROC) load(userptr, "glBindTransformFeedback"); + context->BlendEquationSeparatei = (PFNGLBLENDEQUATIONSEPARATEIPROC) load(userptr, "glBlendEquationSeparatei"); + context->BlendEquationi = (PFNGLBLENDEQUATIONIPROC) load(userptr, "glBlendEquationi"); + context->BlendFuncSeparatei = (PFNGLBLENDFUNCSEPARATEIPROC) load(userptr, "glBlendFuncSeparatei"); + context->BlendFunci = (PFNGLBLENDFUNCIPROC) load(userptr, "glBlendFunci"); + context->DeleteTransformFeedbacks = (PFNGLDELETETRANSFORMFEEDBACKSPROC) load(userptr, "glDeleteTransformFeedbacks"); + context->DrawArraysIndirect = (PFNGLDRAWARRAYSINDIRECTPROC) load(userptr, "glDrawArraysIndirect"); + context->DrawElementsIndirect = (PFNGLDRAWELEMENTSINDIRECTPROC) load(userptr, "glDrawElementsIndirect"); + context->DrawTransformFeedback = (PFNGLDRAWTRANSFORMFEEDBACKPROC) load(userptr, "glDrawTransformFeedback"); + context->DrawTransformFeedbackStream = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMPROC) load(userptr, "glDrawTransformFeedbackStream"); + context->EndQueryIndexed = (PFNGLENDQUERYINDEXEDPROC) load(userptr, "glEndQueryIndexed"); + context->GenTransformFeedbacks = (PFNGLGENTRANSFORMFEEDBACKSPROC) load(userptr, "glGenTransformFeedbacks"); + context->GetActiveSubroutineName = (PFNGLGETACTIVESUBROUTINENAMEPROC) load(userptr, "glGetActiveSubroutineName"); + context->GetActiveSubroutineUniformName = (PFNGLGETACTIVESUBROUTINEUNIFORMNAMEPROC) load(userptr, "glGetActiveSubroutineUniformName"); + context->GetActiveSubroutineUniformiv = (PFNGLGETACTIVESUBROUTINEUNIFORMIVPROC) load(userptr, "glGetActiveSubroutineUniformiv"); + context->GetProgramStageiv = (PFNGLGETPROGRAMSTAGEIVPROC) load(userptr, "glGetProgramStageiv"); + context->GetQueryIndexediv = (PFNGLGETQUERYINDEXEDIVPROC) load(userptr, "glGetQueryIndexediv"); + context->GetSubroutineIndex = (PFNGLGETSUBROUTINEINDEXPROC) load(userptr, "glGetSubroutineIndex"); + context->GetSubroutineUniformLocation = (PFNGLGETSUBROUTINEUNIFORMLOCATIONPROC) load(userptr, "glGetSubroutineUniformLocation"); + context->GetUniformSubroutineuiv = (PFNGLGETUNIFORMSUBROUTINEUIVPROC) load(userptr, "glGetUniformSubroutineuiv"); + context->GetUniformdv = (PFNGLGETUNIFORMDVPROC) load(userptr, "glGetUniformdv"); + context->IsTransformFeedback = (PFNGLISTRANSFORMFEEDBACKPROC) load(userptr, "glIsTransformFeedback"); + context->MinSampleShading = (PFNGLMINSAMPLESHADINGPROC) load(userptr, "glMinSampleShading"); + context->PatchParameterfv = (PFNGLPATCHPARAMETERFVPROC) load(userptr, "glPatchParameterfv"); + context->PatchParameteri = (PFNGLPATCHPARAMETERIPROC) load(userptr, "glPatchParameteri"); + context->PauseTransformFeedback = (PFNGLPAUSETRANSFORMFEEDBACKPROC) load(userptr, "glPauseTransformFeedback"); + context->ResumeTransformFeedback = (PFNGLRESUMETRANSFORMFEEDBACKPROC) load(userptr, "glResumeTransformFeedback"); + context->Uniform1d = (PFNGLUNIFORM1DPROC) load(userptr, "glUniform1d"); + context->Uniform1dv = (PFNGLUNIFORM1DVPROC) load(userptr, "glUniform1dv"); + context->Uniform2d = (PFNGLUNIFORM2DPROC) load(userptr, "glUniform2d"); + context->Uniform2dv = (PFNGLUNIFORM2DVPROC) load(userptr, "glUniform2dv"); + context->Uniform3d = (PFNGLUNIFORM3DPROC) load(userptr, "glUniform3d"); + context->Uniform3dv = (PFNGLUNIFORM3DVPROC) load(userptr, "glUniform3dv"); + context->Uniform4d = (PFNGLUNIFORM4DPROC) load(userptr, "glUniform4d"); + context->Uniform4dv = (PFNGLUNIFORM4DVPROC) load(userptr, "glUniform4dv"); + context->UniformMatrix2dv = (PFNGLUNIFORMMATRIX2DVPROC) load(userptr, "glUniformMatrix2dv"); + context->UniformMatrix2x3dv = (PFNGLUNIFORMMATRIX2X3DVPROC) load(userptr, "glUniformMatrix2x3dv"); + context->UniformMatrix2x4dv = (PFNGLUNIFORMMATRIX2X4DVPROC) load(userptr, "glUniformMatrix2x4dv"); + context->UniformMatrix3dv = (PFNGLUNIFORMMATRIX3DVPROC) load(userptr, "glUniformMatrix3dv"); + context->UniformMatrix3x2dv = (PFNGLUNIFORMMATRIX3X2DVPROC) load(userptr, "glUniformMatrix3x2dv"); + context->UniformMatrix3x4dv = (PFNGLUNIFORMMATRIX3X4DVPROC) load(userptr, "glUniformMatrix3x4dv"); + context->UniformMatrix4dv = (PFNGLUNIFORMMATRIX4DVPROC) load(userptr, "glUniformMatrix4dv"); + context->UniformMatrix4x2dv = (PFNGLUNIFORMMATRIX4X2DVPROC) load(userptr, "glUniformMatrix4x2dv"); + context->UniformMatrix4x3dv = (PFNGLUNIFORMMATRIX4X3DVPROC) load(userptr, "glUniformMatrix4x3dv"); + context->UniformSubroutinesuiv = (PFNGLUNIFORMSUBROUTINESUIVPROC) load(userptr, "glUniformSubroutinesuiv"); +} +static void glad_gl_load_GL_VERSION_4_1(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { + if(!context->VERSION_4_1) return; + context->ActiveShaderProgram = (PFNGLACTIVESHADERPROGRAMPROC) load(userptr, "glActiveShaderProgram"); + context->BindProgramPipeline = (PFNGLBINDPROGRAMPIPELINEPROC) load(userptr, "glBindProgramPipeline"); + context->ClearDepthf = (PFNGLCLEARDEPTHFPROC) load(userptr, "glClearDepthf"); + context->CreateShaderProgramv = (PFNGLCREATESHADERPROGRAMVPROC) load(userptr, "glCreateShaderProgramv"); + context->DeleteProgramPipelines = (PFNGLDELETEPROGRAMPIPELINESPROC) load(userptr, "glDeleteProgramPipelines"); + context->DepthRangeArrayv = (PFNGLDEPTHRANGEARRAYVPROC) load(userptr, "glDepthRangeArrayv"); + context->DepthRangeIndexed = (PFNGLDEPTHRANGEINDEXEDPROC) load(userptr, "glDepthRangeIndexed"); + context->DepthRangef = (PFNGLDEPTHRANGEFPROC) load(userptr, "glDepthRangef"); + context->GenProgramPipelines = (PFNGLGENPROGRAMPIPELINESPROC) load(userptr, "glGenProgramPipelines"); + context->GetDoublei_v = (PFNGLGETDOUBLEI_VPROC) load(userptr, "glGetDoublei_v"); + context->GetFloati_v = (PFNGLGETFLOATI_VPROC) load(userptr, "glGetFloati_v"); + context->GetProgramBinary = (PFNGLGETPROGRAMBINARYPROC) load(userptr, "glGetProgramBinary"); + context->GetProgramPipelineInfoLog = (PFNGLGETPROGRAMPIPELINEINFOLOGPROC) load(userptr, "glGetProgramPipelineInfoLog"); + context->GetProgramPipelineiv = (PFNGLGETPROGRAMPIPELINEIVPROC) load(userptr, "glGetProgramPipelineiv"); + context->GetShaderPrecisionFormat = (PFNGLGETSHADERPRECISIONFORMATPROC) load(userptr, "glGetShaderPrecisionFormat"); + context->GetVertexAttribLdv = (PFNGLGETVERTEXATTRIBLDVPROC) load(userptr, "glGetVertexAttribLdv"); + context->IsProgramPipeline = (PFNGLISPROGRAMPIPELINEPROC) load(userptr, "glIsProgramPipeline"); + context->ProgramBinary = (PFNGLPROGRAMBINARYPROC) load(userptr, "glProgramBinary"); + context->ProgramParameteri = (PFNGLPROGRAMPARAMETERIPROC) load(userptr, "glProgramParameteri"); + context->ProgramUniform1d = (PFNGLPROGRAMUNIFORM1DPROC) load(userptr, "glProgramUniform1d"); + context->ProgramUniform1dv = (PFNGLPROGRAMUNIFORM1DVPROC) load(userptr, "glProgramUniform1dv"); + context->ProgramUniform1f = (PFNGLPROGRAMUNIFORM1FPROC) load(userptr, "glProgramUniform1f"); + context->ProgramUniform1fv = (PFNGLPROGRAMUNIFORM1FVPROC) load(userptr, "glProgramUniform1fv"); + context->ProgramUniform1i = (PFNGLPROGRAMUNIFORM1IPROC) load(userptr, "glProgramUniform1i"); + context->ProgramUniform1iv = (PFNGLPROGRAMUNIFORM1IVPROC) load(userptr, "glProgramUniform1iv"); + context->ProgramUniform1ui = (PFNGLPROGRAMUNIFORM1UIPROC) load(userptr, "glProgramUniform1ui"); + context->ProgramUniform1uiv = (PFNGLPROGRAMUNIFORM1UIVPROC) load(userptr, "glProgramUniform1uiv"); + context->ProgramUniform2d = (PFNGLPROGRAMUNIFORM2DPROC) load(userptr, "glProgramUniform2d"); + context->ProgramUniform2dv = (PFNGLPROGRAMUNIFORM2DVPROC) load(userptr, "glProgramUniform2dv"); + context->ProgramUniform2f = (PFNGLPROGRAMUNIFORM2FPROC) load(userptr, "glProgramUniform2f"); + context->ProgramUniform2fv = (PFNGLPROGRAMUNIFORM2FVPROC) load(userptr, "glProgramUniform2fv"); + context->ProgramUniform2i = (PFNGLPROGRAMUNIFORM2IPROC) load(userptr, "glProgramUniform2i"); + context->ProgramUniform2iv = (PFNGLPROGRAMUNIFORM2IVPROC) load(userptr, "glProgramUniform2iv"); + context->ProgramUniform2ui = (PFNGLPROGRAMUNIFORM2UIPROC) load(userptr, "glProgramUniform2ui"); + context->ProgramUniform2uiv = (PFNGLPROGRAMUNIFORM2UIVPROC) load(userptr, "glProgramUniform2uiv"); + context->ProgramUniform3d = (PFNGLPROGRAMUNIFORM3DPROC) load(userptr, "glProgramUniform3d"); + context->ProgramUniform3dv = (PFNGLPROGRAMUNIFORM3DVPROC) load(userptr, "glProgramUniform3dv"); + context->ProgramUniform3f = (PFNGLPROGRAMUNIFORM3FPROC) load(userptr, "glProgramUniform3f"); + context->ProgramUniform3fv = (PFNGLPROGRAMUNIFORM3FVPROC) load(userptr, "glProgramUniform3fv"); + context->ProgramUniform3i = (PFNGLPROGRAMUNIFORM3IPROC) load(userptr, "glProgramUniform3i"); + context->ProgramUniform3iv = (PFNGLPROGRAMUNIFORM3IVPROC) load(userptr, "glProgramUniform3iv"); + context->ProgramUniform3ui = (PFNGLPROGRAMUNIFORM3UIPROC) load(userptr, "glProgramUniform3ui"); + context->ProgramUniform3uiv = (PFNGLPROGRAMUNIFORM3UIVPROC) load(userptr, "glProgramUniform3uiv"); + context->ProgramUniform4d = (PFNGLPROGRAMUNIFORM4DPROC) load(userptr, "glProgramUniform4d"); + context->ProgramUniform4dv = (PFNGLPROGRAMUNIFORM4DVPROC) load(userptr, "glProgramUniform4dv"); + context->ProgramUniform4f = (PFNGLPROGRAMUNIFORM4FPROC) load(userptr, "glProgramUniform4f"); + context->ProgramUniform4fv = (PFNGLPROGRAMUNIFORM4FVPROC) load(userptr, "glProgramUniform4fv"); + context->ProgramUniform4i = (PFNGLPROGRAMUNIFORM4IPROC) load(userptr, "glProgramUniform4i"); + context->ProgramUniform4iv = (PFNGLPROGRAMUNIFORM4IVPROC) load(userptr, "glProgramUniform4iv"); + context->ProgramUniform4ui = (PFNGLPROGRAMUNIFORM4UIPROC) load(userptr, "glProgramUniform4ui"); + context->ProgramUniform4uiv = (PFNGLPROGRAMUNIFORM4UIVPROC) load(userptr, "glProgramUniform4uiv"); + context->ProgramUniformMatrix2dv = (PFNGLPROGRAMUNIFORMMATRIX2DVPROC) load(userptr, "glProgramUniformMatrix2dv"); + context->ProgramUniformMatrix2fv = (PFNGLPROGRAMUNIFORMMATRIX2FVPROC) load(userptr, "glProgramUniformMatrix2fv"); + context->ProgramUniformMatrix2x3dv = (PFNGLPROGRAMUNIFORMMATRIX2X3DVPROC) load(userptr, "glProgramUniformMatrix2x3dv"); + context->ProgramUniformMatrix2x3fv = (PFNGLPROGRAMUNIFORMMATRIX2X3FVPROC) load(userptr, "glProgramUniformMatrix2x3fv"); + context->ProgramUniformMatrix2x4dv = (PFNGLPROGRAMUNIFORMMATRIX2X4DVPROC) load(userptr, "glProgramUniformMatrix2x4dv"); + context->ProgramUniformMatrix2x4fv = (PFNGLPROGRAMUNIFORMMATRIX2X4FVPROC) load(userptr, "glProgramUniformMatrix2x4fv"); + context->ProgramUniformMatrix3dv = (PFNGLPROGRAMUNIFORMMATRIX3DVPROC) load(userptr, "glProgramUniformMatrix3dv"); + context->ProgramUniformMatrix3fv = (PFNGLPROGRAMUNIFORMMATRIX3FVPROC) load(userptr, "glProgramUniformMatrix3fv"); + context->ProgramUniformMatrix3x2dv = (PFNGLPROGRAMUNIFORMMATRIX3X2DVPROC) load(userptr, "glProgramUniformMatrix3x2dv"); + context->ProgramUniformMatrix3x2fv = (PFNGLPROGRAMUNIFORMMATRIX3X2FVPROC) load(userptr, "glProgramUniformMatrix3x2fv"); + context->ProgramUniformMatrix3x4dv = (PFNGLPROGRAMUNIFORMMATRIX3X4DVPROC) load(userptr, "glProgramUniformMatrix3x4dv"); + context->ProgramUniformMatrix3x4fv = (PFNGLPROGRAMUNIFORMMATRIX3X4FVPROC) load(userptr, "glProgramUniformMatrix3x4fv"); + context->ProgramUniformMatrix4dv = (PFNGLPROGRAMUNIFORMMATRIX4DVPROC) load(userptr, "glProgramUniformMatrix4dv"); + context->ProgramUniformMatrix4fv = (PFNGLPROGRAMUNIFORMMATRIX4FVPROC) load(userptr, "glProgramUniformMatrix4fv"); + context->ProgramUniformMatrix4x2dv = (PFNGLPROGRAMUNIFORMMATRIX4X2DVPROC) load(userptr, "glProgramUniformMatrix4x2dv"); + context->ProgramUniformMatrix4x2fv = (PFNGLPROGRAMUNIFORMMATRIX4X2FVPROC) load(userptr, "glProgramUniformMatrix4x2fv"); + context->ProgramUniformMatrix4x3dv = (PFNGLPROGRAMUNIFORMMATRIX4X3DVPROC) load(userptr, "glProgramUniformMatrix4x3dv"); + context->ProgramUniformMatrix4x3fv = (PFNGLPROGRAMUNIFORMMATRIX4X3FVPROC) load(userptr, "glProgramUniformMatrix4x3fv"); + context->ReleaseShaderCompiler = (PFNGLRELEASESHADERCOMPILERPROC) load(userptr, "glReleaseShaderCompiler"); + context->ScissorArrayv = (PFNGLSCISSORARRAYVPROC) load(userptr, "glScissorArrayv"); + context->ScissorIndexed = (PFNGLSCISSORINDEXEDPROC) load(userptr, "glScissorIndexed"); + context->ScissorIndexedv = (PFNGLSCISSORINDEXEDVPROC) load(userptr, "glScissorIndexedv"); + context->ShaderBinary = (PFNGLSHADERBINARYPROC) load(userptr, "glShaderBinary"); + context->UseProgramStages = (PFNGLUSEPROGRAMSTAGESPROC) load(userptr, "glUseProgramStages"); + context->ValidateProgramPipeline = (PFNGLVALIDATEPROGRAMPIPELINEPROC) load(userptr, "glValidateProgramPipeline"); + context->VertexAttribL1d = (PFNGLVERTEXATTRIBL1DPROC) load(userptr, "glVertexAttribL1d"); + context->VertexAttribL1dv = (PFNGLVERTEXATTRIBL1DVPROC) load(userptr, "glVertexAttribL1dv"); + context->VertexAttribL2d = (PFNGLVERTEXATTRIBL2DPROC) load(userptr, "glVertexAttribL2d"); + context->VertexAttribL2dv = (PFNGLVERTEXATTRIBL2DVPROC) load(userptr, "glVertexAttribL2dv"); + context->VertexAttribL3d = (PFNGLVERTEXATTRIBL3DPROC) load(userptr, "glVertexAttribL3d"); + context->VertexAttribL3dv = (PFNGLVERTEXATTRIBL3DVPROC) load(userptr, "glVertexAttribL3dv"); + context->VertexAttribL4d = (PFNGLVERTEXATTRIBL4DPROC) load(userptr, "glVertexAttribL4d"); + context->VertexAttribL4dv = (PFNGLVERTEXATTRIBL4DVPROC) load(userptr, "glVertexAttribL4dv"); + context->VertexAttribLPointer = (PFNGLVERTEXATTRIBLPOINTERPROC) load(userptr, "glVertexAttribLPointer"); + context->ViewportArrayv = (PFNGLVIEWPORTARRAYVPROC) load(userptr, "glViewportArrayv"); + context->ViewportIndexedf = (PFNGLVIEWPORTINDEXEDFPROC) load(userptr, "glViewportIndexedf"); + context->ViewportIndexedfv = (PFNGLVIEWPORTINDEXEDFVPROC) load(userptr, "glViewportIndexedfv"); +} +static void glad_gl_load_GL_VERSION_4_2(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { + if(!context->VERSION_4_2) return; + context->BindImageTexture = (PFNGLBINDIMAGETEXTUREPROC) load(userptr, "glBindImageTexture"); + context->DrawArraysInstancedBaseInstance = (PFNGLDRAWARRAYSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawArraysInstancedBaseInstance"); + context->DrawElementsInstancedBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseInstance"); + context->DrawElementsInstancedBaseVertexBaseInstance = (PFNGLDRAWELEMENTSINSTANCEDBASEVERTEXBASEINSTANCEPROC) load(userptr, "glDrawElementsInstancedBaseVertexBaseInstance"); + context->DrawTransformFeedbackInstanced = (PFNGLDRAWTRANSFORMFEEDBACKINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackInstanced"); + context->DrawTransformFeedbackStreamInstanced = (PFNGLDRAWTRANSFORMFEEDBACKSTREAMINSTANCEDPROC) load(userptr, "glDrawTransformFeedbackStreamInstanced"); + context->GetActiveAtomicCounterBufferiv = (PFNGLGETACTIVEATOMICCOUNTERBUFFERIVPROC) load(userptr, "glGetActiveAtomicCounterBufferiv"); + context->GetInternalformativ = (PFNGLGETINTERNALFORMATIVPROC) load(userptr, "glGetInternalformativ"); + context->MemoryBarrier = (PFNGLMEMORYBARRIERPROC) load(userptr, "glMemoryBarrier"); + context->TexStorage1D = (PFNGLTEXSTORAGE1DPROC) load(userptr, "glTexStorage1D"); + context->TexStorage2D = (PFNGLTEXSTORAGE2DPROC) load(userptr, "glTexStorage2D"); + context->TexStorage3D = (PFNGLTEXSTORAGE3DPROC) load(userptr, "glTexStorage3D"); +} +static void glad_gl_load_GL_VERSION_4_3(GladGLContext *context, GLADuserptrloadfunc load, void* userptr) { + if(!context->VERSION_4_3) return; + context->BindVertexBuffer = (PFNGLBINDVERTEXBUFFERPROC) load(userptr, "glBindVertexBuffer"); + context->ClearBufferData = (PFNGLCLEARBUFFERDATAPROC) load(userptr, "glClearBufferData"); + context->ClearBufferSubData = (PFNGLCLEARBUFFERSUBDATAPROC) load(userptr, "glClearBufferSubData"); + context->CopyImageSubData = (PFNGLCOPYIMAGESUBDATAPROC) load(userptr, "glCopyImageSubData"); + context->DebugMessageCallback = (PFNGLDEBUGMESSAGECALLBACKPROC) load(userptr, "glDebugMessageCallback"); + context->DebugMessageControl = (PFNGLDEBUGMESSAGECONTROLPROC) load(userptr, "glDebugMessageControl"); + context->DebugMessageInsert = (PFNGLDEBUGMESSAGEINSERTPROC) load(userptr, "glDebugMessageInsert"); + context->DispatchCompute = (PFNGLDISPATCHCOMPUTEPROC) load(userptr, "glDispatchCompute"); + context->DispatchComputeIndirect = (PFNGLDISPATCHCOMPUTEINDIRECTPROC) load(userptr, "glDispatchComputeIndirect"); + context->FramebufferParameteri = (PFNGLFRAMEBUFFERPARAMETERIPROC) load(userptr, "glFramebufferParameteri"); + context->GetDebugMessageLog = (PFNGLGETDEBUGMESSAGELOGPROC) load(userptr, "glGetDebugMessageLog"); + context->GetFramebufferParameteriv = (PFNGLGETFRAMEBUFFERPARAMETERIVPROC) load(userptr, "glGetFramebufferParameteriv"); + context->GetInternalformati64v = (PFNGLGETINTERNALFORMATI64VPROC) load(userptr, "glGetInternalformati64v"); + context->GetObjectLabel = (PFNGLGETOBJECTLABELPROC) load(userptr, "glGetObjectLabel"); + context->GetObjectPtrLabel = (PFNGLGETOBJECTPTRLABELPROC) load(userptr, "glGetObjectPtrLabel"); + context->GetPointerv = (PFNGLGETPOINTERVPROC) load(userptr, "glGetPointerv"); + context->GetProgramInterfaceiv = (PFNGLGETPROGRAMINTERFACEIVPROC) load(userptr, "glGetProgramInterfaceiv"); + context->GetProgramResourceIndex = (PFNGLGETPROGRAMRESOURCEINDEXPROC) load(userptr, "glGetProgramResourceIndex"); + context->GetProgramResourceLocation = (PFNGLGETPROGRAMRESOURCELOCATIONPROC) load(userptr, "glGetProgramResourceLocation"); + context->GetProgramResourceLocationIndex = (PFNGLGETPROGRAMRESOURCELOCATIONINDEXPROC) load(userptr, "glGetProgramResourceLocationIndex"); + context->GetProgramResourceName = (PFNGLGETPROGRAMRESOURCENAMEPROC) load(userptr, "glGetProgramResourceName"); + context->GetProgramResourceiv = (PFNGLGETPROGRAMRESOURCEIVPROC) load(userptr, "glGetProgramResourceiv"); + context->InvalidateBufferData = (PFNGLINVALIDATEBUFFERDATAPROC) load(userptr, "glInvalidateBufferData"); + context->InvalidateBufferSubData = (PFNGLINVALIDATEBUFFERSUBDATAPROC) load(userptr, "glInvalidateBufferSubData"); + context->InvalidateFramebuffer = (PFNGLINVALIDATEFRAMEBUFFERPROC) load(userptr, "glInvalidateFramebuffer"); + context->InvalidateSubFramebuffer = (PFNGLINVALIDATESUBFRAMEBUFFERPROC) load(userptr, "glInvalidateSubFramebuffer"); + context->InvalidateTexImage = (PFNGLINVALIDATETEXIMAGEPROC) load(userptr, "glInvalidateTexImage"); + context->InvalidateTexSubImage = (PFNGLINVALIDATETEXSUBIMAGEPROC) load(userptr, "glInvalidateTexSubImage"); + context->MultiDrawArraysIndirect = (PFNGLMULTIDRAWARRAYSINDIRECTPROC) load(userptr, "glMultiDrawArraysIndirect"); + context->MultiDrawElementsIndirect = (PFNGLMULTIDRAWELEMENTSINDIRECTPROC) load(userptr, "glMultiDrawElementsIndirect"); + context->ObjectLabel = (PFNGLOBJECTLABELPROC) load(userptr, "glObjectLabel"); + context->ObjectPtrLabel = (PFNGLOBJECTPTRLABELPROC) load(userptr, "glObjectPtrLabel"); + context->PopDebugGroup = (PFNGLPOPDEBUGGROUPPROC) load(userptr, "glPopDebugGroup"); + context->PushDebugGroup = (PFNGLPUSHDEBUGGROUPPROC) load(userptr, "glPushDebugGroup"); + context->ShaderStorageBlockBinding = (PFNGLSHADERSTORAGEBLOCKBINDINGPROC) load(userptr, "glShaderStorageBlockBinding"); + context->TexBufferRange = (PFNGLTEXBUFFERRANGEPROC) load(userptr, "glTexBufferRange"); + context->TexStorage2DMultisample = (PFNGLTEXSTORAGE2DMULTISAMPLEPROC) load(userptr, "glTexStorage2DMultisample"); + context->TexStorage3DMultisample = (PFNGLTEXSTORAGE3DMULTISAMPLEPROC) load(userptr, "glTexStorage3DMultisample"); + context->TextureView = (PFNGLTEXTUREVIEWPROC) load(userptr, "glTextureView"); + context->VertexAttribBinding = (PFNGLVERTEXATTRIBBINDINGPROC) load(userptr, "glVertexAttribBinding"); + context->VertexAttribFormat = (PFNGLVERTEXATTRIBFORMATPROC) load(userptr, "glVertexAttribFormat"); + context->VertexAttribIFormat = (PFNGLVERTEXATTRIBIFORMATPROC) load(userptr, "glVertexAttribIFormat"); + context->VertexAttribLFormat = (PFNGLVERTEXATTRIBLFORMATPROC) load(userptr, "glVertexAttribLFormat"); + context->VertexBindingDivisor = (PFNGLVERTEXBINDINGDIVISORPROC) load(userptr, "glVertexBindingDivisor"); +} -#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0) -#define GLAD_GL_IS_SOME_NEW_VERSION 1 -#else -#define GLAD_GL_IS_SOME_NEW_VERSION 0 -#endif - -static int glad_gl_get_extensions(GladGLContext *context, int version, const char **out_exts, unsigned int *out_num_exts_i, char ***out_exts_i) { -#if GLAD_GL_IS_SOME_NEW_VERSION - if(GLAD_VERSION_MAJOR(version) < 3) { -#else - GLAD_UNUSED(version); - GLAD_UNUSED(out_num_exts_i); - GLAD_UNUSED(out_exts_i); -#endif - if (context->GetString == NULL) { - return 0; +static void glad_gl_free_extensions(char **exts_i) { + if (exts_i != NULL) { + unsigned int index; + for(index = 0; exts_i[index]; index++) { + free((void *) (exts_i[index])); } - *out_exts = (const char *)context->GetString(GL_EXTENSIONS); -#if GLAD_GL_IS_SOME_NEW_VERSION - } else { + free((void *)exts_i); + exts_i = NULL; + } +} +static int glad_gl_get_extensions(GladGLContext *context, const char **out_exts, char ***out_exts_i) { +#if defined(GL_ES_VERSION_3_0) || defined(GL_VERSION_3_0) + if (context->GetStringi != NULL && context->GetIntegerv != NULL) { unsigned int index = 0; unsigned int num_exts_i = 0; char **exts_i = NULL; - if (context->GetStringi == NULL || context->GetIntegerv == NULL) { - return 0; - } context->GetIntegerv(GL_NUM_EXTENSIONS, (int*) &num_exts_i); - if (num_exts_i > 0) { - exts_i = (char **) malloc(num_exts_i * (sizeof *exts_i)); - } + exts_i = (char **) malloc((num_exts_i + 1) * (sizeof *exts_i)); if (exts_i == NULL) { return 0; } @@ -452,31 +643,40 @@ static int glad_gl_get_extensions(GladGLContext *context, int version, const cha size_t len = strlen(gl_str_tmp) + 1; char *local_str = (char*) malloc(len * sizeof(char)); - if(local_str != NULL) { - memcpy(local_str, gl_str_tmp, len * sizeof(char)); + if(local_str == NULL) { + exts_i[index] = NULL; + glad_gl_free_extensions(exts_i); + return 0; } + memcpy(local_str, gl_str_tmp, len * sizeof(char)); exts_i[index] = local_str; } + exts_i[index] = NULL; - *out_num_exts_i = num_exts_i; *out_exts_i = exts_i; + + return 1; } +#else + GLAD_UNUSED(out_exts_i); #endif + if (context->GetString == NULL) { + return 0; + } + *out_exts = (const char *)context->GetString(GL_EXTENSIONS); return 1; } -static void glad_gl_free_extensions(char **exts_i, unsigned int num_exts_i) { - if (exts_i != NULL) { +static int glad_gl_has_extension(const char *exts, char **exts_i, const char *ext) { + if(exts_i) { unsigned int index; - for(index = 0; index < num_exts_i; index++) { - free((void *) (exts_i[index])); + for(index = 0; exts_i[index]; index++) { + const char *e = exts_i[index]; + if(strcmp(e, ext) == 0) { + return 1; + } } - free((void *)exts_i); - exts_i = NULL; - } -} -static int glad_gl_has_extension(int version, const char *exts, unsigned int num_exts_i, char **exts_i, const char *ext) { - if(GLAD_VERSION_MAJOR(version) < 3 || !GLAD_GL_IS_SOME_NEW_VERSION) { + } else { const char *extensions; const char *loc; const char *terminator; @@ -496,14 +696,6 @@ static int glad_gl_has_extension(int version, const char *exts, unsigned int num } extensions = terminator; } - } else { - unsigned int index; - for(index = 0; index < num_exts_i; index++) { - const char *e = exts_i[index]; - if(strcmp(e, ext) == 0) { - return 1; - } - } } return 0; } @@ -512,15 +704,14 @@ static GLADapiproc glad_gl_get_proc_from_userptr(void *userptr, const char* name return (GLAD_GNUC_EXTENSION (GLADapiproc (*)(const char *name)) userptr)(name); } -static int glad_gl_find_extensions_gl(GladGLContext *context, int version) { +static int glad_gl_find_extensions_gl(GladGLContext *context) { const char *exts = NULL; - unsigned int num_exts_i = 0; char **exts_i = NULL; - if (!glad_gl_get_extensions(context, version, &exts, &num_exts_i, &exts_i)) return 0; + if (!glad_gl_get_extensions(context, &exts, &exts_i)) return 0; - GLAD_UNUSED(glad_gl_has_extension); + GLAD_UNUSED(&glad_gl_has_extension); - glad_gl_free_extensions(exts_i, num_exts_i); + glad_gl_free_extensions(exts_i); return 1; } @@ -561,6 +752,10 @@ static int glad_gl_find_core_gl(GladGLContext *context) { context->VERSION_3_1 = (major == 3 && minor >= 1) || major > 3; context->VERSION_3_2 = (major == 3 && minor >= 2) || major > 3; context->VERSION_3_3 = (major == 3 && minor >= 3) || major > 3; + context->VERSION_4_0 = (major == 4 && minor >= 0) || major > 4; + context->VERSION_4_1 = (major == 4 && minor >= 1) || major > 4; + context->VERSION_4_2 = (major == 4 && minor >= 2) || major > 4; + context->VERSION_4_3 = (major == 4 && minor >= 3) || major > 4; return GLAD_MAKE_VERSION(major, minor); } @@ -570,7 +765,6 @@ int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, v context->GetString = (PFNGLGETSTRINGPROC) load(userptr, "glGetString"); if(context->GetString == NULL) return 0; - if(context->GetString(GL_VERSION) == NULL) return 0; version = glad_gl_find_core_gl(context); glad_gl_load_GL_VERSION_1_0(context, load, userptr); @@ -585,8 +779,12 @@ int gladLoadGLContextUserPtr(GladGLContext *context, GLADuserptrloadfunc load, v glad_gl_load_GL_VERSION_3_1(context, load, userptr); glad_gl_load_GL_VERSION_3_2(context, load, userptr); glad_gl_load_GL_VERSION_3_3(context, load, userptr); + glad_gl_load_GL_VERSION_4_0(context, load, userptr); + glad_gl_load_GL_VERSION_4_1(context, load, userptr); + glad_gl_load_GL_VERSION_4_2(context, load, userptr); + glad_gl_load_GL_VERSION_4_3(context, load, userptr); - if (!glad_gl_find_extensions_gl(context, version)) return 0; + if (!glad_gl_find_extensions_gl(context)) return 0; From 371d62a82ce26a6dd7c61d0175759f2d53771065 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 16 Jun 2025 17:44:44 -0600 Subject: [PATCH 503/642] renderer: big rework, graphics API abstraction layers, unified logic This commit is very large, representing about a month of work with many interdependent changes that don't separate cleanly in to atomic commits. The main change here is unifying the renderer logic to a single generic renderer, implemented on top of an abstraction layer over OpenGL/Metal. I'll write a more complete summary of the changes in the description of the PR. --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 9 - pkg/macos/animation.zig | 2 + pkg/macos/build.zig | 2 + pkg/macos/dispatch.zig | 10 + pkg/macos/foundation.zig | 1 + pkg/macos/foundation/type.zig | 1 + pkg/macos/iosurface.zig | 8 + pkg/macos/iosurface/c.zig | 1 + pkg/macos/iosurface/iosurface.zig | 136 + pkg/macos/main.zig | 3 + pkg/macos/video.zig | 2 + pkg/macos/video/pixel_format.zig | 171 + pkg/opengl/Buffer.zig | 7 +- pkg/opengl/Framebuffer.zig | 24 + pkg/opengl/Renderbuffer.zig | 56 + pkg/opengl/Texture.zig | 29 +- pkg/opengl/VertexArray.zig | 84 + pkg/opengl/draw.zig | 39 +- pkg/opengl/main.zig | 9 + pkg/opengl/primitives.zig | 18 + src/Surface.zig | 5 +- src/apprt/gtk/App.zig | 5 + src/apprt/gtk/Surface.zig | 20 +- src/config/Config.zig | 10 +- src/renderer.zig | 5 +- src/renderer/Metal.zig | 3441 ++--------------- src/renderer/OpenGL.zig | 2799 ++------------ src/renderer/Options.zig | 3 + src/renderer/Thread.zig | 32 +- src/renderer/generic.zig | 2866 ++++++++++++++ src/renderer/metal/Frame.zig | 137 + src/renderer/metal/IOSurfaceLayer.zig | 187 + src/renderer/metal/Pipeline.zig | 203 + src/renderer/metal/RenderPass.zig | 220 ++ src/renderer/metal/Target.zig | 110 + src/renderer/metal/Texture.zig | 196 + src/renderer/metal/api.zig | 18 +- src/renderer/metal/buffer.zig | 77 +- src/renderer/metal/image.zig | 98 +- src/renderer/metal/sampler.zig | 38 - src/renderer/metal/shaders.zig | 331 +- src/renderer/opengl/CellProgram.zig | 196 - src/renderer/opengl/Frame.zig | 75 + src/renderer/opengl/ImageProgram.zig | 134 - src/renderer/opengl/Pipeline.zig | 170 + src/renderer/opengl/RenderPass.zig | 141 + src/renderer/opengl/Target.zig | 62 + src/renderer/opengl/Texture.zig | 99 + src/renderer/opengl/buffer.zig | 127 + src/renderer/opengl/cell.zig | 220 ++ src/renderer/opengl/custom.zig | 310 -- src/renderer/opengl/image.zig | 49 +- src/renderer/opengl/shaders.zig | 310 ++ src/renderer/shaders/cell.f.glsl | 53 - src/renderer/shaders/cell.metal | 68 +- src/renderer/shaders/cell.v.glsl | 258 -- src/renderer/shaders/custom.v.glsl | 8 - src/renderer/shaders/glsl/cell_bg.f.glsl | 61 + src/renderer/shaders/glsl/cell_text.f.glsl | 109 + src/renderer/shaders/glsl/cell_text.v.glsl | 162 + src/renderer/shaders/glsl/common.glsl | 155 + src/renderer/shaders/glsl/full_screen.v.glsl | 24 + src/renderer/shaders/glsl/image.f.glsl | 21 + src/renderer/shaders/glsl/image.v.glsl | 46 + src/renderer/shaders/image.f.glsl | 29 - src/renderer/shaders/image.v.glsl | 44 - src/renderer/shaders/shadertoy_prefix.glsl | 30 +- src/renderer/shadertoy.zig | 25 +- 68 files changed, 7088 insertions(+), 7311 deletions(-) create mode 100644 pkg/macos/iosurface.zig create mode 100644 pkg/macos/iosurface/c.zig create mode 100644 pkg/macos/iosurface/iosurface.zig create mode 100644 pkg/macos/video/pixel_format.zig create mode 100644 pkg/opengl/Renderbuffer.zig create mode 100644 pkg/opengl/primitives.zig create mode 100644 src/renderer/generic.zig create mode 100644 src/renderer/metal/Frame.zig create mode 100644 src/renderer/metal/IOSurfaceLayer.zig create mode 100644 src/renderer/metal/Pipeline.zig create mode 100644 src/renderer/metal/RenderPass.zig create mode 100644 src/renderer/metal/Target.zig create mode 100644 src/renderer/metal/Texture.zig delete mode 100644 src/renderer/metal/sampler.zig delete mode 100644 src/renderer/opengl/CellProgram.zig create mode 100644 src/renderer/opengl/Frame.zig delete mode 100644 src/renderer/opengl/ImageProgram.zig create mode 100644 src/renderer/opengl/Pipeline.zig create mode 100644 src/renderer/opengl/RenderPass.zig create mode 100644 src/renderer/opengl/Target.zig create mode 100644 src/renderer/opengl/Texture.zig create mode 100644 src/renderer/opengl/buffer.zig create mode 100644 src/renderer/opengl/cell.zig delete mode 100644 src/renderer/opengl/custom.zig create mode 100644 src/renderer/opengl/shaders.zig delete mode 100644 src/renderer/shaders/cell.f.glsl delete mode 100644 src/renderer/shaders/cell.v.glsl delete mode 100644 src/renderer/shaders/custom.v.glsl create mode 100644 src/renderer/shaders/glsl/cell_bg.f.glsl create mode 100644 src/renderer/shaders/glsl/cell_text.f.glsl create mode 100644 src/renderer/shaders/glsl/cell_text.v.glsl create mode 100644 src/renderer/shaders/glsl/common.glsl create mode 100644 src/renderer/shaders/glsl/full_screen.v.glsl create mode 100644 src/renderer/shaders/glsl/image.f.glsl create mode 100644 src/renderer/shaders/glsl/image.v.glsl delete mode 100644 src/renderer/shaders/image.f.glsl delete mode 100644 src/renderer/shaders/image.v.glsl diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index a47dbdaca..7be249b1a 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -147,10 +147,6 @@ extension Ghostty { // We need to support being a first responder so that we can get input events override var acceptsFirstResponder: Bool { return true } - // I don't think we need this but this lets us know we should redraw our layer - // so we'll use that to tell ghostty to refresh. - override var wantsUpdateLayer: Bool { return true } - init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) { self.markedText = NSMutableAttributedString() self.uuid = uuid ?? .init() @@ -703,11 +699,6 @@ extension Ghostty { setSurfaceSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height)) } - override func updateLayer() { - guard let surface = self.surface else { return } - ghostty_surface_draw(surface); - } - override func mouseDown(with event: NSEvent) { guard let surface = self.surface else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) diff --git a/pkg/macos/animation.zig b/pkg/macos/animation.zig index 5c3c8fd30..247f97605 100644 --- a/pkg/macos/animation.zig +++ b/pkg/macos/animation.zig @@ -2,6 +2,8 @@ pub const c = @import("animation/c.zig").c; /// https://developer.apple.com/documentation/quartzcore/calayer/contents_gravity_values?language=objc pub extern "c" const kCAGravityTopLeft: *anyopaque; +pub extern "c" const kCAGravityBottomLeft: *anyopaque; +pub extern "c" const kCAGravityCenter: *anyopaque; test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/macos/build.zig b/pkg/macos/build.zig index df76da9b4..3e0a97d1a 100644 --- a/pkg/macos/build.zig +++ b/pkg/macos/build.zig @@ -33,6 +33,7 @@ pub fn build(b: *std.Build) !void { lib.linkFramework("CoreText"); lib.linkFramework("CoreVideo"); lib.linkFramework("QuartzCore"); + lib.linkFramework("IOSurface"); if (target.result.os.tag == .macos) { lib.linkFramework("Carbon"); module.linkFramework("Carbon", .{}); @@ -44,6 +45,7 @@ pub fn build(b: *std.Build) !void { module.linkFramework("CoreText", .{}); module.linkFramework("CoreVideo", .{}); module.linkFramework("QuartzCore", .{}); + module.linkFramework("IOSurface", .{}); try apple_sdk.addPaths(b, lib); } diff --git a/pkg/macos/dispatch.zig b/pkg/macos/dispatch.zig index 2bc7e8396..3add9c0e9 100644 --- a/pkg/macos/dispatch.zig +++ b/pkg/macos/dispatch.zig @@ -3,6 +3,16 @@ pub const data = @import("dispatch/data.zig"); pub const queue = @import("dispatch/queue.zig"); pub const Data = data.Data; +pub extern "c" fn dispatch_sync( + queue: *anyopaque, + block: *anyopaque, +) void; + +pub extern "c" fn dispatch_async( + queue: *anyopaque, + block: *anyopaque, +) void; + test { @import("std").testing.refAllDecls(@This()); } diff --git a/pkg/macos/foundation.zig b/pkg/macos/foundation.zig index 85562faf0..d4f634091 100644 --- a/pkg/macos/foundation.zig +++ b/pkg/macos/foundation.zig @@ -30,6 +30,7 @@ pub const stringGetSurrogatePairForLongCharacter = string.stringGetSurrogatePair pub const URL = url.URL; pub const URLPathStyle = url.URLPathStyle; pub const CFRelease = typepkg.CFRelease; +pub const CFRetain = typepkg.CFRetain; test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/macos/foundation/type.zig b/pkg/macos/foundation/type.zig index e3ee150f2..45bd09054 100644 --- a/pkg/macos/foundation/type.zig +++ b/pkg/macos/foundation/type.zig @@ -1 +1,2 @@ pub extern "c" fn CFRelease(*anyopaque) void; +pub extern "c" fn CFRetain(*anyopaque) void; diff --git a/pkg/macos/iosurface.zig b/pkg/macos/iosurface.zig new file mode 100644 index 000000000..9d2e750cf --- /dev/null +++ b/pkg/macos/iosurface.zig @@ -0,0 +1,8 @@ +const iosurface = @import("iosurface/iosurface.zig"); + +pub const c = @import("iosurface/c.zig").c; +pub const IOSurface = iosurface.IOSurface; + +test { + @import("std").testing.refAllDecls(@This()); +} diff --git a/pkg/macos/iosurface/c.zig b/pkg/macos/iosurface/c.zig new file mode 100644 index 000000000..1a7d1627e --- /dev/null +++ b/pkg/macos/iosurface/c.zig @@ -0,0 +1 @@ +pub const c = @import("../main.zig").c; diff --git a/pkg/macos/iosurface/iosurface.zig b/pkg/macos/iosurface/iosurface.zig new file mode 100644 index 000000000..37f8712ba --- /dev/null +++ b/pkg/macos/iosurface/iosurface.zig @@ -0,0 +1,136 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const c = @import("c.zig").c; +const foundation = @import("../foundation.zig"); +const graphics = @import("../graphics.zig"); +const video = @import("../video.zig"); + +pub const IOSurface = opaque { + pub const Error = error{ + InvalidOperation, + }; + + pub const Properties = struct { + width: c_int, + height: c_int, + pixel_format: video.PixelFormat, + bytes_per_element: c_int, + colorspace: ?*graphics.ColorSpace, + }; + + pub fn init(properties: Properties) Allocator.Error!*IOSurface { + var w = try foundation.Number.create(.int, &properties.width); + defer w.release(); + var h = try foundation.Number.create(.int, &properties.height); + defer h.release(); + var pf = try foundation.Number.create(.int, &@as(c_int, @intFromEnum(properties.pixel_format))); + defer pf.release(); + var bpe = try foundation.Number.create(.int, &properties.bytes_per_element); + defer bpe.release(); + + var properties_dict = try foundation.Dictionary.create( + &[_]?*const anyopaque{ + c.kIOSurfaceWidth, + c.kIOSurfaceHeight, + c.kIOSurfacePixelFormat, + c.kIOSurfaceBytesPerElement, + }, + &[_]?*const anyopaque{ w, h, pf, bpe }, + ); + defer properties_dict.release(); + + var surface = @as(?*IOSurface, @ptrFromInt(@intFromPtr( + c.IOSurfaceCreate(@ptrCast(properties_dict)), + ))) orelse return error.OutOfMemory; + + if (properties.colorspace) |space| { + surface.setColorSpace(space); + } + + return surface; + } + + pub fn deinit(self: *IOSurface) void { + // We mark it purgeable so that it is immediately unloaded, so that we + // don't have to wait for CoreFoundation garbage collection to trigger. + _ = c.IOSurfaceSetPurgeable( + @ptrCast(self), + c.kIOSurfacePurgeableEmpty, + null, + ); + foundation.CFRelease(self); + } + + pub fn retain(self: *IOSurface) void { + foundation.CFRetain(self); + } + + pub fn release(self: *IOSurface) void { + foundation.CFRelease(self); + } + + pub fn setColorSpace(self: *IOSurface, colorspace: *graphics.ColorSpace) void { + const serialized_colorspace = graphics.c.CGColorSpaceCopyPropertyList( + @ptrCast(colorspace), + ).?; + defer foundation.CFRelease(@constCast(serialized_colorspace)); + + c.IOSurfaceSetValue( + @ptrCast(self), + c.kIOSurfaceColorSpace, + @ptrCast(serialized_colorspace), + ); + } + + pub inline fn lock(self: *IOSurface) void { + c.IOSurfaceLock( + @ptrCast(self), + 0, + null, + ); + } + pub inline fn unlock(self: *IOSurface) void { + c.IOSurfaceUnlock( + @ptrCast(self), + 0, + null, + ); + } + + pub inline fn getAllocSize(self: *IOSurface) usize { + return c.IOSurfaceGetAllocSize(@ptrCast(self)); + } + + pub inline fn getWidth(self: *IOSurface) usize { + return c.IOSurfaceGetWidth(@ptrCast(self)); + } + + pub inline fn getHeight(self: *IOSurface) usize { + return c.IOSurfaceGetHeight(@ptrCast(self)); + } + + pub inline fn getBytesPerElement(self: *IOSurface) usize { + return c.IOSurfaceGetBytesPerElement(@ptrCast(self)); + } + + pub inline fn getBytesPerRow(self: *IOSurface) usize { + return c.IOSurfaceGetBytesPerRow(@ptrCast(self)); + } + + pub inline fn getBaseAddress(self: *IOSurface) ?[*]u8 { + return @ptrCast(c.IOSurfaceGetBaseAddress(@ptrCast(self))); + } + + pub inline fn getElementWidth(self: *IOSurface) usize { + return c.IOSurfaceGetElementWidth(@ptrCast(self)); + } + + pub inline fn getElementHeight(self: *IOSurface) usize { + return c.IOSurfaceGetElementHeight(@ptrCast(self)); + } + + pub inline fn getPixelFormat(self: *IOSurface) video.PixelFormat { + return @enumFromInt(c.IOSurfaceGetPixelFormat(@ptrCast(self))); + } +}; diff --git a/pkg/macos/main.zig b/pkg/macos/main.zig index d094b987e..42253ba48 100644 --- a/pkg/macos/main.zig +++ b/pkg/macos/main.zig @@ -8,6 +8,7 @@ pub const graphics = @import("graphics.zig"); pub const os = @import("os.zig"); pub const text = @import("text.zig"); pub const video = @import("video.zig"); +pub const iosurface = @import("iosurface.zig"); // All of our C imports consolidated into one place. We used to // import them one by one in each package but Zig 0.14 has some @@ -17,7 +18,9 @@ pub const c = @cImport({ @cInclude("CoreGraphics/CoreGraphics.h"); @cInclude("CoreText/CoreText.h"); @cInclude("CoreVideo/CoreVideo.h"); + @cInclude("CoreVideo/CVPixelBuffer.h"); @cInclude("QuartzCore/CALayer.h"); + @cInclude("IOSurface/IOSurfaceRef.h"); @cInclude("dispatch/dispatch.h"); @cInclude("os/log.h"); diff --git a/pkg/macos/video.zig b/pkg/macos/video.zig index 0f5cbc4d6..d0b1125ab 100644 --- a/pkg/macos/video.zig +++ b/pkg/macos/video.zig @@ -1,7 +1,9 @@ const display_link = @import("video/display_link.zig"); +const pixel_format = @import("video/pixel_format.zig"); pub const c = @import("video/c.zig").c; pub const DisplayLink = display_link.DisplayLink; +pub const PixelFormat = pixel_format.PixelFormat; test { @import("std").testing.refAllDecls(@This()); diff --git a/pkg/macos/video/pixel_format.zig b/pkg/macos/video/pixel_format.zig new file mode 100644 index 000000000..30f11881e --- /dev/null +++ b/pkg/macos/video/pixel_format.zig @@ -0,0 +1,171 @@ +const c = @import("c.zig").c; + +pub const PixelFormat = enum(c_int) { + /// 1 bit indexed + @"1Monochrome" = c.kCVPixelFormatType_1Monochrome, + /// 2 bit indexed + @"2Indexed" = c.kCVPixelFormatType_2Indexed, + /// 4 bit indexed + @"4Indexed" = c.kCVPixelFormatType_4Indexed, + /// 8 bit indexed + @"8Indexed" = c.kCVPixelFormatType_8Indexed, + /// 1 bit indexed gray, white is zero + @"1IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_1IndexedGray_WhiteIsZero, + /// 2 bit indexed gray, white is zero + @"2IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_2IndexedGray_WhiteIsZero, + /// 4 bit indexed gray, white is zero + @"4IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_4IndexedGray_WhiteIsZero, + /// 8 bit indexed gray, white is zero + @"8IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_8IndexedGray_WhiteIsZero, + /// 16 bit BE RGB 555 + @"16BE555" = c.kCVPixelFormatType_16BE555, + /// 16 bit LE RGB 555 + @"16LE555" = c.kCVPixelFormatType_16LE555, + /// 16 bit LE RGB 5551 + @"16LE5551" = c.kCVPixelFormatType_16LE5551, + /// 16 bit BE RGB 565 + @"16BE565" = c.kCVPixelFormatType_16BE565, + /// 16 bit LE RGB 565 + @"16LE565" = c.kCVPixelFormatType_16LE565, + /// 24 bit RGB + @"24RGB" = c.kCVPixelFormatType_24RGB, + /// 24 bit BGR + @"24BGR" = c.kCVPixelFormatType_24BGR, + /// 32 bit ARGB + @"32ARGB" = c.kCVPixelFormatType_32ARGB, + /// 32 bit BGRA + @"32BGRA" = c.kCVPixelFormatType_32BGRA, + /// 32 bit ABGR + @"32ABGR" = c.kCVPixelFormatType_32ABGR, + /// 32 bit RGBA + @"32RGBA" = c.kCVPixelFormatType_32RGBA, + /// 64 bit ARGB, 16-bit big-endian samples + @"64ARGB" = c.kCVPixelFormatType_64ARGB, + /// 64 bit RGBA, 16-bit little-endian full-range (0-65535) samples + @"64RGBALE" = c.kCVPixelFormatType_64RGBALE, + /// 48 bit RGB, 16-bit big-endian samples + @"48RGB" = c.kCVPixelFormatType_48RGB, + /// 32 bit AlphaGray, 16-bit big-endian samples, black is zero + @"32AlphaGray" = c.kCVPixelFormatType_32AlphaGray, + /// 16 bit Grayscale, 16-bit big-endian samples, black is zero + @"16Gray" = c.kCVPixelFormatType_16Gray, + /// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at least significant end). + @"30RGB" = c.kCVPixelFormatType_30RGB, + /// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at most significant end), video-range (64-940). + @"30RGB_r210" = c.kCVPixelFormatType_30RGB_r210, + /// Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1 + @"422YpCbCr8" = c.kCVPixelFormatType_422YpCbCr8, + /// Component Y'CbCrA 8-bit 4:4:4:4, ordered Cb Y' Cr A + @"4444YpCbCrA8" = c.kCVPixelFormatType_4444YpCbCrA8, + /// Component Y'CbCrA 8-bit 4:4:4:4, rendering format. full range alpha, zero biased YUV, ordered A Y' Cb Cr + @"4444YpCbCrA8R" = c.kCVPixelFormatType_4444YpCbCrA8R, + /// Component Y'CbCrA 8-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr. + @"4444AYpCbCr8" = c.kCVPixelFormatType_4444AYpCbCr8, + /// Component Y'CbCrA 16-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr, 16-bit little-endian samples. + @"4444AYpCbCr16" = c.kCVPixelFormatType_4444AYpCbCr16, + /// Component AY'CbCr single precision floating-point 4:4:4:4 + @"4444AYpCbCrFloat" = c.kCVPixelFormatType_4444AYpCbCrFloat, + /// Component Y'CbCr 8-bit 4:4:4, ordered Cr Y' Cb, video range Y'CbCr + @"444YpCbCr8" = c.kCVPixelFormatType_444YpCbCr8, + /// Component Y'CbCr 10,12,14,16-bit 4:2:2 + @"422YpCbCr16" = c.kCVPixelFormatType_422YpCbCr16, + /// Component Y'CbCr 10-bit 4:2:2 + @"422YpCbCr10" = c.kCVPixelFormatType_422YpCbCr10, + /// Component Y'CbCr 10-bit 4:4:4 + @"444YpCbCr10" = c.kCVPixelFormatType_444YpCbCr10, + /// Planar Component Y'CbCr 8-bit 4:2:0. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct + @"420YpCbCr8Planar" = c.kCVPixelFormatType_420YpCbCr8Planar, + /// Planar Component Y'CbCr 8-bit 4:2:0, full range. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct + @"420YpCbCr8PlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8PlanarFullRange, + /// First plane: Video-range Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1; second plane: alpha 8-bit 0-255 + @"422YpCbCr_4A_8BiPlanar" = c.kCVPixelFormatType_422YpCbCr_4A_8BiPlanar, + /// Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct + @"420YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, + /// Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct + @"420YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + /// Bi-Planar Component Y'CbCr 8-bit 4:2:2, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct + @"422YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange, + /// Bi-Planar Component Y'CbCr 8-bit 4:2:2, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct + @"422YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarFullRange, + /// Bi-Planar Component Y'CbCr 8-bit 4:4:4, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct + @"444YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange, + /// Bi-Planar Component Y'CbCr 8-bit 4:4:4, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct + @"444YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarFullRange, + /// Component Y'CbCr 8-bit 4:2:2, ordered Y'0 Cb Y'1 Cr + @"422YpCbCr8_yuvs" = c.kCVPixelFormatType_422YpCbCr8_yuvs, + /// Component Y'CbCr 8-bit 4:2:2, full range, ordered Y'0 Cb Y'1 Cr + @"422YpCbCr8FullRange" = c.kCVPixelFormatType_422YpCbCr8FullRange, + /// 8 bit one component, black is zero + @"OneComponent8" = c.kCVPixelFormatType_OneComponent8, + /// 8 bit two component, black is zero + @"TwoComponent8" = c.kCVPixelFormatType_TwoComponent8, + /// little-endian RGB101010, 2 MSB are ignored, wide-gamut (384-895) + @"30RGBLEPackedWideGamut" = c.kCVPixelFormatType_30RGBLEPackedWideGamut, + /// little-endian ARGB2101010 full-range ARGB + @"ARGB2101010LEPacked" = c.kCVPixelFormatType_ARGB2101010LEPacked, + /// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha) + @"40ARGBLEWideGamut" = c.kCVPixelFormatType_40ARGBLEWideGamut, + /// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha). Alpha premultiplied + @"40ARGBLEWideGamutPremultiplied" = c.kCVPixelFormatType_40ARGBLEWideGamutPremultiplied, + /// 10 bit little-endian one component, stored as 10 MSBs of 16 bits, black is zero + @"OneComponent10" = c.kCVPixelFormatType_OneComponent10, + /// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero + @"OneComponent12" = c.kCVPixelFormatType_OneComponent12, + /// 16 bit little-endian one component, black is zero + @"OneComponent16" = c.kCVPixelFormatType_OneComponent16, + /// 16 bit little-endian two component, black is zero + @"TwoComponent16" = c.kCVPixelFormatType_TwoComponent16, + /// 16 bit one component IEEE half-precision float, 16-bit little-endian samples + @"OneComponent16Half" = c.kCVPixelFormatType_OneComponent16Half, + /// 32 bit one component IEEE float, 32-bit little-endian samples + @"OneComponent32Float" = c.kCVPixelFormatType_OneComponent32Float, + /// 16 bit two component IEEE half-precision float, 16-bit little-endian samples + @"TwoComponent16Half" = c.kCVPixelFormatType_TwoComponent16Half, + /// 32 bit two component IEEE float, 32-bit little-endian samples + @"TwoComponent32Float" = c.kCVPixelFormatType_TwoComponent32Float, + /// 64 bit RGBA IEEE half-precision float, 16-bit little-endian samples + @"64RGBAHalf" = c.kCVPixelFormatType_64RGBAHalf, + /// 128 bit RGBA IEEE float, 32-bit little-endian samples + @"128RGBAFloat" = c.kCVPixelFormatType_128RGBAFloat, + /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G R G R... alternating with B G B G... + @"14Bayer_GRBG" = c.kCVPixelFormatType_14Bayer_GRBG, + /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered R G R G... alternating with G B G B... + @"14Bayer_RGGB" = c.kCVPixelFormatType_14Bayer_RGGB, + /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered B G B G... alternating with G R G R... + @"14Bayer_BGGR" = c.kCVPixelFormatType_14Bayer_BGGR, + /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G B G B... alternating with R G R G... + @"14Bayer_GBRG" = c.kCVPixelFormatType_14Bayer_GBRG, + /// IEEE754-2008 binary16 (half float), describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) ) + @"DisparityFloat16" = c.kCVPixelFormatType_DisparityFloat16, + /// IEEE754-2008 binary32 float, describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) ) + @"DisparityFloat32" = c.kCVPixelFormatType_DisparityFloat32, + /// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters + @"DepthFloat16" = c.kCVPixelFormatType_DepthFloat16, + /// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters + @"DepthFloat32" = c.kCVPixelFormatType_DepthFloat32, + /// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960]) + @"420YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange, + /// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960]) + @"422YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange, + /// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960]) + @"444YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange, + /// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023) + @"420YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarFullRange, + /// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023) + @"422YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarFullRange, + /// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023) + @"444YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarFullRange, + /// first and second planes as per 420YpCbCr8BiPlanarVideoRange (420v), alpha 8 bits in third plane full-range. No CVPlanarPixelBufferInfo struct. + @"420YpCbCr8VideoRange_8A_TriPlanar" = c.kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar, + /// Single plane Bayer 16-bit little-endian sensor element ("sensel".*) samples from full-size decoding of ProRes RAW images; Bayer pattern (sensel ordering) and other raw conversion information is described via buffer attachments + @"16VersatileBayer" = c.kCVPixelFormatType_16VersatileBayer, + /// Single plane 64-bit RGBA (16-bit little-endian samples) from downscaled decoding of ProRes RAW images; components--which may not be co-sited with one another--are sensel values and require raw conversion, information for which is described via buffer attachments + @"64RGBA_DownscaledProResRAW" = c.kCVPixelFormatType_64RGBA_DownscaledProResRAW, + /// 2 plane YCbCr16 4:2:2, video-range (luma=[4096,60160] chroma=[4096,61440]) + @"422YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr16BiPlanarVideoRange, + /// 2 plane YCbCr16 4:4:4, video-range (luma=[4096,60160] chroma=[4096,61440]) + @"444YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr16BiPlanarVideoRange, + /// 3 plane video-range YCbCr16 4:4:4 with 16-bit full-range alpha (luma=[4096,60160] chroma=[4096,61440] alpha=[0,65535]). No CVPlanarPixelBufferInfo struct. + @"444YpCbCr16VideoRange_16A_TriPlanar" = c.kCVPixelFormatType_444YpCbCr16VideoRange_16A_TriPlanar, + _, +}; diff --git a/pkg/opengl/Buffer.zig b/pkg/opengl/Buffer.zig index 3e55410b7..609342958 100644 --- a/pkg/opengl/Buffer.zig +++ b/pkg/opengl/Buffer.zig @@ -51,7 +51,7 @@ pub const Binding = struct { data: anytype, usage: Usage, ) !void { - const info = dataInfo(&data); + const info = dataInfo(data); glad.context.BufferData.?( @intFromEnum(b.target), info.size, @@ -136,10 +136,6 @@ pub const Binding = struct { }; } - pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void { - glad.context.EnableVertexAttribArray.?(idx); - } - /// Shorthand for vertexAttribPointer that is specialized towards the /// common use case of specifying an array of homogeneous types that /// don't need normalization. This also enables the attribute at idx. @@ -230,6 +226,7 @@ pub const Target = enum(c_uint) { array = c.GL_ARRAY_BUFFER, element_array = c.GL_ELEMENT_ARRAY_BUFFER, uniform = c.GL_UNIFORM_BUFFER, + storage = c.GL_SHADER_STORAGE_BUFFER, _, }; diff --git a/pkg/opengl/Framebuffer.zig b/pkg/opengl/Framebuffer.zig index c5d659f98..ea1f0d2ba 100644 --- a/pkg/opengl/Framebuffer.zig +++ b/pkg/opengl/Framebuffer.zig @@ -5,6 +5,7 @@ const c = @import("c.zig").c; const errors = @import("errors.zig"); const glad = @import("glad.zig"); const Texture = @import("Texture.zig"); +const Renderbuffer = @import("Renderbuffer.zig"); id: c.GLuint, @@ -86,6 +87,29 @@ pub const Binding = struct { try errors.getError(); } + pub fn renderbuffer( + self: Binding, + attachment: Attachment, + buffer: Renderbuffer, + ) !void { + glad.context.FramebufferRenderbuffer.?( + @intFromEnum(self.target), + @intFromEnum(attachment), + c.GL_RENDERBUFFER, + buffer.id, + ); + try errors.getError(); + } + + pub fn drawBuffers( + self: Binding, + bufs: []Attachment, + ) !void { + _ = self; + glad.context.DrawBuffers.?(@intCast(bufs.len), bufs.ptr); + try errors.getError(); + } + pub fn checkStatus(self: Binding) Status { return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target))); } diff --git a/pkg/opengl/Renderbuffer.zig b/pkg/opengl/Renderbuffer.zig new file mode 100644 index 000000000..ef21287f7 --- /dev/null +++ b/pkg/opengl/Renderbuffer.zig @@ -0,0 +1,56 @@ +const Renderbuffer = @This(); + +const std = @import("std"); +const c = @import("c.zig").c; +const errors = @import("errors.zig"); +const glad = @import("glad.zig"); + +const Texture = @import("Texture.zig"); + +id: c.GLuint, + +/// Create a single buffer. +pub fn create() !Renderbuffer { + var rbo: c.GLuint = undefined; + glad.context.GenRenderbuffers.?(1, &rbo); + return .{ .id = rbo }; +} + +pub fn destroy(v: Renderbuffer) void { + glad.context.DeleteRenderbuffers.?(1, &v.id); +} + +pub fn bind(v: Renderbuffer) !Binding { + // Keep track of the previous binding so we can restore it in unbind. + var current: c.GLint = undefined; + glad.context.GetIntegerv.?(c.GL_RENDERBUFFER_BINDING, ¤t); + glad.context.BindRenderbuffer.?(c.GL_RENDERBUFFER, v.id); + return .{ .previous = @intCast(current) }; +} + +pub const Binding = struct { + previous: c.GLuint, + + pub fn unbind(self: Binding) void { + glad.context.BindRenderbuffer.?( + c.GL_RENDERBUFFER, + self.previous, + ); + } + + pub fn storage( + self: Binding, + format: Texture.InternalFormat, + width: c.GLsizei, + height: c.GLsizei, + ) !void { + _ = self; + glad.context.RenderbufferStorage.?( + c.GL_RENDERBUFFER, + @intCast(@intFromEnum(format)), + width, + height, + ); + try errors.getError(); + } +}; diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index fa5cf770b..833a9bb4d 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -7,8 +7,8 @@ const glad = @import("glad.zig"); id: c.GLuint, -pub fn active(target: c.GLenum) !void { - glad.context.ActiveTexture.?(target); +pub fn active(index: c_uint) !void { + glad.context.ActiveTexture.?(index + c.GL_TEXTURE0); try errors.getError(); } @@ -30,7 +30,7 @@ pub fn destroy(v: Texture) void { glad.context.DeleteTextures.?(1, &v.id); } -/// Enun for possible texture binding targets. +/// Enum for possible texture binding targets. pub const Target = enum(c_uint) { @"1D" = c.GL_TEXTURE_1D, @"2D" = c.GL_TEXTURE_2D, @@ -67,11 +67,11 @@ pub const Parameter = enum(c_uint) { /// Internal format enum for texture images. pub const InternalFormat = enum(c_int) { red = c.GL_RED, - rgb = c.GL_RGB, - rgba = c.GL_RGBA, + rgb = c.GL_RGB8, + rgba = c.GL_RGBA8, - srgb = c.GL_SRGB, - srgba = c.GL_SRGB_ALPHA, + srgb = c.GL_SRGB8, + srgba = c.GL_SRGB8_ALPHA8, // There are so many more that I haven't filled in. _, @@ -116,6 +116,7 @@ pub const Binding = struct { ), else => unreachable, } + try errors.getError(); } pub fn image2D( @@ -140,6 +141,7 @@ pub const Binding = struct { @intFromEnum(typ), data, ); + try errors.getError(); } pub fn subImage2D( @@ -164,6 +166,7 @@ pub const Binding = struct { @intFromEnum(typ), data, ); + try errors.getError(); } pub fn copySubImage2D( @@ -176,6 +179,16 @@ pub const Binding = struct { width: c.GLsizei, height: c.GLsizei, ) !void { - glad.context.CopyTexSubImage2D.?(@intFromEnum(b.target), level, xoffset, yoffset, x, y, width, height); + glad.context.CopyTexSubImage2D.?( + @intFromEnum(b.target), + level, + xoffset, + yoffset, + x, + y, + width, + height, + ); + try errors.getError(); } }; diff --git a/pkg/opengl/VertexArray.zig b/pkg/opengl/VertexArray.zig index 4a6a37576..44bf31621 100644 --- a/pkg/opengl/VertexArray.zig +++ b/pkg/opengl/VertexArray.zig @@ -29,4 +29,88 @@ pub const Binding = struct { _ = self; glad.context.BindVertexArray.?(0); } + + pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void { + glad.context.EnableVertexAttribArray.?(idx); + try errors.getError(); + } + + pub fn bindingDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void { + glad.context.VertexBindingDivisor.?(idx, divisor); + try errors.getError(); + } + + pub fn attributeBinding( + _: Binding, + attrib_idx: c.GLuint, + binding_idx: c.GLuint, + ) !void { + glad.context.VertexAttribBinding.?(attrib_idx, binding_idx); + try errors.getError(); + } + + pub fn attributeFormat( + _: Binding, + idx: c.GLuint, + size: c.GLint, + typ: c.GLenum, + normalized: bool, + offset: c.GLuint, + ) !void { + glad.context.VertexAttribFormat.?( + idx, + size, + typ, + @intCast(@intFromBool(normalized)), + offset, + ); + try errors.getError(); + } + + pub fn attributeIFormat( + _: Binding, + idx: c.GLuint, + size: c.GLint, + typ: c.GLenum, + offset: c.GLuint, + ) !void { + glad.context.VertexAttribIFormat.?( + idx, + size, + typ, + offset, + ); + try errors.getError(); + } + + pub fn attributeLFormat( + _: Binding, + idx: c.GLuint, + size: c.GLint, + offset: c.GLuint, + ) !void { + glad.context.VertexAttribLFormat.?( + idx, + size, + c.GL_DOUBLE, + offset, + ); + try errors.getError(); + } + + pub fn bindVertexBuffer( + _: Binding, + idx: c.GLuint, + buffer: c.GLuint, + offset: c.GLintptr, + stride: c.GLsizei, + ) !void { + glad.context.BindVertexBuffer.?( + idx, + buffer, + offset, + stride, + ); + try errors.getError(); + } }; diff --git a/pkg/opengl/draw.zig b/pkg/opengl/draw.zig index 866511c32..50110f605 100644 --- a/pkg/opengl/draw.zig +++ b/pkg/opengl/draw.zig @@ -1,6 +1,7 @@ const c = @import("c.zig").c; const errors = @import("errors.zig"); const glad = @import("glad.zig"); +const Primitive = @import("primitives.zig").Primitive; pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void { glad.context.ClearColor.?(r, g, b, a); @@ -15,6 +16,21 @@ pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void { try errors.getError(); } +pub fn drawArraysInstanced( + mode: Primitive, + first: c.GLint, + count: c.GLsizei, + primcount: c.GLsizei, +) !void { + glad.context.DrawArraysInstanced.?( + @intCast(@intFromEnum(mode)), + first, + count, + primcount, + ); + try errors.getError(); +} + pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void { const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset)); glad.context.DrawElements.?(mode, count, typ, offsetPtr); @@ -25,9 +41,15 @@ pub fn drawElementsInstanced( mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, - primcount: usize, + primcount: c.GLsizei, ) !void { - glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount)); + glad.context.DrawElementsInstanced.?( + mode, + count, + typ, + null, + primcount, + ); try errors.getError(); } @@ -36,6 +58,11 @@ pub fn enable(cap: c.GLenum) !void { try errors.getError(); } +pub fn disable(cap: c.GLenum) !void { + glad.context.Disable.?(cap); + try errors.getError(); +} + pub fn frontFace(mode: c.GLenum) !void { glad.context.FrontFace.?(mode); try errors.getError(); @@ -57,3 +84,11 @@ pub fn pixelStore(mode: c.GLenum, value: anytype) !void { } try errors.getError(); } + +pub fn finish() void { + glad.context.Finish.?(); +} + +pub fn flush() void { + glad.context.Flush.?(); +} diff --git a/pkg/opengl/main.zig b/pkg/opengl/main.zig index 19cd750d0..7165ad3ab 100644 --- a/pkg/opengl/main.zig +++ b/pkg/opengl/main.zig @@ -16,20 +16,29 @@ pub const glad = @import("glad.zig"); pub const ext = @import("extensions.zig"); pub const Buffer = @import("Buffer.zig"); pub const Framebuffer = @import("Framebuffer.zig"); +pub const Renderbuffer = @import("Renderbuffer.zig"); pub const Program = @import("Program.zig"); pub const Shader = @import("Shader.zig"); pub const Texture = @import("Texture.zig"); pub const VertexArray = @import("VertexArray.zig"); +pub const errors = @import("errors.zig"); + +pub const Primitive = @import("primitives.zig").Primitive; + const draw = @import("draw.zig"); pub const blendFunc = draw.blendFunc; pub const clear = draw.clear; pub const clearColor = draw.clearColor; pub const drawArrays = draw.drawArrays; +pub const drawArraysInstanced = draw.drawArraysInstanced; pub const drawElements = draw.drawElements; pub const drawElementsInstanced = draw.drawElementsInstanced; pub const enable = draw.enable; +pub const disable = draw.disable; pub const frontFace = draw.frontFace; pub const pixelStore = draw.pixelStore; pub const viewport = draw.viewport; +pub const flush = draw.flush; +pub const finish = draw.finish; diff --git a/pkg/opengl/primitives.zig b/pkg/opengl/primitives.zig new file mode 100644 index 000000000..e12f51a66 --- /dev/null +++ b/pkg/opengl/primitives.zig @@ -0,0 +1,18 @@ +pub const c = @import("c.zig").c; + +pub const Primitive = enum(c_int) { + point = c.GL_POINTS, + line = c.GL_LINES, + line_strip = c.GL_LINE_STRIP, + triangle = c.GL_TRIANGLES, + triangle_strip = c.GL_TRIANGLE_STRIP, + + // Commented out primitive types are excluded for parity with Metal. + // + // line_loop = c.GL_LINE_LOOP, + // line_adjacency = c.GL_LINES_ADJACENCY, + // line_strip_adjacency = c.GL_LINE_STRIP_ADJACENCY, + // triangle_fan = c.GL_TRIANGLE_FAN, + // triangle_adjacency = c.GL_TRIANGLES_ADJACENCY, + // triangle_strip_adjacency = c.GL_TRIANGLE_STRIP_ADJACENCY, +}; diff --git a/src/Surface.zig b/src/Surface.zig index 41d40125a..a25b200f7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -468,6 +468,7 @@ pub fn init( .size = size, .surface_mailbox = .{ .surface = self, .app = app_mailbox }, .rt_surface = rt_surface, + .thread = &self.renderer_thread, }); errdefer renderer_impl.deinit(); @@ -726,7 +727,9 @@ pub fn close(self: *Surface) void { /// is in the middle of animation (such as a resize, etc.) or when /// the render timer is managed manually by the apprt. pub fn draw(self: *Surface) !void { - try self.renderer_thread.draw_now.notify(); + // Renderers are required to support `drawFrame` being called from + // the main thread, so that they can update contents during resize. + try self.renderer.drawFrame(true); } /// Activate the inspector. This will begin collecting inspection data. diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 099a051a4..0299cc1ff 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -55,6 +55,11 @@ pub const c = @cImport({ const log = std.log.scoped(.gtk); +/// This is detected by the Renderer, in which case it sends a `redraw_surface` +/// message so that we can call `drawFrame` ourselves from the app thread, +/// because GTK's `GLArea` does not support drawing from a different thread. +pub const must_draw_from_app_thread = true; + pub const Options = struct {}; core_app: *CoreApp, diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 1e5b1bfe8..cf8d651dd 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -41,10 +41,6 @@ const adw_version = @import("adw_version.zig"); const log = std.log.scoped(.gtk_surface); -/// This is detected by the OpenGL renderer to move to a single-threaded -/// draw operation. This basically puts locks around our draw path. -pub const opengl_single_threaded_draw = true; - pub const Options = struct { /// The parent surface to inherit settings such as font size, working /// directory, etc. from. @@ -394,7 +390,10 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { // Various other GL properties gl_area_widget.setCursorFromName("text"); - gl_area.setRequiredVersion(3, 3); + gl_area.setRequiredVersion( + renderer.OpenGL.MIN_VERSION_MAJOR, + renderer.OpenGL.MIN_VERSION_MINOR, + ); gl_area.setHasStencilBuffer(0); gl_area.setHasDepthBuffer(0); gl_area.setUseEs(0); @@ -683,12 +682,13 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { fn realize(self: *Surface) !void { // If this surface has already been realized, then we don't need to - // reinitialize. This can happen if a surface is moved from one GDK surface - // to another (i.e. a tab is pulled out into a window). + // reinitialize. This can happen if a surface is moved from one GDK + // surface to another (i.e. a tab is pulled out into a window). if (self.realized) { // If we have no OpenGL state though, we do need to reinitialize. - // We allow the renderer to figure that out - try self.core_surface.renderer.displayRealize(); + // We allow the renderer to figure that out, and then queue a draw. + try self.core_surface.renderer.displayRealized(); + self.redraw(); return; } @@ -794,7 +794,7 @@ pub fn primaryWidget(self: *Surface) *gtk.Widget { } fn render(self: *Surface) !void { - try self.core_surface.renderer.drawFrame(self); + try self.core_surface.renderer.drawFrame(true); } /// Called by core surface to get the cgroup. diff --git a/src/config/Config.zig b/src/config/Config.zig index 2df66ba45..f7a197184 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -266,6 +266,9 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ /// This affects the appearance of text and of any images with transparency. /// Additionally, custom shaders will receive colors in the configured space. /// +/// On macOS the default is `native`, on all other platforms the default is +/// `linear-corrected`. +/// /// Valid values: /// /// * `native` - Perform alpha blending in the native color space for the OS. @@ -276,12 +279,15 @@ pub const renamed = std.StaticStringMap([]const u8).initComptime(&.{ /// when certain color combinations are used (e.g. red / green), but makes /// dark text look much thinner than normal and light text much thicker. /// This is also sometimes known as "gamma correction". -/// (Currently only supported on macOS. Has no effect on Linux.) /// /// * `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. -@"alpha-blending": AlphaBlending = .native, +@"alpha-blending": AlphaBlending = + if (builtin.os.tag == .macos) + .native + else + .@"linear-corrected", /// All of the configurations behavior adjust various metrics determined by the /// font. The values can be integers (1, -1, etc.) or a percentage (20%, -15%, diff --git a/src/renderer.zig b/src/renderer.zig index 61d9a4e53..e3ed070b6 100644 --- a/src/renderer.zig +++ b/src/renderer.zig @@ -16,6 +16,7 @@ const cursor = @import("renderer/cursor.zig"); const message = @import("renderer/message.zig"); const size = @import("renderer/size.zig"); pub const shadertoy = @import("renderer/shadertoy.zig"); +pub const GenericRenderer = @import("renderer/generic.zig").Renderer; pub const Metal = @import("renderer/Metal.zig"); pub const OpenGL = @import("renderer/OpenGL.zig"); pub const WebGL = @import("renderer/WebGL.zig"); @@ -56,8 +57,8 @@ pub const Impl = enum { /// The implementation to use for the renderer. This is comptime chosen /// so that every build has exactly one renderer implementation. pub const Renderer = switch (build_config.renderer) { - .metal => Metal, - .opengl => OpenGL, + .metal => GenericRenderer(Metal), + .opengl => GenericRenderer(OpenGL), .webgl => WebGL, }; diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 639ef354b..766cbefa5 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -1,547 +1,80 @@ -//! Renderer implementation for Metal. -//! -//! Open questions: -//! +//! Graphics API wrapper for Metal. pub const Metal = @This(); const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const glfw = @import("glfw"); const objc = @import("objc"); const macos = @import("macos"); -const imgui = @import("imgui"); -const glslang = @import("glslang"); -const xev = @import("../global.zig").xev; -const apprt = @import("../apprt.zig"); -const configpkg = @import("../config.zig"); -const font = @import("../font/main.zig"); -const os = @import("../os/main.zig"); -const terminal = @import("../terminal/main.zig"); -const renderer = @import("../renderer.zig"); -const math = @import("../math.zig"); -const Surface = @import("../Surface.zig"); -const link = @import("link.zig"); const graphics = macos.graphics; -const fgMode = @import("cell.zig").fgMode; -const isCovering = @import("cell.zig").isCovering; +const apprt = @import("../apprt.zig"); +const font = @import("../font/main.zig"); +const configpkg = @import("../config.zig"); +const rendererpkg = @import("../renderer.zig"); +const Renderer = rendererpkg.GenericRenderer(Metal); const shadertoy = @import("shadertoy.zig"); -const assert = std.debug.assert; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const CFReleaseThread = os.CFReleaseThread; -const Terminal = terminal.Terminal; -const Health = renderer.Health; const mtl = @import("metal/api.zig"); -const mtl_buffer = @import("metal/buffer.zig"); -const mtl_cell = @import("metal/cell.zig"); -const mtl_image = @import("metal/image.zig"); -const mtl_sampler = @import("metal/sampler.zig"); -const mtl_shaders = @import("metal/shaders.zig"); -const Image = mtl_image.Image; -const ImageMap = mtl_image.ImageMap; -const Shaders = mtl_shaders.Shaders; +const IOSurfaceLayer = @import("metal/IOSurfaceLayer.zig"); -const ImageBuffer = mtl_buffer.Buffer(mtl_shaders.Image); -const InstanceBuffer = mtl_buffer.Buffer(u16); +pub const GraphicsAPI = Metal; +pub const Target = @import("metal/Target.zig"); +pub const Frame = @import("metal/Frame.zig"); +pub const RenderPass = @import("metal/RenderPass.zig"); +pub const Pipeline = @import("metal/Pipeline.zig"); +const bufferpkg = @import("metal/buffer.zig"); +pub const Buffer = bufferpkg.Buffer; +pub const Texture = @import("metal/Texture.zig"); +pub const shaders = @import("metal/shaders.zig"); -const ImagePlacementList = std.ArrayListUnmanaged(mtl_image.Placement); +pub const cellpkg = @import("metal/cell.zig"); +pub const imagepkg = @import("metal/image.zig"); -const DisplayLink = switch (builtin.os.tag) { - .macos => *macos.video.DisplayLink, - else => void, -}; +pub const custom_shader_target: shadertoy.Target = .msl; + +const log = std.log.scoped(.metal); // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ .cocoa = builtin.os.tag == .macos, }); -const log = std.log.scoped(.metal); +layer: IOSurfaceLayer, -/// Allocator that can be used -alloc: std.mem.Allocator, +/// MTLDevice +device: objc.Object, +/// MTLCommandQueue +queue: objc.Object, -/// The configuration we need derived from the main config. -config: DerivedConfig, +/// Alpha blending mode +blending: configpkg.Config.AlphaBlending, -/// The mailbox for communicating with the window. -surface_mailbox: apprt.surface.Mailbox, - -/// Current font metrics defining our grid. -grid_metrics: font.Metrics, - -/// The size of everything. -size: renderer.Size, - -/// True if the window is focused -focused: bool, - -/// The foreground color set by an OSC 10 sequence. If unset then -/// default_foreground_color is used. -foreground_color: ?terminal.color.RGB, - -/// Foreground color set in the user's config file. -default_foreground_color: terminal.color.RGB, - -/// The background color set by an OSC 11 sequence. If unset then -/// default_background_color is used. -background_color: ?terminal.color.RGB, - -/// Background color set in the user's config file. -default_background_color: terminal.color.RGB, - -/// The cursor color set by an OSC 12 sequence. If unset then -/// default_cursor_color is used. -cursor_color: ?terminal.color.RGB, - -/// Default cursor color when no color is set explicitly by an OSC 12 command. -/// This is cursor color as set in the user's config, if any. If no cursor color -/// is set in the user's config, then the cursor color is determined by the -/// current foreground color. -default_cursor_color: ?terminal.color.RGB, - -/// When `cursor_color` is null, swap the foreground and background colors of -/// the cell under the cursor for the cursor color. Otherwise, use the default -/// foreground color as the cursor color. -cursor_invert: bool, - -/// The current set of cells to render. This is rebuilt on every frame -/// but we keep this around so that we don't reallocate. Each set of -/// cells goes into a separate shader. -cells: mtl_cell.Contents, - -/// The last viewport that we based our rebuild off of. If this changes, -/// then we do a full rebuild of the cells. The pointer values in this pin -/// are NOT SAFE to read because they may be modified, freed, etc from the -/// termio thread. We treat the pointers as integers for comparison only. -cells_viewport: ?terminal.Pin = null, - -/// Set to true after rebuildCells is called. This can be used -/// to determine if any possible changes have been made to the -/// cells for the draw call. -cells_rebuilt: bool = false, - -/// The current GPU uniform values. -uniforms: mtl_shaders.Uniforms, - -/// The font structures. -font_grid: *font.SharedGrid, -font_shaper: font.Shaper, -font_shaper_cache: font.ShaperCache, - -/// The images that we may render. -images: ImageMap = .{}, -image_placements: ImagePlacementList = .{}, -image_bg_end: u32 = 0, -image_text_end: u32 = 0, -image_virtual: bool = false, - -/// Metal state -shaders: Shaders, // Compiled shaders - -/// Metal objects -layer: objc.Object, // CAMetalLayer - -/// The CVDisplayLink used to drive the rendering loop in sync -/// with the display. This is void on platforms that don't support -/// a display link. -display_link: ?DisplayLink = null, - -/// The `CGColorSpace` that represents our current terminal color space -terminal_colorspace: *graphics.ColorSpace, - -/// Custom shader state. This is only set if we have custom shaders. -custom_shader_state: ?CustomShaderState = null, - -/// Health of the last frame. Note that when we do double/triple buffering -/// this will have to be part of the frame state. -health: std.atomic.Value(Health) = .{ .raw = .healthy }, - -/// Our GPU state -gpu_state: GPUState, - -/// State we need for the GPU that is shared between all frames. -pub const GPUState = struct { - // The count of buffers we use for double/triple buffering. If - // this is one then we don't do any double+ buffering at all. This - // is comptime because there isn't a good reason to change this at - // runtime and there is a lot of complexity to support it. For comptime, - // this is useful for debugging. - const BufferCount = 3; - - /// The frame data, the current frame index, and the semaphore protecting - /// the frame data. This is used to implement double/triple/etc. buffering. - frames: [BufferCount]FrameState, - frame_index: std.math.IntFittingRange(0, BufferCount) = 0, - frame_sema: std.Thread.Semaphore = .{ .permits = BufferCount }, - - device: objc.Object, // MTLDevice - queue: objc.Object, // MTLCommandQueue - - /// This buffer is written exactly once so we can use it globally. - instance: InstanceBuffer, // MTLBuffer - - /// The default storage mode to use for resources created with our device. - /// - /// This is based on whether the device is a discrete GPU or not, since - /// discrete GPUs do not have unified memory and therefore do not support - /// the "shared" storage mode, instead we have to use the "managed" mode. - default_storage_mode: mtl.MTLResourceOptions.StorageMode, - - pub fn init() !GPUState { - const device = try chooseDevice(); - const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); - errdefer queue.release(); - - // We determine whether our device is a discrete GPU based on these: - // - We're on macOS (iOS, iPadOS, etc. are guaranteed to be integrated). - // - We're not on aarch64 (Apple Silicon, therefore integrated). - // - The device reports that it does not have unified memory. - const is_discrete = - builtin.target.os.tag == .macos and - builtin.target.cpu.arch != .aarch64 and - !device.getProperty(bool, "hasUnifiedMemory"); - - const default_storage_mode: mtl.MTLResourceOptions.StorageMode = - if (is_discrete) .managed else .shared; - - var instance = try InstanceBuffer.initFill(device, &.{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .{ .storage_mode = default_storage_mode }); - errdefer instance.deinit(); - - var result: GPUState = .{ - .device = device, - .queue = queue, - .instance = instance, - .frames = undefined, - .default_storage_mode = default_storage_mode, - }; - - // Initialize all of our frame state. - for (&result.frames) |*frame| { - frame.* = try FrameState.init(result.device, default_storage_mode); - } - - return result; - } - - fn chooseDevice() error{NoMetalDevice}!objc.Object { - var chosen_device: ?objc.Object = null; - - switch (comptime builtin.os.tag) { - .macos => { - const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); - defer devices.release(); - - var iter = devices.iterate(); - while (iter.next()) |device| { - // We want a GPU that’s connected to a display. - if (device.getProperty(bool, "isHeadless")) continue; - chosen_device = device; - // If the user has an eGPU plugged in, they probably want - // to use it. Otherwise, integrated GPUs are better for - // battery life and thermals. - if (device.getProperty(bool, "isRemovable") or - device.getProperty(bool, "isLowPower")) break; - } - }, - .ios => { - chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); - }, - else => @compileError("unsupported target for Metal"), - } - - const device = chosen_device orelse return error.NoMetalDevice; - return device.retain(); - } - - pub fn deinit(self: *GPUState) void { - // Wait for all of our inflight draws to complete so that - // we can cleanly deinit our GPU state. - for (0..BufferCount) |_| self.frame_sema.wait(); - for (&self.frames) |*frame| frame.deinit(); - self.instance.deinit(); - self.queue.release(); - self.device.release(); - } - - /// Get the next frame state to draw to. This will wait on the - /// semaphore to ensure that the frame is available. This must - /// always be paired with a call to releaseFrame. - pub fn nextFrame(self: *GPUState) *FrameState { - self.frame_sema.wait(); - errdefer self.frame_sema.post(); - self.frame_index = (self.frame_index + 1) % BufferCount; - return &self.frames[self.frame_index]; - } - - /// This should be called when the frame has completed drawing. - pub fn releaseFrame(self: *GPUState) void { - self.frame_sema.post(); - } -}; - -/// State we need duplicated for every frame. Any state that could be -/// in a data race between the GPU and CPU while a frame is being -/// drawn should be in this struct. +/// The default storage mode to use for resources created with our device. /// -/// While a draw is in-process, we "lock" the state (via a semaphore) -/// and prevent the CPU from updating the state until Metal reports -/// that the frame is complete. -/// -/// This is used to implement double/triple buffering. -pub const FrameState = struct { - uniforms: UniformBuffer, - cells: CellTextBuffer, - cells_bg: CellBgBuffer, +/// This is based on whether the device is a discrete GPU or not, since +/// discrete GPUs do not have unified memory and therefore do not support +/// the "shared" storage mode, instead we have to use the "managed" mode. +default_storage_mode: mtl.MTLResourceOptions.StorageMode, - grayscale: objc.Object, // MTLTexture - grayscale_modified: usize = 0, - color: objc.Object, // MTLTexture - color_modified: usize = 0, - - /// A buffer containing the uniform data. - const UniformBuffer = mtl_buffer.Buffer(mtl_shaders.Uniforms); - const CellBgBuffer = mtl_buffer.Buffer(mtl_shaders.CellBg); - const CellTextBuffer = mtl_buffer.Buffer(mtl_shaders.CellText); - - pub fn init( - device: objc.Object, - /// Storage mode for buffers and textures. - storage_mode: mtl.MTLResourceOptions.StorageMode, - ) !FrameState { - // Uniform buffer contains exactly 1 uniform struct. The - // uniform data will be undefined so this must be set before - // a frame is drawn. - var uniforms = try UniformBuffer.init( - device, - 1, - .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - errdefer uniforms.deinit(); - - // Create the buffers for our vertex data. The preallocation size - // is likely too small but our first frame update will resize it. - var cells = try CellTextBuffer.init( - device, - 10 * 10, - .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - errdefer cells.deinit(); - var cells_bg = try CellBgBuffer.init( - device, - 10 * 10, - .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - - errdefer cells_bg.deinit(); - - // Initialize our textures for our font atlas. - const grayscale = try initAtlasTexture(device, &.{ - .data = undefined, - .size = 8, - .format = .grayscale, - }, storage_mode); - errdefer grayscale.release(); - const color = try initAtlasTexture(device, &.{ - .data = undefined, - .size = 8, - .format = .rgba, - }, storage_mode); - errdefer color.release(); - - return .{ - .uniforms = uniforms, - .cells = cells, - .cells_bg = cells_bg, - .grayscale = grayscale, - .color = color, - }; - } - - pub fn deinit(self: *FrameState) void { - self.uniforms.deinit(); - self.cells.deinit(); - self.cells_bg.deinit(); - self.grayscale.release(); - self.color.release(); - } -}; - -pub const CustomShaderState = struct { - /// When we have a custom shader state, we maintain a front - /// and back texture which we use as a swap chain to render - /// between when multiple custom shaders are defined. - front_texture: objc.Object, // MTLTexture - back_texture: objc.Object, // MTLTexture - - sampler: mtl_sampler.Sampler, - uniforms: mtl_shaders.PostUniforms, - - /// The first time a frame was drawn. - /// This is used to update the time uniform. - first_frame_time: std.time.Instant, - - /// The last time a frame was drawn. - /// This is used to update the time uniform. - last_frame_time: std.time.Instant, - - /// Swap the front and back textures. - pub fn swap(self: *CustomShaderState) void { - std.mem.swap(objc.Object, &self.front_texture, &self.back_texture); - } - - pub fn deinit(self: *CustomShaderState) void { - self.front_texture.release(); - self.back_texture.release(); - self.sampler.deinit(); - } -}; - -/// The configuration for this renderer that is derived from the main -/// configuration. This must be exported so that we don't need to -/// pass around Config pointers which makes memory management a pain. -pub const DerivedConfig = struct { - arena: ArenaAllocator, - - font_thicken: bool, - font_thicken_strength: u8, - font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.CodepointResolver.StyleStatus, - cursor_color: ?terminal.color.RGB, - cursor_invert: bool, - cursor_opacity: f64, - cursor_text: ?terminal.color.RGB, - background: terminal.color.RGB, - background_opacity: f64, - foreground: terminal.color.RGB, - selection_background: ?terminal.color.RGB, - selection_foreground: ?terminal.color.RGB, - invert_selection_fg_bg: bool, - bold_is_bright: bool, - min_contrast: f32, - padding_color: configpkg.WindowPaddingColor, - custom_shaders: configpkg.RepeatablePath, - links: link.Set, - vsync: bool, - colorspace: configpkg.Config.WindowColorspace, - blending: configpkg.Config.AlphaBlending, - - pub fn init( - alloc_gpa: Allocator, - config: *const configpkg.Config, - ) !DerivedConfig { - var arena = ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - // Copy our shaders - const custom_shaders = try config.@"custom-shader".clone(alloc); - - // Copy our font features - const font_features = try config.@"font-feature".clone(alloc); - - // Get our font styles - var font_styles = font.CodepointResolver.StyleStatus.initFill(true); - font_styles.set(.bold, config.@"font-style-bold" != .false); - font_styles.set(.italic, config.@"font-style-italic" != .false); - font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Our link configs - const links = try link.Set.fromConfig( - alloc, - config.link.links.items, - ); - - const cursor_invert = config.@"cursor-invert-fg-bg"; - - return .{ - .background_opacity = @max(0, @min(1, config.@"background-opacity")), - .font_thicken = config.@"font-thicken", - .font_thicken_strength = config.@"font-thicken-strength", - .font_features = font_features.list, - .font_styles = font_styles, - - .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) - config.@"cursor-color".?.toTerminalRGB() - else - null, - - .cursor_invert = cursor_invert, - - .cursor_text = if (config.@"cursor-text") |txt| - txt.toTerminalRGB() - else - null, - - .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), - - .background = config.background.toTerminalRGB(), - .foreground = config.foreground.toTerminalRGB(), - .invert_selection_fg_bg = config.@"selection-invert-fg-bg", - .bold_is_bright = config.@"bold-is-bright", - .min_contrast = @floatCast(config.@"minimum-contrast"), - .padding_color = config.@"window-padding-color", - - .selection_background = if (config.@"selection-background") |bg| - bg.toTerminalRGB() - else - null, - - .selection_foreground = if (config.@"selection-foreground") |bg| - bg.toTerminalRGB() - else - null, - - .custom_shaders = custom_shaders, - .links = links, - .vsync = config.@"window-vsync", - .colorspace = config.@"window-colorspace", - .blending = config.@"alpha-blending", - .arena = arena, - }; - } - - pub fn deinit(self: *DerivedConfig) void { - const alloc = self.arena.allocator(); - self.links.deinit(alloc); - self.arena.deinit(); - } -}; - -/// Returns the hints that we want for this -pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { - return .{ - .client_api = .no_api, - .transparent_framebuffer = config.@"background-opacity" < 1, +pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { + comptime switch (builtin.os.tag) { + .macos, .ios => {}, + else => @compileError("unsupported platform for Metal"), }; -} -/// This is called early right after window creation to setup our -/// window surface as necessary. -pub fn surfaceInit(surface: *apprt.Surface) !void { - _ = surface; + _ = alloc; - // We don't do anything else here because we want to set everything - // else up during actual initialization. -} + // Choose our MTLDevice and create a MTLCommandQueue for that device. + const device = try chooseDevice(); + errdefer device.release(); + const queue = device.msgSend(objc.Object, objc.sel("newCommandQueue"), .{}); + errdefer queue.release(); + + const default_storage_mode: mtl.MTLResourceOptions.StorageMode = + if (device.getProperty(bool, "hasUnifiedMemory")) .shared else .managed; -pub fn init(alloc: Allocator, options: renderer.Options) !Metal { const ViewInfo = struct { view: objc.Object, scaleFactor: f64, @@ -553,7 +86,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { // Everything in glfw is window-oriented so we grab the backing // window, then derive everything from that. const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow( - options.rt_surface.window, + opts.rt_surface.window, ).?); const contentView = objc.Object.fromId( @@ -571,8 +104,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { }, apprt.embedded => .{ - .scaleFactor = @floatCast(options.rt_surface.content_scale.x), - .view = switch (options.rt_surface.platform) { + .scaleFactor = @floatCast(opts.rt_surface.content_scale.x), + .view = switch (opts.rt_surface.platform) { .macos => |v| v.nsview, .ios => |v| v.uiview, }, @@ -581,2772 +114,236 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { else => @compileError("unsupported apprt for metal"), }; - // Initialize our metal stuff - var gpu_state = try GPUState.init(); - errdefer gpu_state.deinit(); + // Create an IOSurfaceLayer which we can assign to the view to make + // it in to a "layer-hosting view", so that we can manually control + // the layer contents. + var layer = try IOSurfaceLayer.init(); + errdefer layer.release(); - // Get our CAMetalLayer - const layer: objc.Object = switch (builtin.os.tag) { - .macos => layer: { - const CAMetalLayer = objc.getClass("CAMetalLayer").?; - break :layer CAMetalLayer.msgSend(objc.Object, objc.sel("layer"), .{}); + // Add our layer to the view. + // + // On macOS we do this by making the view "layer-hosting" + // by assigning it to the view's `layer` property BEFORE + // setting `wantsLayer` to `true`. + // + // On iOS, views are always layer-backed, and `layer` + // is readonly, so instead we add it as a sublayer. + switch (comptime builtin.os.tag) { + .macos => { + info.view.setProperty("layer", layer.layer.value); + info.view.setProperty("wantsLayer", true); }, - // iOS is always layer-backed so we don't need to do anything here. - .ios => info.view.getProperty(objc.Object, "layer"), + .ios => { + info.view.msgSend(void, objc.sel("addSublayer"), .{layer.layer.value}); + }, else => @compileError("unsupported target for Metal"), - }; - layer.setProperty("device", gpu_state.device.value); - layer.setProperty("opaque", options.config.background_opacity >= 1); - layer.setProperty("displaySyncEnabled", options.config.vsync); - - // Set our layer's pixel format appropriately. - layer.setProperty( - "pixelFormat", - // Using an `*_srgb` pixel format makes Metal gamma encode - // the pixels written to it *after* blending, which means - // we get linear alpha blending rather than gamma-incorrect - // blending. - if (options.config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); - - // Set our layer's color space to Display P3. - // This allows us to have "Apple-style" alpha blending, - // since it seems to be the case that Apple apps like - // Terminal and TextEdit render text in the display's - // color space using converted colors, which reduces, - // but does not fully eliminate blending artifacts. - const colorspace = try graphics.ColorSpace.createNamed(.displayP3); - defer colorspace.release(); - layer.setProperty("colorspace", colorspace); - - // Create a colorspace the represents our terminal colors - // this will allow us to create e.g. `CGColor`s for things - // like the current background color. - const terminal_colorspace = try graphics.ColorSpace.createNamed( - switch (options.config.colorspace) { - .@"display-p3" => .displayP3, - .srgb => .sRGB, - }, - ); - errdefer terminal_colorspace.release(); - - // Make our view layer-backed with our Metal layer. On iOS views are - // always layer backed so we don't need to do this. But on iOS the - // caller MUST be sure to set the layerClass to CAMetalLayer. - if (comptime builtin.os.tag == .macos) { - info.view.setProperty("layer", layer.value); - info.view.setProperty("wantsLayer", true); - - // The layer gravity is set to top-left so that when we resize - // the view, the contents aren't stretched before a redraw. - layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft); } - // Ensure that our metal layer has a content scale set to match the - // scale factor of the window. This avoids magnification issues leading - // to blurry rendering. - layer.setProperty("contentsScale", info.scaleFactor); + // Ensure that if our layer is oversized it + // does not overflow the bounds of the view. + info.view.setProperty("clipsToBounds", true); - // Create the font shaper. We initially create a shaper that can support - // a width of 160 which is a common width for modern screens to help - // avoid allocations later. - var font_shaper = try font.Shaper.init(alloc, .{ - .features = options.config.font_features.items, - }); - errdefer font_shaper.deinit(); + // Ensure that our layer has a content scale set to + // match the scale factor of the window. This avoids + // magnification issues leading to blurry rendering. + layer.layer.setProperty("contentsScale", info.scaleFactor); - // Initialize all the data that requires a critical font section. - const font_critical: struct { - metrics: font.Metrics, - } = font_critical: { - const grid = options.font_grid; - grid.lock.lockShared(); - defer grid.lock.unlockShared(); - break :font_critical .{ - .metrics = grid.metrics, - }; - }; + // This makes it so that our display callback will actually be called. + layer.layer.setProperty("needsDisplayOnBoundsChange", true); - const display_link: ?DisplayLink = switch (builtin.os.tag) { - .macos => if (options.config.vsync) - try macos.video.DisplayLink.createWithActiveCGDisplays() - else - null, - else => null, - }; - errdefer if (display_link) |v| v.release(); - - var result: Metal = .{ - .alloc = alloc, - .config = options.config, - .surface_mailbox = options.surface_mailbox, - .grid_metrics = font_critical.metrics, - .size = options.size, - .focused = true, - .foreground_color = null, - .default_foreground_color = options.config.foreground, - .background_color = null, - .default_background_color = options.config.background, - .cursor_color = null, - .default_cursor_color = options.config.cursor_color, - .cursor_invert = options.config.cursor_invert, - - // Render state - .cells = .{}, - .uniforms = .{ - .projection_matrix = undefined, - .cell_size = undefined, - .grid_size = undefined, - .grid_padding = undefined, - .padding_extend = .{}, - .min_contrast = options.config.min_contrast, - .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, - .cursor_color = undefined, - .bg_color = .{ - options.config.background.r, - options.config.background.g, - options.config.background.b, - @intFromFloat(@round(options.config.background_opacity * 255.0)), - }, - .cursor_wide = false, - .use_display_p3 = options.config.colorspace == .@"display-p3", - .use_linear_blending = options.config.blending.isLinear(), - .use_linear_correction = options.config.blending == .@"linear-corrected", - }, - - // Fonts - .font_grid = options.font_grid, - .font_shaper = font_shaper, - .font_shaper_cache = font.ShaperCache.init(), - - // Shaders (initialized below) - .shaders = undefined, - - // Metal stuff + return .{ .layer = layer, - .display_link = display_link, - .terminal_colorspace = terminal_colorspace, - .custom_shader_state = null, - .gpu_state = gpu_state, + .device = device, + .queue = queue, + .blending = opts.config.blending, + .default_storage_mode = default_storage_mode, }; - - try result.initShaders(); - - // Do an initialize screen size setup to ensure our undefined values - // above are initialized. - try result.setScreenSize(result.size); - - return result; } pub fn deinit(self: *Metal) void { - self.gpu_state.deinit(); + self.queue.release(); + self.device.release(); - if (DisplayLink != void) { - if (self.display_link) |display_link| { - display_link.stop() catch {}; - display_link.release(); - } - } - - self.terminal_colorspace.release(); - - self.cells.deinit(self.alloc); - - self.font_shaper.deinit(); - self.font_shaper_cache.deinit(self.alloc); - - self.config.deinit(); - - { - var it = self.images.iterator(); - while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); - self.images.deinit(self.alloc); - } - self.image_placements.deinit(self.alloc); - - self.deinitShaders(); - - self.* = undefined; + // NOTE: We don't release the layer here because that should be taken + // care of automatically when the hosting view is destroyed. } -fn deinitShaders(self: *Metal) void { - if (self.custom_shader_state) |*state| state.deinit(); - - self.shaders.deinit(self.alloc); +pub fn loopEnter(self: *Metal) void { + const renderer: *align(1) Renderer = @fieldParentPtr("api", self); + self.layer.setDisplayCallback( + @ptrCast(&displayCallback), + @ptrCast(renderer), + ); } -fn initShaders(self: *Metal) !void { - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Load our custom shaders - const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - self.config.custom_shaders, - .msl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; +fn displayCallback(renderer: *Renderer) align(8) void { + renderer.drawFrame(true) catch |err| { + log.warn("Error drawing frame in display callback, err={}", .{err}); }; +} - var custom_shader_state: ?CustomShaderState = state: { - if (custom_shaders.len == 0) break :state null; - - // Build our sampler for our texture - var sampler = try mtl_sampler.Sampler.init(self.gpu_state.device); - errdefer sampler.deinit(); - - break :state .{ - // Resolution and screen textures will be fixed up by first - // call to setScreenSize. Draw calls will bail out early if - // the screen size hasn't been set yet, so it won't error. - .front_texture = undefined, - .back_texture = undefined, - .sampler = sampler, - .uniforms = .{ - .resolution = .{ 0, 0, 1 }, - .time = 1, - .time_delta = 1, - .frame_rate = 1, - .frame = 1, - .channel_time = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - .channel_resolution = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - .mouse = .{ 0, 0, 0, 0 }, - .date = .{ 0, 0, 0, 0 }, - .sample_rate = 1, - }, - - .first_frame_time = try std.time.Instant.now(), - .last_frame_time = try std.time.Instant.now(), - }; - }; - errdefer if (custom_shader_state) |*state| state.deinit(); - - var shaders = try Shaders.init( - self.alloc, - self.gpu_state.device, +pub fn initShaders( + self: *const Metal, + alloc: Allocator, + custom_shaders: []const [:0]const u8, +) !shaders.Shaders { + return try shaders.Shaders.init( + alloc, + self.device, custom_shaders, // Using an `*_srgb` pixel format makes Metal gamma encode // the pixels written to it *after* blending, which means // we get linear alpha blending rather than gamma-incorrect // blending. - if (self.config.blending.isLinear()) + if (self.blending.isLinear()) mtl.MTLPixelFormat.bgra8unorm_srgb else mtl.MTLPixelFormat.bgra8unorm, ); - errdefer shaders.deinit(self.alloc); - - self.shaders = shaders; - self.custom_shader_state = custom_shader_state; } -/// This is called just prior to spinning up the renderer thread for -/// final main thread setup requirements. -pub fn finalizeSurfaceInit(self: *Metal, surface: *apprt.Surface) !void { - _ = self; - _ = surface; - - // Metal doesn't have to do anything here. OpenGL has to do things - // like release the context but Metal doesn't have anything like that. -} - -/// Callback called by renderer.Thread when it begins. -pub fn threadEnter(self: *const Metal, surface: *apprt.Surface) !void { - _ = self; - _ = surface; - - // Metal requires no per-thread state. -} - -/// Callback called by renderer.Thread when it exits. -pub fn threadExit(self: *const Metal) void { - _ = self; - - // Metal requires no per-thread state. -} - -/// Called by renderer.Thread when it starts the main loop. -pub fn loopEnter(self: *Metal, thr: *renderer.Thread) !void { - // If we don't support a display link we have no work to do. - if (comptime DisplayLink == void) return; - - // This is when we know our "self" pointer is stable so we can - // setup the display link. To setup the display link we set our - // callback and we can start it immediately. - const display_link = self.display_link orelse return; - try display_link.setOutputCallback( - xev.Async, - &displayLinkCallback, - &thr.draw_now, - ); - display_link.start() catch {}; -} - -/// Called by renderer.Thread when it exits the main loop. -pub fn loopExit(self: *Metal) void { - // If we don't support a display link we have no work to do. - if (comptime DisplayLink == void) return; - - // Stop our display link. If this fails its okay it just means - // that we either never started it or the view its attached to - // is gone which is fine. - const display_link = self.display_link orelse return; - display_link.stop() catch {}; -} - -fn displayLinkCallback( - _: *macos.video.DisplayLink, - ud: ?*xev.Async, -) void { - const draw_now = ud orelse return; - draw_now.notify() catch |err| { - log.err("error notifying draw_now err={}", .{err}); +/// Get the current size of the runtime surface. +pub fn surfaceSize(self: *const Metal) !struct { width: u32, height: u32 } { + const bounds = self.layer.layer.getProperty(graphics.Rect, "bounds"); + const scale = self.layer.layer.getProperty(f64, "contentsScale"); + return .{ + .width = @intFromFloat(bounds.size.width * scale), + .height = @intFromFloat(bounds.size.height * scale), }; } -/// Mark the full screen as dirty so that we redraw everything. -pub fn markDirty(self: *Metal) void { - // This is how we force a full rebuild with metal. - self.cells_viewport = null; -} - -/// Called when we get an updated display ID for our display link. -pub fn setMacOSDisplayID(self: *Metal, id: u32) !void { - if (comptime DisplayLink == void) return; - const display_link = self.display_link orelse return; - log.info("updating display link display id={}", .{id}); - display_link.setCurrentCGDisplay(id) catch |err| { - log.warn("error setting display link display id err={}", .{err}); - }; -} - -/// True if our renderer has animations so that a higher frequency -/// timer is used. -pub fn hasAnimations(self: *const Metal) bool { - return self.custom_shader_state != null; -} - -/// True if our renderer is using vsync. If true, the renderer or apprt -/// is responsible for triggering draw_now calls to the render thread. That -/// is the only way to trigger a drawFrame. -pub fn hasVsync(self: *const Metal) bool { - if (comptime DisplayLink == void) return false; - const display_link = self.display_link orelse return false; - return display_link.isRunning(); -} - -/// Callback when the focus changes for the terminal this is rendering. -/// -/// Must be called on the render thread. -pub fn setFocus(self: *Metal, focus: bool) !void { - self.focused = focus; - - // If we're not focused, then we want to stop the display link - // because it is a waste of resources and we can move to pure - // change-driven updates. - if (comptime DisplayLink != void) link: { - const display_link = self.display_link orelse break :link; - if (focus) { - display_link.start() catch {}; - } else { - display_link.stop() catch {}; - } - } -} - -/// Callback when the window is visible or occluded. -/// -/// Must be called on the render thread. -pub fn setVisible(self: *Metal, visible: bool) void { - // If we're not visible, then we want to stop the display link - // because it is a waste of resources and we can move to pure - // change-driven updates. - if (comptime DisplayLink != void) link: { - const display_link = self.display_link orelse break :link; - if (visible and self.focused) { - display_link.start() catch {}; - } else { - display_link.stop() catch {}; - } - } -} - -/// Set the new font grid. -/// -/// Must be called on the render thread. -pub fn setFontGrid(self: *Metal, grid: *font.SharedGrid) void { - // Update our grid - self.font_grid = grid; - - // Update all our textures so that they sync on the next frame. - // We can modify this without a lock because the GPU does not - // touch this data. - for (&self.gpu_state.frames) |*frame| { - frame.grayscale_modified = 0; - frame.color_modified = 0; - } - - // Get our metrics from the grid. This doesn't require a lock because - // the metrics are never recalculated. - const metrics = grid.metrics; - self.grid_metrics = metrics; - - // Reset our shaper cache. If our font changed (not just the size) then - // the data in the shaper cache may be invalid and cannot be used, so we - // always clear the cache just in case. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Run a screen size update since this handles a lot of our uniforms - // that are grid size dependent and changing the font grid can change - // the grid size. - // - // If the screen size isn't set, it will be eventually so that'll call - // the setScreenSize automatically. - self.setScreenSize(self.size) catch |err| { - // The setFontGrid function can't fail but resizing our cell - // buffer definitely can fail. If it does, our renderer is probably - // screwed but let's just log it and continue until we can figure - // out a better way to handle this. - log.err("error resizing cells buffer err={}", .{err}); - }; - - // Reset our viewport to force a rebuild, since `setScreenSize` only - // does this when the number of cells changes, which isn't guaranteed. - self.cells_viewport = null; -} - -/// Update the frame data. -pub fn updateFrame( - self: *Metal, - surface: *apprt.Surface, - state: *renderer.State, - cursor_blink_visible: bool, -) !void { - _ = surface; - - // Data we extract out of the critical area. - const Critical = struct { - bg: terminal.color.RGB, - screen: terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style: ?renderer.CursorStyle, - color_palette: terminal.color.Palette, - viewport_pin: terminal.Pin, - - /// If true, rebuild the full screen. - full_rebuild: bool, - }; - - // Update all our data as tightly as possible within the mutex. - var critical: Critical = critical: { - // const start = try std.time.Instant.now(); - // const start_micro = std.time.microTimestamp(); - // defer { - // const end = std.time.Instant.now() catch unreachable; - // // "[updateFrame critical time] \t" - // std.log.err("[updateFrame critical time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); - // } - - state.mutex.lock(); - defer state.mutex.unlock(); - - // If we're in a synchronized output state, we pause all rendering. - if (state.terminal.modes.get(.synchronized_output)) { - log.debug("synchronized output started, skipping render", .{}); - return; - } - - // Swap bg/fg if the terminal is reversed - const bg = self.background_color orelse self.default_background_color; - const fg = self.foreground_color orelse self.default_foreground_color; - defer { - if (self.background_color) |*c| { - c.* = bg; - } else { - self.default_background_color = bg; - } - - if (self.foreground_color) |*c| { - c.* = fg; - } else { - self.default_foreground_color = fg; - } - } - - if (state.terminal.modes.get(.reverse_colors)) { - if (self.background_color) |*c| { - c.* = fg; - } else { - self.default_background_color = fg; - } - - if (self.foreground_color) |*c| { - c.* = bg; - } else { - self.default_foreground_color = bg; - } - } - - // Get the viewport pin so that we can compare it to the current. - const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; - - // We used to share terminal state, but we've since learned through - // analysis that it is faster to copy the terminal state than to - // hold the lock while rebuilding GPU cells. - var screen_copy = try state.terminal.screen.clone( - self.alloc, - .{ .viewport = .{} }, - null, - ); - errdefer screen_copy.deinit(); - - // Whether to draw our cursor or not. - const cursor_style = if (state.terminal.flags.password_input) - .lock +/// Initialize a new render target which can be presented by this API. +pub fn initTarget(self: *const Metal, width: usize, height: usize) !Target { + return Target.init(.{ + .device = self.device, + // Using an `*_srgb` pixel format makes Metal gamma encode the pixels + // written to it *after* blending, which means we get linear alpha + // blending rather than gamma-incorrect blending. + .pixel_format = if (self.blending.isLinear()) + .bgra8unorm_srgb else - renderer.cursorStyle( - state, - self.focused, - cursor_blink_visible, - ); - - // Get our preedit state - const preedit: ?renderer.State.Preedit = preedit: { - if (cursor_style == null) break :preedit null; - const p = state.preedit orelse break :preedit null; - break :preedit try p.clone(self.alloc); - }; - errdefer if (preedit) |p| p.deinit(self.alloc); - - // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. - // We only do this if the Kitty image state is dirty meaning only if - // it changes. - // - // If we have any virtual references, we must also rebuild our - // kitty state on every frame because any cell change can move - // an image. - if (state.terminal.screen.kitty_images.dirty or - self.image_virtual) - { - try self.prepKittyGraphics(state.terminal); - } - - // If we have any terminal dirty flags set then we need to rebuild - // the entire screen. This can be optimized in the future. - const full_rebuild: bool = rebuild: { - { - const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.flags.dirty); - if (v > 0) break :rebuild true; - } - { - const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.screen.dirty); - if (v > 0) break :rebuild true; - } - - // If our viewport changed then we need to rebuild the entire - // screen because it means we scrolled. If we have no previous - // viewport then we must rebuild. - const prev_viewport = self.cells_viewport orelse break :rebuild true; - if (!prev_viewport.eql(viewport_pin)) break :rebuild true; - - break :rebuild false; - }; - - // Reset the dirty flags in the terminal and screen. We assume - // that our rebuild will be successful since so we optimize for - // success and reset while we hold the lock. This is much easier - // than coordinating row by row or as changes are persisted. - state.terminal.flags.dirty = .{}; - state.terminal.screen.dirty = .{}; - { - var it = state.terminal.screen.pages.pageIterator( - .right_down, - .{ .screen = .{} }, - null, - ); - while (it.next()) |chunk| { - var dirty_set = chunk.node.data.dirtyBitSet(); - dirty_set.unsetAll(); - } - } - - break :critical .{ - .bg = self.background_color orelse self.default_background_color, - .screen = screen_copy, - .screen_type = state.terminal.active_screen, - .mouse = state.mouse, - .preedit = preedit, - .cursor_style = cursor_style, - .color_palette = state.terminal.color_palette.colors, - .viewport_pin = viewport_pin, - .full_rebuild = full_rebuild, - }; - }; - defer { - critical.screen.deinit(); - if (critical.preedit) |p| p.deinit(self.alloc); - } - - // Build our GPU cells - try self.rebuildCells( - critical.full_rebuild, - &critical.screen, - critical.screen_type, - critical.mouse, - critical.preedit, - critical.cursor_style, - &critical.color_palette, - ); - - // Notify our shaper we're done for the frame. For some shapers like - // CoreText this triggers off-thread cleanup logic. - self.font_shaper.endFrame(); - - // Update our viewport pin - self.cells_viewport = critical.viewport_pin; - - // Update our background color - self.uniforms.bg_color = .{ - critical.bg.r, - critical.bg.g, - critical.bg.b, - @intFromFloat(@round(self.config.background_opacity * 255.0)), - }; - - // Update the background color on our layer - // - // TODO: Is this expensive? Should we be checking if our - // bg color has changed first before doing this work? - { - const color = graphics.c.CGColorCreate( - @ptrCast(self.terminal_colorspace), - &[4]f64{ - @as(f64, @floatFromInt(critical.bg.r)) / 255.0, - @as(f64, @floatFromInt(critical.bg.g)) / 255.0, - @as(f64, @floatFromInt(critical.bg.b)) / 255.0, - self.config.background_opacity, - }, - ); - defer graphics.c.CGColorRelease(color); - - // We use a CATransaction so that Core Animation knows that we - // updated the background color property. Otherwise it behaves - // weird, not updating the color until we resize. - const CATransaction = objc.getClass("CATransaction").?; - CATransaction.msgSend(void, "begin", .{}); - defer CATransaction.msgSend(void, "commit", .{}); - - self.layer.setProperty("backgroundColor", color); - } - - // Go through our images and see if we need to setup any textures. - { - var image_it = self.images.iterator(); - while (image_it.next()) |kv| { - switch (kv.value_ptr.image) { - .ready => {}, - - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, - .replace_gray, - .replace_gray_alpha, - .replace_rgb, - .replace_rgba, - => try kv.value_ptr.image.upload( - self.alloc, - self.gpu_state.device, - self.gpu_state.default_storage_mode, - ), - - .unload_pending, - .unload_replace, - .unload_ready, - => { - kv.value_ptr.image.deinit(self.alloc); - self.images.removeByPtr(kv.key_ptr); - }, - } - } - } -} - -/// Draw the frame to the screen. -pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void { - _ = surface; - - // If we have no cells rebuilt we can usually skip drawing since there - // is no changed data. However, if we have active animations we still - // need to draw so that we can update the time uniform and render the - // changes. - if (!self.cells_rebuilt and !self.hasAnimations()) return; - self.cells_rebuilt = false; - - // Wait for a frame to be available. - const frame = self.gpu_state.nextFrame(); - errdefer self.gpu_state.releaseFrame(); - // log.debug("drawing frame index={}", .{self.gpu_state.frame_index}); - - // Setup our frame data - try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms}); - try frame.cells_bg.sync(self.gpu_state.device, self.cells.bg_cells); - const fg_count = try frame.cells.syncFromArrayLists(self.gpu_state.device, self.cells.fg_rows.lists); - - // If we have custom shaders, update the animation time. - if (self.custom_shader_state) |*state| { - const now = std.time.Instant.now() catch state.first_frame_time; - const since_ns: f32 = @floatFromInt(now.since(state.first_frame_time)); - const delta_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); - state.uniforms.time = since_ns / std.time.ns_per_s; - state.uniforms.time_delta = delta_ns / std.time.ns_per_s; - state.last_frame_time = now; - } - - // @autoreleasepool {} - const pool = objc.AutoreleasePool.init(); - defer pool.deinit(); - - // Get our drawable (CAMetalDrawable) - const drawable = self.layer.msgSend(objc.Object, objc.sel("nextDrawable"), .{}); - - // Get our screen texture. If we don't have a dedicated screen texture - // then we just use the drawable texture. - const screen_texture = if (self.custom_shader_state) |state| - state.back_texture - else tex: { - const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); - break :tex objc.Object.fromId(texture); - }; - - // If our font atlas changed, sync the texture data - texture: { - const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); - if (modified <= frame.grayscale_modified) break :texture; - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); - try syncAtlasTexture( - self.gpu_state.device, - &self.font_grid.atlas_grayscale, - &frame.grayscale, - self.gpu_state.default_storage_mode, - ); - } - texture: { - const modified = self.font_grid.atlas_color.modified.load(.monotonic); - if (modified <= frame.color_modified) break :texture; - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); - try syncAtlasTexture( - self.gpu_state.device, - &self.font_grid.atlas_color, - &frame.color, - self.gpu_state.default_storage_mode, - ); - } - - // Command buffer (MTLCommandBuffer) - const buffer = self.gpu_state.queue.msgSend(objc.Object, objc.sel("commandBuffer"), .{}); - - { - // MTLRenderPassDescriptor - const desc = desc: { - const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; - const desc = MTLRenderPassDescriptor.msgSend( - objc.Object, - objc.sel("renderPassDescriptor"), - .{}, - ); - - // Set our color attachment to be our drawable surface. - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); - attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("texture", screen_texture.value); - attachment.setProperty("clearColor", mtl.MTLClearColor{ - .red = 0.0, - .green = 0.0, - .blue = 0.0, - .alpha = 0.0, - }); - } - - break :desc desc; - }; - - // MTLRenderCommandEncoder - const encoder = buffer.msgSend( - objc.Object, - objc.sel("renderCommandEncoderWithDescriptor:"), - .{desc.value}, - ); - defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - - // Draw background images first - try self.drawImagePlacements(encoder, frame, self.image_placements.items[0..self.image_bg_end]); - - // Then draw background cells - try self.drawCellBgs(encoder, frame); - - // Then draw images under text - try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_bg_end..self.image_text_end]); - - // Then draw fg cells - try self.drawCellFgs(encoder, frame, fg_count); - - // Then draw remaining images - try self.drawImagePlacements(encoder, frame, self.image_placements.items[self.image_text_end..]); - } - - // If we have custom shaders, then we render them. - if (self.custom_shader_state) |*state| { - // MTLRenderPassDescriptor - const desc = desc: { - const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; - const desc = MTLRenderPassDescriptor.msgSend( - objc.Object, - objc.sel("renderPassDescriptor"), - .{}, - ); - - break :desc desc; - }; - - // Prepare our color attachment (output). - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - attachment.setProperty("loadAction", @intFromEnum(mtl.MTLLoadAction.clear)); - attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store)); - attachment.setProperty("clearColor", mtl.MTLClearColor{ - .red = 0, - .green = 0, - .blue = 0, - .alpha = 1, - }); - - const post_len = self.shaders.post_pipelines.len; - - for (self.shaders.post_pipelines[0 .. post_len - 1]) |pipeline| { - // Set our color attachment to be our front texture. - attachment.setProperty("texture", state.front_texture.value); - - // MTLRenderCommandEncoder - const encoder = buffer.msgSend( - objc.Object, - objc.sel("renderCommandEncoderWithDescriptor:"), - .{desc.value}, - ); - defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - - // Draw shader - try self.drawPostShader(encoder, pipeline, state); - // Swap the front and back textures. - state.swap(); - } - - // Draw the final shader directly to the drawable. - { - // Set our color attachment to be our drawable. - // - // Texture is a property of CAMetalDrawable but if you run - // Ghostty in XCode in debug mode it returns a CaptureMTLDrawable - // which ironically doesn't implement CAMetalDrawable as a - // property so we just send a message. - const texture = drawable.msgSend(objc.c.id, objc.sel("texture"), .{}); - attachment.setProperty("texture", texture); - - // MTLRenderCommandEncoder - const encoder = buffer.msgSend( - objc.Object, - objc.sel("renderCommandEncoderWithDescriptor:"), - .{desc.value}, - ); - defer encoder.msgSend(void, objc.sel("endEncoding"), .{}); - - try self.drawPostShader( - encoder, - self.shaders.post_pipelines[post_len - 1], - state, - ); - } - } - - buffer.msgSend(void, objc.sel("presentDrawable:"), .{drawable.value}); - - // Create our block to register for completion updates. This is used - // so we can detect failures. The block is deallocated by the objC - // runtime on success. - const block = try CompletionBlock.init(.{ .self = self }, &bufferCompleted); - errdefer block.deinit(); - buffer.msgSend(void, objc.sel("addCompletedHandler:"), .{block.context}); - - buffer.msgSend(void, objc.sel("commit"), .{}); -} - -/// This is the block type used for the addCompletedHandler call.back. -const CompletionBlock = objc.Block(struct { self: *Metal }, .{ - objc.c.id, // MTLCommandBuffer -}, void); - -/// This is the callback called by the CompletionBlock invocation for -/// addCompletedHandler. -/// -/// Note: this is USUALLY called on a separate thread because the renderer -/// thread and the Apple event loop threads are usually different. Therefore, -/// we need to be mindful of thread safety here. -fn bufferCompleted( - block: *const CompletionBlock.Context, - buffer_id: objc.c.id, -) callconv(.c) void { - const self = block.self; - const buffer = objc.Object.fromId(buffer_id); - - // Get our command buffer status. If it is anything other than error - // then we don't care and just return right away. We're looking for - // errors so that we can log them. - const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status"); - const health: Health = switch (status) { - .@"error" => .unhealthy, - else => .healthy, - }; - - // If our health value hasn't changed, then we do nothing. We don't - // do a cmpxchg here because strict atomicity isn't important. - if (self.health.load(.seq_cst) != health) { - self.health.store(health, .seq_cst); - - // Our health value changed, so we notify the surface so that it - // can do something about it. - _ = self.surface_mailbox.push(.{ - .renderer_health = health, - }, .{ .forever = {} }); - } - - // Always release our semaphore - self.gpu_state.releaseFrame(); -} - -fn drawPostShader( - self: *Metal, - encoder: objc.Object, - pipeline: objc.Object, - state: *const CustomShaderState, -) !void { - _ = self; - - // Use our custom shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{pipeline.value}, - ); - - // Set our sampler - encoder.msgSend( - void, - objc.sel("setFragmentSamplerState:atIndex:"), - .{ state.sampler.sampler.value, @as(c_ulong, 0) }, - ); - - // Set our uniforms - encoder.msgSend( - void, - objc.sel("setFragmentBytes:length:atIndex:"), - .{ - @as(*const anyopaque, @ptrCast(&state.uniforms)), - @as(c_ulong, @sizeOf(@TypeOf(state.uniforms))), - @as(c_ulong, 0), - }, - ); - - // Screen texture - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ - state.back_texture.value, - @as(c_ulong, 0), - }, - ); - - // Draw! - encoder.msgSend( - void, - objc.sel("drawPrimitives:vertexStart:vertexCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 0), - @as(c_ulong, 3), - }, - ); -} - -fn drawImagePlacements( - self: *Metal, - encoder: objc.Object, - frame: *const FrameState, - placements: []const mtl_image.Placement, -) !void { - if (placements.len == 0) return; - - // Use our image shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.image_pipeline.value}, - ); - - // Set our uniforms - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - - for (placements) |placement| { - try self.drawImagePlacement(encoder, placement); - } -} - -fn drawImagePlacement( - self: *Metal, - encoder: objc.Object, - p: mtl_image.Placement, -) !void { - // Look up the image - const image = self.images.get(p.image_id) orelse { - log.warn("image not found for placement image_id={}", .{p.image_id}); - return; - }; - - // Get the texture - const texture = switch (image.image) { - .ready => |t| t, - else => { - log.warn("image not ready for placement image_id={}", .{p.image_id}); - return; - }, - }; - - // Create our vertex buffer, which is always exactly one item. - // future(mitchellh): we can group rendering multiple instances of a single image - const Buffer = mtl_buffer.Buffer(mtl_shaders.Image); - var buf = try Buffer.initFill(self.gpu_state.device, &.{.{ - .grid_pos = .{ - @as(f32, @floatFromInt(p.x)), - @as(f32, @floatFromInt(p.y)), - }, - - .cell_offset = .{ - @as(f32, @floatFromInt(p.cell_offset_x)), - @as(f32, @floatFromInt(p.cell_offset_y)), - }, - - .source_rect = .{ - @as(f32, @floatFromInt(p.source_x)), - @as(f32, @floatFromInt(p.source_y)), - @as(f32, @floatFromInt(p.source_width)), - @as(f32, @floatFromInt(p.source_height)), - }, - - .dest_size = .{ - @as(f32, @floatFromInt(p.width)), - @as(f32, @floatFromInt(p.height)), - }, - }}, .{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = self.gpu_state.default_storage_mode, - }); - defer buf.deinit(); - - // Set our buffer - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ buf.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); - - // Set our texture - encoder.msgSend( - void, - objc.sel("setVertexTexture:atIndex:"), - .{ - texture.value, - @as(c_ulong, 0), - }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ - texture.value, - @as(c_ulong, 0), - }, - ); - - // Draw! - encoder.msgSend( - void, - objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 6), - @intFromEnum(mtl.MTLIndexType.uint16), - self.gpu_state.instance.buffer.value, - @as(c_ulong, 0), - @as(c_ulong, 1), - }, - ); - - // log.debug("drawImagePlacement: {}", .{p}); -} - -/// Draw the cell backgrounds. -fn drawCellBgs( - self: *Metal, - encoder: objc.Object, - frame: *const FrameState, -) !void { - // Use our shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_bg_pipeline.value}, - ); - - // Set our buffers - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - - encoder.msgSend( - void, - objc.sel("drawPrimitives:vertexStart:vertexCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 0), - @as(c_ulong, 3), - }, - ); -} - -/// Draw the cell foregrounds using the text shader. -fn drawCellFgs( - self: *Metal, - encoder: objc.Object, - frame: *const FrameState, - len: usize, -) !void { - // This triggers an assertion in the Metal API if we try to draw - // with an instance count of 0 so just bail. - if (len == 0) return; - - // Use our shader pipeline - encoder.msgSend( - void, - objc.sel("setRenderPipelineState:"), - .{self.shaders.cell_text_pipeline.value}, - ); - - // Set our buffers - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.cells.buffer.value, @as(c_ulong, 0), @as(c_ulong, 0) }, - ); - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setVertexBuffer:offset:atIndex:"), - .{ frame.cells_bg.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ frame.grayscale.value, @as(c_ulong, 0) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentTexture:atIndex:"), - .{ frame.color.value, @as(c_ulong, 1) }, - ); - encoder.msgSend( - void, - objc.sel("setFragmentBuffer:offset:atIndex:"), - .{ frame.uniforms.buffer.value, @as(c_ulong, 0), @as(c_ulong, 2) }, - ); - - encoder.msgSend( - void, - objc.sel("drawIndexedPrimitives:indexCount:indexType:indexBuffer:indexBufferOffset:instanceCount:"), - .{ - @intFromEnum(mtl.MTLPrimitiveType.triangle), - @as(c_ulong, 6), - @intFromEnum(mtl.MTLIndexType.uint16), - self.gpu_state.instance.buffer.value, - @as(c_ulong, 0), - @as(c_ulong, len), - }, - ); -} - -/// This goes through the Kitty graphic placements and accumulates the -/// placements we need to render on our viewport. It also ensures that -/// the visible images are loaded on the GPU. -fn prepKittyGraphics( - self: *Metal, - t: *terminal.Terminal, -) !void { - const storage = &t.screen.kitty_images; - defer storage.dirty = false; - - // We always clear our previous placements no matter what because - // we rebuild them from scratch. - self.image_placements.clearRetainingCapacity(); - self.image_virtual = false; - - // Go through our known images and if there are any that are no longer - // in use then mark them to be freed. - // - // This never conflicts with the below because a placement can't - // reference an image that doesn't exist. - { - var it = self.images.iterator(); - while (it.next()) |kv| { - if (storage.imageById(kv.key_ptr.*) == null) { - kv.value_ptr.image.markForUnload(); - } - } - } - - // The top-left and bottom-right corners of our viewport in screen - // points. This lets us determine offsets and containment of placements. - const top = t.screen.pages.getTopLeft(.viewport); - const bot = t.screen.pages.getBottomRight(.viewport).?; - const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; - const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; - - // Go through the placements and ensure the image is loaded on the GPU. - var it = storage.placements.iterator(); - while (it.next()) |kv| { - const p = kv.value_ptr; - - // Special logic based on location - switch (p.location) { - .pin => {}, - .virtual => { - // We need to mark virtual placements on our renderer so that - // we know to rebuild in more scenarios since cell changes can - // now trigger placement changes. - self.image_virtual = true; - - // We also continue out because virtual placements are - // only triggered by the unicode placeholder, not by the - // placement itself. - continue; - }, - } - - // Get the image for the placement - const image = storage.imageById(kv.key_ptr.image_id) orelse { - log.warn( - "missing image for placement, ignoring image_id={}", - .{kv.key_ptr.image_id}, - ); - continue; - }; - - try self.prepKittyPlacement(t, top_y, bot_y, &image, p); - } - - // If we have virtual placements then we need to scan for placeholders. - if (self.image_virtual) { - var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( - t, - &virtual_p, - ); - } - - // Sort the placements by their Z value. - std.mem.sortUnstable( - mtl_image.Placement, - self.image_placements.items, - {}, - struct { - fn lessThan( - ctx: void, - lhs: mtl_image.Placement, - rhs: mtl_image.Placement, - ) bool { - _ = ctx; - return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); - } - }.lessThan, - ); - - // Find our indices. The values are sorted by z so we can find the - // first placement out of bounds to find the limits. - var bg_end: ?u32 = null; - var text_end: ?u32 = null; - const bg_limit = std.math.minInt(i32) / 2; - for (self.image_placements.items, 0..) |p, i| { - if (bg_end == null and p.z >= bg_limit) { - bg_end = @intCast(i); - } - if (text_end == null and p.z >= 0) { - text_end = @intCast(i); - } - } - - self.image_bg_end = bg_end orelse 0; - self.image_text_end = text_end orelse self.image_bg_end; -} - -fn prepKittyVirtualPlacement( - self: *Metal, - t: *terminal.Terminal, - p: *const terminal.kitty.graphics.unicode.Placement, -) !void { - const storage = &t.screen.kitty_images; - const image = storage.imageById(p.image_id) orelse { - log.warn( - "missing image for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }; - - const rp = p.renderPlacement( - storage, - &image, - self.grid_metrics.cell_width, - self.grid_metrics.cell_height, - ) catch |err| { - log.warn("error rendering virtual placement err={}", .{err}); - return; - }; - - // If our placement is zero sized then we don't do anything. - if (rp.dest_width == 0 or rp.dest_height == 0) return; - - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rp.top_left, - ) orelse { - // This is unreachable with virtual placements because we should - // only ever be looking at virtual placements that are in our - // viewport in the renderer and virtual placements only ever take - // up one row. - unreachable; - }; - - // Send our image to the GPU and store the placement for rendering. - try self.prepKittyImage(&image); - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rp.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = -1, - .width = rp.dest_width, - .height = rp.dest_height, - .cell_offset_x = rp.offset_x, - .cell_offset_y = rp.offset_y, - .source_x = rp.source_x, - .source_y = rp.source_y, - .source_width = rp.source_width, - .source_height = rp.source_height, + .bgra8unorm, + .storage_mode = self.default_storage_mode, + .width = width, + .height = height, }); } -fn prepKittyPlacement( - self: *Metal, - t: *terminal.Terminal, - top_y: u32, - bot_y: u32, - image: *const terminal.kitty.graphics.Image, - p: *const terminal.kitty.graphics.ImageStorage.Placement, -) !void { - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image.*, t) orelse return; - - // This is expensive but necessary. - const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; - - // If the selection isn't within our viewport then skip it. - if (img_top_y > bot_y) return; - if (img_bot_y < top_y) return; - - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - try self.prepKittyImage(image); - - // Calculate the dimensions of our image, taking in to - // account the rows / columns specified by the placement. - const dest_size = p.calculatedSize(image.*, t); - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height - source_y, p.source_height) - else - image.height; - - // Get the viewport-relative Y position of the placement. - const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); - - // Accumulate the placement - if (dest_size.width > 0 and dest_size.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rect.top_left.x), - .y = y_pos, - .z = p.z, - .width = dest_size.width, - .height = dest_size.height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } -} - -fn prepKittyImage( - self: *Metal, - image: *const terminal.kitty.graphics.Image, -) !void { - // If this image exists and its transmit time is the same we assume - // it is the identical image so we don't need to send it to the GPU. - const gop = try self.images.getOrPut(self.alloc, image.id); - if (gop.found_existing and - gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) - { - return; - } - - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .gray => .{ .pending_gray = pending }, - .gray_alpha => .{ .pending_gray_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; +/// Present the provided target. +pub inline fn present(self: *Metal, target: Target, sync: bool) !void { + if (sync) { + self.layer.setSurfaceSync(target.surface); } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; -} - -/// Update the configuration. -pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void { - // We always redo the font shaper in case font features changed. We - // could check to see if there was an actual config change but this is - // easier and rare enough to not cause performance issues. - { - var font_shaper = try font.Shaper.init(self.alloc, .{ - .features = config.font_features.items, - }); - errdefer font_shaper.deinit(); - self.font_shaper.deinit(); - self.font_shaper = font_shaper; - } - - // We also need to reset the shaper cache so shaper info - // from the previous font isn't re-used for the new font. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Set our new minimum contrast - self.uniforms.min_contrast = config.min_contrast; - - // Set our new color space and blending - self.uniforms.use_display_p3 = config.colorspace == .@"display-p3"; - self.uniforms.use_linear_blending = config.blending.isLinear(); - self.uniforms.use_linear_correction = config.blending == .@"linear-corrected"; - - // Set our new colors - self.default_background_color = config.background; - self.default_foreground_color = config.foreground; - self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; - self.cursor_invert = config.cursor_invert; - - // Update our layer's opaqueness and display sync in case they changed. - { - // We use a CATransaction so that Core Animation knows that we - // updated the opaque property. Otherwise it behaves weird, not - // properly going from opaque to transparent unless we resize. - const CATransaction = objc.getClass("CATransaction").?; - CATransaction.msgSend(void, "begin", .{}); - defer CATransaction.msgSend(void, "commit", .{}); - - self.layer.setProperty("opaque", config.background_opacity >= 1); - self.layer.setProperty("displaySyncEnabled", config.vsync); - } - - // Update our terminal colorspace if it changed - if (self.config.colorspace != config.colorspace) { - const terminal_colorspace = try graphics.ColorSpace.createNamed( - switch (config.colorspace) { - .@"display-p3" => .displayP3, - .srgb => .sRGB, - }, - ); - errdefer terminal_colorspace.release(); - self.terminal_colorspace.release(); - self.terminal_colorspace = terminal_colorspace; - } - - const old_blending = self.config.blending; - const old_custom_shaders = self.config.custom_shaders; - - self.config.deinit(); - self.config = config.*; - - // Reset our viewport to force a rebuild, in case of a font change. - self.cells_viewport = null; - - // We reinitialize our shaders if our - // blending or custom shaders changed. - if (old_blending != config.blending or - !old_custom_shaders.equal(config.custom_shaders)) - { - self.deinitShaders(); - try self.initShaders(); - // We call setScreenSize to reinitialize - // the textures used for custom shaders. - if (self.custom_shader_state != null) { - try self.setScreenSize(self.size); - } - // And we update our layer's pixel format appropriately. - self.layer.setProperty( - "pixelFormat", - if (config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); + try self.layer.setSurface(target.surface); } } -/// Resize the screen. -pub fn setScreenSize( - self: *Metal, - size: renderer.Size, -) !void { - // Store our sizes - self.size = size; - const grid_size = size.grid(); - const terminal_size = size.terminal(); - - // Blank space around the grid. - const blank: renderer.Padding = size.screen.blankPadding( - size.padding, - grid_size, - size.cell, - ).add(size.padding); - - var padding_extend = self.uniforms.padding_extend; - switch (self.config.padding_color) { - .extend => { - // If padding extension is enabled, we extend left and right always - // because there is no downside to this. Up/down is dependent - // on some heuristics (see rebuildCells). - padding_extend.left = true; - padding_extend.right = true; - }, - - .@"extend-always" => { - padding_extend.up = true; - padding_extend.down = true; - padding_extend.left = true; - padding_extend.right = true; - }, - - .background => { - // Otherwise, disable all padding extension. - padding_extend = .{}; - }, - } - - // Set the size of the drawable surface to the bounds - self.layer.setProperty("drawableSize", graphics.Size{ - .width = @floatFromInt(size.screen.width), - .height = @floatFromInt(size.screen.height), - }); - - // Setup our uniforms - const old = self.uniforms; - self.uniforms = .{ - .projection_matrix = math.ortho2d( - -1 * @as(f32, @floatFromInt(size.padding.left)), - @floatFromInt(terminal_size.width + size.padding.right), - @floatFromInt(terminal_size.height + size.padding.bottom), - -1 * @as(f32, @floatFromInt(size.padding.top)), - ), - .cell_size = .{ - @floatFromInt(self.grid_metrics.cell_width), - @floatFromInt(self.grid_metrics.cell_height), - }, - .grid_size = .{ - grid_size.columns, - grid_size.rows, - }, - .grid_padding = .{ - @floatFromInt(blank.top), - @floatFromInt(blank.right), - @floatFromInt(blank.bottom), - @floatFromInt(blank.left), - }, - .padding_extend = padding_extend, - .min_contrast = old.min_contrast, - .cursor_pos = old.cursor_pos, - .cursor_color = old.cursor_color, - .bg_color = old.bg_color, - .cursor_wide = old.cursor_wide, - .use_display_p3 = old.use_display_p3, - .use_linear_blending = old.use_linear_blending, - .use_linear_correction = old.use_linear_correction, - }; - - // Reset our cell contents if our grid size has changed. - if (!self.cells.size.equals(grid_size)) { - try self.cells.resize(self.alloc, grid_size); - - // Reset our viewport to force a rebuild - self.cells_viewport = null; - } - - // If we have custom shaders then we update the state - if (self.custom_shader_state) |*state| { - // Only free our previous texture if this isn't our first - // time setting the custom shader state. - if (state.uniforms.resolution[0] > 0) { - state.front_texture.release(); - state.back_texture.release(); - } - - state.uniforms.resolution = .{ - @floatFromInt(size.screen.width), - @floatFromInt(size.screen.height), - 1, - }; - - state.front_texture = texture: { - // This texture is the size of our drawable but supports being a - // render target AND reading so that the custom shaders can read from it. - const desc = init: { - const Class = objc.getClass("MTLTextureDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - desc.setProperty( - "pixelFormat", - // Using an `*_srgb` pixel format makes Metal gamma encode - // the pixels written to it *after* blending, which means - // we get linear alpha blending rather than gamma-incorrect - // blending. - if (self.config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); - desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width))); - desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); - desc.setProperty( - "usage", - mtl.MTLTextureUsage{ - .render_target = true, - .shader_read = true, - .shader_write = true, - }, - ); - - // If we fail to create the texture, then we just don't have a screen - // texture and our custom shaders won't run. - const id = self.gpu_state.device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - break :texture objc.Object.fromId(id); - }; - - state.back_texture = texture: { - // This texture is the size of our drawable but supports being a - // render target AND reading so that the custom shaders can read from it. - const desc = init: { - const Class = objc.getClass("MTLTextureDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - desc.setProperty( - "pixelFormat", - // Using an `*_srgb` pixel format makes Metal gamma encode - // the pixels written to it *after* blending, which means - // we get linear alpha blending rather than gamma-incorrect - // blending. - if (self.config.blending.isLinear()) - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm_srgb) - else - @intFromEnum(mtl.MTLPixelFormat.bgra8unorm), - ); - desc.setProperty("width", @as(c_ulong, @intCast(size.screen.width))); - desc.setProperty("height", @as(c_ulong, @intCast(size.screen.height))); - desc.setProperty( - "usage", - mtl.MTLTextureUsage{ - .render_target = true, - .shader_read = true, - .shader_write = true, - }, - ); - - // If we fail to create the texture, then we just don't have a screen - // texture and our custom shaders won't run. - const id = self.gpu_state.device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - break :texture objc.Object.fromId(id); - }; - } - - log.debug("screen size size={}", .{size}); +/// Present the last presented target again. (noop for Metal) +pub inline fn repeat(self: *Metal) !void { + _ = self; } -/// Convert the terminal state to GPU cells stored in CPU memory. These -/// are then synced to the GPU in the next frame. This only updates CPU -/// memory and doesn't touch the GPU. -fn rebuildCells( - self: *Metal, - rebuild: bool, - screen: *terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style_: ?renderer.CursorStyle, - color_palette: *const terminal.color.Palette, -) !void { - // const start = try std.time.Instant.now(); - // const start_micro = std.time.microTimestamp(); - // defer { - // const end = std.time.Instant.now() catch unreachable; - // // "[rebuildCells time] \t" - // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); - // } - - _ = screen_type; // we might use this again later so not deleting it yet - - // Create an arena for all our temporary allocations while rebuilding - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Create our match set for the links. - var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( - arena_alloc, - screen, - mouse_pt, - mouse.mods, - ) else .{}; - - // Determine our x/y range for preedit. We don't want to render anything - // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { - const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); - break :preedit .{ - .y = screen.cursor.y, - .x = .{ range.start, range.end }, - .cp_offset = range.cp_offset, - }; - } else null; - - if (rebuild) { - // If we are doing a full rebuild, then we clear the entire cell buffer. - self.cells.reset(); - - // We also reset our padding extension depending on the screen type - switch (self.config.padding_color) { - .background => {}, - - // For extension, assume we are extending in all directions. - // For "extend" this may be disabled due to heuristics below. - .extend, .@"extend-always" => { - self.uniforms.padding_extend = .{ - .up = true, - .down = true, - .left = true, - .right = true, - }; - }, - } - } - - // We rebuild the cells row-by-row because we - // do font shaping and dirty tracking by row. - var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); - // If our cell contents buffer is shorter than the screen viewport, - // we render the rows that fit, starting from the bottom. If instead - // the viewport is shorter than the cell contents buffer, we align - // the top of the viewport with the top of the contents buffer. - var y: terminal.size.CellCountInt = @min( - screen.pages.rows, - self.cells.size.rows, - ); - while (row_it.next()) |row| { - // The viewport may have more rows than our cell contents, - // so we need to break from the loop early if we hit y = 0. - if (y == 0) break; - - y -= 1; - - if (!rebuild) { - // Only rebuild if we are doing a full rebuild or this row is dirty. - if (!row.isDirty()) continue; - - // Clear the cells if the row is dirty - self.cells.clear(y); - } - - // True if we want to do font shaping around the cursor. We want to - // do font shaping as long as the cursor is enabled. - const shape_cursor = screen.viewportIsBottom() and - y == screen.cursor.y; - - // We need to get this row's selection if there is one for proper - // run splitting. - const row_selection = sel: { - const sel = screen.selection orelse break :sel null; - const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse - break :sel null; - break :sel sel.containedRow(screen, pin) orelse null; - }; - - // On primary screen, we still apply vertical padding extension - // under certain conditions we feel are safe. This helps make some - // scenarios look better while avoiding scenarios we know do NOT look - // good. - switch (self.config.padding_color) { - // These already have the correct values set above. - .background, .@"extend-always" => {}, - - // Apply heuristics for padding extension. - .extend => if (y == 0) { - self.uniforms.padding_extend.up = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - } else if (y == self.cells.size.rows - 1) { - self.uniforms.padding_extend.down = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - }, - } - - // Iterator of runs for shaping. - var run_iter = self.font_shaper.runIterator( - self.font_grid, - screen, - row, - row_selection, - if (shape_cursor) screen.cursor.x else null, - ); - var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); - var shaper_cells: ?[]const font.shape.Cell = null; - var shaper_cells_i: usize = 0; - - const row_cells_all = row.cells(.all); - - // If our viewport is wider than our cell contents buffer, - // we still only process cells up to the width of the buffer. - const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)]; - - for (row_cells, 0..) |*cell, x| { - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| preedit: { - // We're not on the preedit line, no actions necessary. - if (range.y != y) break :preedit; - // We're before the preedit range, no actions necessary. - if (x < range.x[0]) break :preedit; - // We're in the preedit range, skip this cell. - if (x <= range.x[1]) continue; - // After exiting the preedit range we need to catch - // the run position up because of the missed cells. - // In all other cases, no action is necessary. - if (x != range.x[1] + 1) break :preedit; - - // Step the run iterator until we find a run that ends - // after the current cell, which will be the soonest run - // that might contain glyphs for our cell. - while (shaper_run) |run| { - if (run.offset + run.cells > x) break; - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - const run = shaper_run orelse break :preedit; - - // If we haven't shaped this run, do so now. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - // Advance our index until we reach or pass - // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x < x) { - shaper_cells_i += 1; - } - } - - const wide = cell.wide; - - const style = row.style(cell); - - const cell_pin: terminal.Pin = cell: { - var copy = row; - copy.x = @intCast(x); - break :cell copy; - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, .{ - .node = row.node, - .y = row.y, - .x = @intCast( - // Spacer tails should show the selection - // state of the wide cell they belong to. - if (wide == .spacer_tail) - x -| 1 - else - x, - ), - }) - else - false; - - const bg_style = style.bg(cell, color_palette); - const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; - - // The final background color for the cell. - const bg = bg: { - if (selected) { - break :bg if (self.config.invert_selection_fg_bg) - if (style.flags.inverse) - // Cell is selected with invert selection fg/bg - // enabled, and the cell has the inverse style - // flag, so they cancel out and we get the normal - // bg color. - bg_style - else - // If it doesn't have the inverse style - // flag then we use the fg color instead. - fg_style - else - // If we don't have invert selection fg/bg set then we - // just use the selection background if set, otherwise - // the default fg color. - break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; - } - - // Not selected - break :bg if (style.flags.inverse != isCovering(cell.codepoint())) - // Two cases cause us to invert (use the fg color as the bg) - // - The "inverse" style flag. - // - A "covering" glyph; we use fg for bg in that case to - // help make sure that padding extension works correctly. - // If one of these is true (but not the other) - // then we use the fg style color for the bg. - fg_style - else - // Otherwise they cancel out. - bg_style; - }; - - const fg = fg: { - if (selected and !self.config.invert_selection_fg_bg) { - // If we don't have invert selection fg/bg set - // then we just use the selection foreground if - // set, otherwise the default bg color. - break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; - } - - // Whether we need to use the bg color as our fg color: - // - Cell is inverted and not selected - // - Cell is selected and not inverted - // Note: if selected then invert sel fg / bg must be - // false since we separately handle it if true above. - break :fg if (style.flags.inverse != selected) - bg_style orelse self.background_color orelse self.default_background_color - else - fg_style; - }; - - // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // Set the cell's background color. - { - const rgb = bg orelse self.background_color orelse self.default_background_color; - - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // Cells that are selected should be fully opaque. - if (selected) break :bg_alpha default; - - // Cells that are reversed should be fully opaque. - if (style.flags.inverse) break :bg_alpha default; - - // Cells that have an explicit bg color should be fully opaque. - if (bg_style != null) { - break :bg_alpha default; - } - - // Otherwise, we use the configured background opacity. - break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0)); - }; - - self.cells.bgCell(y, x).* = .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } - - // If the invisible flag is set on this cell then we - // don't need to render any foreground elements, so - // we just skip all glyphs with this x coordinate. - // - // NOTE: This behavior matches xterm. Some other terminal - // emulators, e.g. Alacritty, still render text decorations - // and only make the text itself invisible. The decision - // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { - continue; - } - - // Give links a single underline, unless they already have - // an underline, in which case use a double underline to - // distinguish them. - const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) - if (style.flags.underline == .single) - .double - else - .single - else - style.flags.underline; - - // We draw underlines first so that they layer underneath text. - // This improves readability when a colored underline is used - // which intersects parts of the text (descenders). - if (underline != .none) self.addUnderline( - @intCast(x), - @intCast(y), - underline, - style.underlineColor(color_palette) orelse fg, - alpha, - ) catch |err| { - log.warn( - "error adding underline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { - log.warn( - "error adding overline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - // If we're at or past the end of our shaper run then - // we need to get the next run from the run iterator. - if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - if (shaper_run) |run| glyphs: { - // If we haven't shaped this run yet, do so. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - const cells = shaper_cells orelse break :glyphs; - - // If there are no shaper cells for this run, ignore it. - // This can occur for runs of empty cells, and is fine. - if (cells.len == 0) break :glyphs; - - // If we encounter a shaper cell to the left of the current - // cell then we have some problems. This logic relies on x - // position monotonically increasing. - assert(cells[shaper_cells_i].x >= x); - - // NOTE: An assumption is made here that a single cell will never - // be present in more than one shaper run. If that assumption is - // violated, this logic breaks. - - while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ - shaper_cells_i += 1; - }) { - self.addGlyph( - @intCast(x), - @intCast(y), - cell_pin, - cells[shaper_cells_i], - shaper_run.?, - fg, - alpha, - ) catch |err| { - log.warn( - "error adding glyph to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Finally, draw a strikethrough if necessary. - if (style.flags.strikethrough) self.addStrikethrough( - @intCast(x), - @intCast(y), - fg, - alpha, - ) catch |err| { - log.warn( - "error adding strikethrough to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Setup our cursor rendering information. - cursor: { - // By default, we don't handle cursor inversion on the shader. - self.cells.setCursor(null); - self.uniforms.cursor_pos = .{ - std.math.maxInt(u16), - std.math.maxInt(u16), - }; - - // If we have preedit text, we don't setup a cursor - if (preedit != null) break :cursor; - - // Prepare the cursor cell contents. - const style = cursor_style_ orelse break :cursor; - const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { - if (self.cursor_invert) { - // Use the foreground color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :color if (sty.flags.inverse) - // If the cell is reversed, use background color instead. - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) - else - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); - } else { - break :color self.foreground_color orelse self.default_foreground_color; - } - }; - - self.addCursor(screen, style, cursor_color); - - // If the cursor is visible then we set our uniforms. - if (style == .block and screen.viewportIsBottom()) { - const wide = screen.cursor.page_cell.wide; - - self.uniforms.cursor_pos = .{ - // If we are a spacer tail of a wide cell, our cursor needs - // to move back one cell. The saturate is to ensure we don't - // overflow but this shouldn't happen with well-formed input. - switch (wide) { - .narrow, .spacer_head, .wide => screen.cursor.x, - .spacer_tail => screen.cursor.x -| 1, - }, - screen.cursor.y, - }; - - self.uniforms.cursor_wide = switch (wide) { - .narrow, .spacer_head => false, - .wide, .spacer_tail => true, - }; - - const uniform_color = if (self.cursor_invert) blk: { - // Use the background color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :blk if (sty.flags.inverse) - // If the cell is reversed, use foreground color instead. - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) - else - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); - } else if (self.config.cursor_text) |txt| - txt - else - self.background_color orelse self.default_background_color; - - self.uniforms.cursor_color = .{ - uniform_color.r, - uniform_color.g, - uniform_color.b, - 255, - }; - } - } - - // Setup our preedit text. - if (preedit) |preedit_v| { - const range = preedit_range.?; - var x = range.x[0]; - for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { - log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - x, - range.y, - err, - }); - }; - - x += if (cp.wide) 2 else 1; - } - } - - // Update that our cells rebuilt - self.cells_rebuilt = true; - - // Log some things - // log.debug("rebuildCells complete cached_runs={}", .{ - // self.font_shaper_cache.count(), - // }); -} - -/// Add an underline decoration to the specified cell -fn addUnderline( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - style: terminal.Attribute.Underline, - color: terminal.color.RGB, - alpha: u8, -) !void { - const sprite: font.Sprite = switch (style) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.add(self.alloc, .underline, .{ - .mode = .fg, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); -} - -/// Add a overline decoration to the specified cell -fn addOverline( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.overline), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.add(self.alloc, .overline, .{ - .mode = .fg, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); -} - -/// Add a strikethrough decoration to the specified cell -fn addStrikethrough( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.add(self.alloc, .strikethrough, .{ - .mode = .fg, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = 1, - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); -} - -// Add a glyph to the specified cell. -fn addGlyph( - self: *Metal, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - cell_pin: terminal.Pin, - shaper_cell: font.shape.Cell, - shaper_run: font.shape.TextRun, - color: terminal.color.RGB, - alpha: u8, -) !void { - const rac = cell_pin.rowAndCell(); - const cell = rac.cell; - - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - shaper_cell.glyph_index, - .{ - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - .thicken_strength = self.config.font_thicken_strength, - }, - ); - - // If the glyph is 0 width or height, it will be invisible - // when drawn, so don't bother adding it to the buffer. - if (render.glyph.width == 0 or render.glyph.height == 0) { - return; - } - - const mode: mtl_shaders.CellText.Mode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - - try self.cells.add(self.alloc, .text, .{ - .mode = mode, - .grid_pos = .{ @intCast(x), @intCast(y) }, - .constraint_width = cell.gridWidth(), - .color = .{ color.r, color.g, color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x + shaper_cell.x_offset), - @intCast(render.glyph.offset_y + shaper_cell.y_offset), - }, - }); -} - -fn addCursor( - self: *Metal, - screen: *terminal.Screen, - cursor_style: renderer.CursorStyle, - cursor_color: terminal.color.RGB, -) void { - // Add the cursor. We render the cursor over the wide character if - // we're on the wide character tail. - const wide, const x = cell: { - // The cursor goes over the screen cursor position. - const cell = screen.cursor.page_cell; - if (cell.wide != .spacer_tail or screen.cursor.x == 0) - break :cell .{ cell.wide == .wide, screen.cursor.x }; - - // If we're part of a wide character, we move the cursor back to - // the actual character. - const prev_cell = screen.cursorCellLeft(1); - break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; - }; - - const alpha: u8 = if (!self.focused) 255 else alpha: { - const alpha = 255 * self.config.cursor_opacity; - break :alpha @intFromFloat(@ceil(alpha)); - }; - - const render = switch (cursor_style) { - .block, - .block_hollow, - .bar, - .underline, - => render: { - const sprite: font.Sprite = switch (cursor_style) { - .block => .cursor_rect, - .block_hollow => .cursor_hollow_rect, - .bar => .cursor_bar, - .underline => .underline, - .lock => unreachable, - }; - - break :render self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return; - }; - }, - - .lock => self.font_grid.renderCodepoint( - self.alloc, - 0xF023, // lock symbol - .regular, - .text, - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return; - } orelse { - // This should never happen because we embed nerd - // fonts so we just log and return instead of fallback. - log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); - return; +/// Returns the options to use when constructing buffers. +pub inline fn bufferOptions(self: Metal) bufferpkg.Options { + return .{ + .device = self.device, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, }, }; - - self.cells.setCursor(.{ - .mode = .cursor, - .grid_pos = .{ x, screen.cursor.y }, - .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), - }, - }); } -fn addPreeditCell( - self: *Metal, - cp: renderer.State.Preedit.Codepoint, - coord: terminal.Coordinate, -) !void { - // Preedit is rendered inverted - const bg = self.foreground_color orelse self.default_foreground_color; - const fg = self.background_color orelse self.default_background_color; +pub const instanceBufferOptions = bufferOptions; +pub const uniformBufferOptions = bufferOptions; +pub const fgBufferOptions = bufferOptions; +pub const bgBufferOptions = bufferOptions; +pub const imageBufferOptions = bufferOptions; - // Render the glyph for our preedit text - const render_ = self.font_grid.renderCodepoint( - self.alloc, - @intCast(cp.codepoint), - .regular, - .text, - .{ .grid_metrics = self.grid_metrics }, - ) catch |err| { - log.warn("error rendering preedit glyph err={}", .{err}); - return; - }; - const render = render_ orelse { - log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); - return; - }; - - // Add our opaque background cell - self.cells.bgCell(coord.y, coord.x).* = .{ - bg.r, bg.g, bg.b, 255, - }; - if (cp.wide and coord.x < self.cells.size.columns - 1) { - self.cells.bgCell(coord.y, coord.x + 1).* = .{ - bg.r, bg.g, bg.b, 255, - }; - } - - // Add our text - try self.cells.add(self.alloc, .text, .{ - .mode = .fg, - .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, - .color = .{ fg.r, fg.g, fg.b, 255 }, - .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, - .glyph_size = .{ render.glyph.width, render.glyph.height }, - .bearings = .{ - @intCast(render.glyph.offset_x), - @intCast(render.glyph.offset_y), +/// Returns the options to use when constructing textures. +pub inline fn textureOptions(self: Metal) Texture.Options { + return .{ + .device = self.device, + // Using an `*_srgb` pixel format makes Metal gamma encode the pixels + // written to it *after* blending, which means we get linear alpha + // blending rather than gamma-incorrect blending. + .pixel_format = if (self.blending.isLinear()) + .bgra8unorm_srgb + else + .bgra8unorm, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, }, - }); + }; } -/// Sync the atlas data to the given texture. This copies the bytes -/// associated with the atlas to the given texture. If the atlas no longer -/// fits into the texture, the texture will be resized. -fn syncAtlasTexture( - device: objc.Object, - atlas: *const font.Atlas, - texture: *objc.Object, - /// Storage mode for the MTLTexture object - storage_mode: mtl.MTLResourceOptions.StorageMode, -) !void { - const width = texture.getProperty(c_ulong, "width"); - if (atlas.size > width) { - // Free our old texture - texture.*.release(); - - // Reallocate - texture.* = try initAtlasTexture(device, atlas, storage_mode); - } - - texture.msgSend( - void, - objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), - .{ - mtl.MTLRegion{ - .origin = .{ .x = 0, .y = 0, .z = 0 }, - .size = .{ - .width = @intCast(atlas.size), - .height = @intCast(atlas.size), - .depth = 1, - }, - }, - @as(c_ulong, 0), - @as(*const anyopaque, atlas.data.ptr), - @as(c_ulong, atlas.format.depth() * atlas.size), - }, - ); -} - -/// Initialize a MTLTexture object for the given atlas. -fn initAtlasTexture( - device: objc.Object, - atlas: *const font.Atlas, - /// Storage mode for the MTLTexture object - storage_mode: mtl.MTLResourceOptions.StorageMode, -) !objc.Object { - // Determine our pixel format +/// Initializes a Texture suitable for the provided font atlas. +pub fn initAtlasTexture(self: *const Metal, atlas: *const font.Atlas) !Texture { const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) { .grayscale => .r8unorm, .rgba => .bgra8unorm, else => @panic("unsupported atlas format for Metal texture"), }; - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLTextureDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - - // Set our properties - desc.setProperty("pixelFormat", @intFromEnum(pixel_format)); - desc.setProperty("width", @as(c_ulong, @intCast(atlas.size))); - desc.setProperty("height", @as(c_ulong, @intCast(atlas.size))); - - desc.setProperty( - "resourceOptions", - mtl.MTLResourceOptions{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, + return Texture.init( + .{ + .device = self.device, + .pixel_format = pixel_format, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = self.default_storage_mode, + }, }, + atlas.size, + atlas.size, + null, ); - - // Initialize - const id = device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - return objc.Object.fromId(id); } -test { - _ = mtl_cell; +/// Begin a frame. +pub inline fn beginFrame( + self: *const Metal, + /// Once the frame has been completed, the `frameCompleted` method + /// on the renderer is called with the health status of the frame. + renderer: *Renderer, + /// The target is presented via the provided renderer's API when completed. + target: *Target, +) !Frame { + return try Frame.begin(.{ .queue = self.queue }, renderer, target); +} + +fn chooseDevice() error{NoMetalDevice}!objc.Object { + var chosen_device: ?objc.Object = null; + + switch (comptime builtin.os.tag) { + .macos => { + const devices = objc.Object.fromId(mtl.MTLCopyAllDevices()); + defer devices.release(); + + var iter = devices.iterate(); + while (iter.next()) |device| { + // We want a GPU that’s connected to a display. + if (device.getProperty(bool, "isHeadless")) continue; + chosen_device = device; + // If the user has an eGPU plugged in, they probably want + // to use it. Otherwise, integrated GPUs are better for + // battery life and thermals. + if (device.getProperty(bool, "isRemovable") or + device.getProperty(bool, "isLowPower")) break; + } + }, + .ios => { + chosen_device = objc.Object.fromId(mtl.MTLCreateSystemDefaultDevice()); + }, + else => @compileError("unsupported target for Metal"), + } + + const device = chosen_device orelse return error.NoMetalDevice; + return device.retain(); } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index d0222a390..c2f8bd652 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -1,452 +1,159 @@ -//! Rendering implementation for OpenGL. +//! Graphics API wrapper for OpenGL. pub const OpenGL = @This(); const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; const builtin = @import("builtin"); const glfw = @import("glfw"); -const assert = std.debug.assert; -const testing = std.testing; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const link = @import("link.zig"); -const isCovering = @import("cell.zig").isCovering; -const fgMode = @import("cell.zig").fgMode; +const gl = @import("opengl"); const shadertoy = @import("shadertoy.zig"); const apprt = @import("../apprt.zig"); -const configpkg = @import("../config.zig"); const font = @import("../font/main.zig"); -const imgui = @import("imgui"); -const renderer = @import("../renderer.zig"); -const terminal = @import("../terminal/main.zig"); -const Terminal = terminal.Terminal; -const gl = @import("opengl"); -const math = @import("../math.zig"); -const Surface = @import("../Surface.zig"); +const configpkg = @import("../config.zig"); +const rendererpkg = @import("../renderer.zig"); +const Renderer = rendererpkg.GenericRenderer(OpenGL); -const CellProgram = @import("opengl/CellProgram.zig"); -const ImageProgram = @import("opengl/ImageProgram.zig"); -const gl_image = @import("opengl/image.zig"); -const custom = @import("opengl/custom.zig"); -const Image = gl_image.Image; -const ImageMap = gl_image.ImageMap; -const ImagePlacementList = std.ArrayListUnmanaged(gl_image.Placement); +pub const GraphicsAPI = OpenGL; +pub const Target = @import("opengl/Target.zig"); +pub const Frame = @import("opengl/Frame.zig"); +pub const RenderPass = @import("opengl/RenderPass.zig"); +pub const Pipeline = @import("opengl/Pipeline.zig"); +const bufferpkg = @import("opengl/buffer.zig"); +pub const Buffer = bufferpkg.Buffer; +pub const Texture = @import("opengl/Texture.zig"); +pub const shaders = @import("opengl/shaders.zig"); -const log = std.log.scoped(.grid); +pub const cellpkg = @import("opengl/cell.zig"); +pub const imagepkg = @import("opengl/image.zig"); -/// The runtime can request a single-threaded draw by setting this boolean -/// to true. In this case, the renderer.draw() call is expected to be called -/// from the runtime. -pub const single_threaded_draw = if (@hasDecl(apprt.Surface, "opengl_single_threaded_draw")) - apprt.Surface.opengl_single_threaded_draw -else - false; -const DrawMutex = if (single_threaded_draw) std.Thread.Mutex else void; -const drawMutexZero: DrawMutex = if (DrawMutex == void) void{} else .{}; +pub const custom_shader_target: shadertoy.Target = .glsl; + +const log = std.log.scoped(.opengl); + +/// We require at least OpenGL 4.3 +pub const MIN_VERSION_MAJOR = 4; +pub const MIN_VERSION_MINOR = 3; alloc: std.mem.Allocator, -/// The configuration we need derived from the main config. -config: DerivedConfig, +/// Alpha blending mode +blending: configpkg.Config.AlphaBlending, -/// Current font metrics defining our grid. -grid_metrics: font.Metrics, +/// The most recently presented target, in case we need to present it again. +last_target: ?Target = null, -/// The size of everything. -size: renderer.Size, - -/// The current set of cells to render. Each set of cells goes into -/// a separate shader call. -cells_bg: std.ArrayListUnmanaged(CellProgram.Cell), -cells: std.ArrayListUnmanaged(CellProgram.Cell), - -/// The last viewport that we based our rebuild off of. If this changes, -/// then we do a full rebuild of the cells. The pointer values in this pin -/// are NOT SAFE to read because they may be modified, freed, etc from the -/// termio thread. We treat the pointers as integers for comparison only. -cells_viewport: ?terminal.Pin = null, - -/// The size of the cells list that was sent to the GPU. This is used -/// to detect when the cells array was reallocated/resized and handle that -/// accordingly. -gl_cells_size: usize = 0, - -/// The last length of the cells that was written to the GPU. This is used to -/// determine what data needs to be rewritten on the GPU. -gl_cells_written: usize = 0, - -/// Shader program for cell rendering. -gl_state: ?GLState = null, - -/// The font structures. -font_grid: *font.SharedGrid, -font_shaper: font.Shaper, -font_shaper_cache: font.ShaperCache, -texture_grayscale_modified: usize = 0, -texture_grayscale_resized: usize = 0, -texture_color_modified: usize = 0, -texture_color_resized: usize = 0, - -/// True if the window is focused -focused: bool, - -/// The foreground color set by an OSC 10 sequence. If unset then the default -/// value from the config file is used. -foreground_color: ?terminal.color.RGB, - -/// Foreground color set in the user's config file. -default_foreground_color: terminal.color.RGB, - -/// The background color set by an OSC 11 sequence. If unset then the default -/// value from the config file is used. -background_color: ?terminal.color.RGB, - -/// Background color set in the user's config file. -default_background_color: terminal.color.RGB, - -/// The cursor color set by an OSC 12 sequence. If unset then -/// default_cursor_color is used. -cursor_color: ?terminal.color.RGB, - -/// Default cursor color when no color is set explicitly by an OSC 12 command. -/// This is cursor color as set in the user's config, if any. If no cursor color -/// is set in the user's config, then the cursor color is determined by the -/// current foreground color. -default_cursor_color: ?terminal.color.RGB, - -/// When `cursor_color` is null, swap the foreground and background colors of -/// the cell under the cursor for the cursor color. Otherwise, use the default -/// foreground color as the cursor color. -cursor_invert: bool, - -/// The mailbox for communicating with the window. -surface_mailbox: apprt.surface.Mailbox, - -/// Deferred operations. This is used to apply changes to the OpenGL context. -/// Some runtimes (GTK) do not support multi-threading so to keep our logic -/// simple we apply all OpenGL context changes in the render() call. -deferred_screen_size: ?SetScreenSize = null, -deferred_font_size: ?SetFontSize = null, -deferred_config: ?SetConfig = null, - -/// If we're drawing with single threaded operations -draw_mutex: DrawMutex = drawMutexZero, - -/// Current background to draw. This may not match self.background if the -/// terminal is in reversed mode. -draw_background: terminal.color.RGB, - -/// Whether we're doing padding extension for vertical sides. -padding_extend_top: bool = true, -padding_extend_bottom: bool = true, - -/// The images that we may render. -images: ImageMap = .{}, -image_placements: ImagePlacementList = .{}, -image_bg_end: u32 = 0, -image_text_end: u32 = 0, -image_virtual: bool = false, - -/// Deferred OpenGL operation to update the screen size. -const SetScreenSize = struct { - size: renderer.Size, - - fn apply(self: SetScreenSize, r: *OpenGL) !void { - const gl_state: *GLState = if (r.gl_state) |*v| - v - else - return error.OpenGLUninitialized; - - // Apply our padding - const grid_size = self.size.grid(); - const terminal_size = self.size.terminal(); - - // Blank space around the grid. - const blank: renderer.Padding = switch (r.config.padding_color) { - // We can use zero padding because the background color is our - // clear color. - .background => .{}, - - .extend, .@"extend-always" => self.size.screen.blankPadding( - self.size.padding, - grid_size, - self.size.cell, - ).add(self.size.padding), - }; - - // Update our viewport for this context to be the entire window. - // OpenGL works in pixels, so we have to use the pixel size. - try gl.viewport( - 0, - 0, - @intCast(self.size.screen.width), - @intCast(self.size.screen.height), - ); - - // Update the projection uniform within our shader - inline for (.{ "cell_program", "image_program" }) |name| { - const program = @field(gl_state, name); - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "projection", - - // 2D orthographic projection with the full w/h - math.ortho2d( - -1 * @as(f32, @floatFromInt(self.size.padding.left)), - @floatFromInt(terminal_size.width + self.size.padding.right), - @floatFromInt(terminal_size.height + self.size.padding.bottom), - -1 * @as(f32, @floatFromInt(self.size.padding.top)), - ), - ); - } - - // Setup our grid padding - { - const program = gl_state.cell_program; - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "grid_padding", - @Vector(4, f32){ - @floatFromInt(blank.top), - @floatFromInt(blank.right), - @floatFromInt(blank.bottom), - @floatFromInt(blank.left), - }, - ); - try program.program.setUniform( - "grid_size", - @Vector(2, f32){ - @floatFromInt(grid_size.columns), - @floatFromInt(grid_size.rows), - }, - ); - } - - // Update our custom shader resolution - if (gl_state.custom) |*custom_state| { - try custom_state.setScreenSize(self.size); - } - } -}; - -const SetFontSize = struct { - metrics: font.Metrics, - - fn apply(self: SetFontSize, r: *const OpenGL) !void { - const gl_state = r.gl_state orelse return error.OpenGLUninitialized; - - inline for (.{ "cell_program", "image_program" }) |name| { - const program = @field(gl_state, name); - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "cell_size", - @Vector(2, f32){ - @floatFromInt(self.metrics.cell_width), - @floatFromInt(self.metrics.cell_height), - }, - ); - } - } -}; - -const SetConfig = struct { - fn apply(self: SetConfig, r: *const OpenGL) !void { - _ = self; - const gl_state = r.gl_state orelse return error.OpenGLUninitialized; - - const bind = try gl_state.cell_program.program.use(); - defer bind.unbind(); - try gl_state.cell_program.program.setUniform( - "min_contrast", - r.config.min_contrast, - ); - } -}; - -/// The configuration for this renderer that is derived from the main -/// configuration. This must be exported so that we don't need to -/// pass around Config pointers which makes memory management a pain. -pub const DerivedConfig = struct { - arena: ArenaAllocator, - - font_thicken: bool, - font_thicken_strength: u8, - font_features: std.ArrayListUnmanaged([:0]const u8), - font_styles: font.CodepointResolver.StyleStatus, - cursor_color: ?terminal.color.RGB, - cursor_invert: bool, - cursor_text: ?terminal.color.RGB, - cursor_opacity: f64, - background: terminal.color.RGB, - background_opacity: f64, - foreground: terminal.color.RGB, - selection_background: ?terminal.color.RGB, - selection_foreground: ?terminal.color.RGB, - invert_selection_fg_bg: bool, - bold_is_bright: bool, - min_contrast: f32, - padding_color: configpkg.WindowPaddingColor, - custom_shaders: configpkg.RepeatablePath, - links: link.Set, - - pub fn init( - alloc_gpa: Allocator, - config: *const configpkg.Config, - ) !DerivedConfig { - var arena = ArenaAllocator.init(alloc_gpa); - errdefer arena.deinit(); - const alloc = arena.allocator(); - - // Copy our shaders - const custom_shaders = try config.@"custom-shader".clone(alloc); - - // Copy our font features - const font_features = try config.@"font-feature".clone(alloc); - - // Get our font styles - var font_styles = font.CodepointResolver.StyleStatus.initFill(true); - font_styles.set(.bold, config.@"font-style-bold" != .false); - font_styles.set(.italic, config.@"font-style-italic" != .false); - font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); - - // Our link configs - const links = try link.Set.fromConfig( - alloc, - config.link.links.items, - ); - - const cursor_invert = config.@"cursor-invert-fg-bg"; - - return .{ - .background_opacity = @max(0, @min(1, config.@"background-opacity")), - .font_thicken = config.@"font-thicken", - .font_thicken_strength = config.@"font-thicken-strength", - .font_features = font_features.list, - .font_styles = font_styles, - - .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) - config.@"cursor-color".?.toTerminalRGB() - else - null, - - .cursor_invert = cursor_invert, - - .cursor_text = if (config.@"cursor-text") |txt| - txt.toTerminalRGB() - else - null, - - .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), - - .background = config.background.toTerminalRGB(), - .foreground = config.foreground.toTerminalRGB(), - .invert_selection_fg_bg = config.@"selection-invert-fg-bg", - .bold_is_bright = config.@"bold-is-bright", - .min_contrast = @floatCast(config.@"minimum-contrast"), - .padding_color = config.@"window-padding-color", - - .selection_background = if (config.@"selection-background") |bg| - bg.toTerminalRGB() - else - null, - - .selection_foreground = if (config.@"selection-foreground") |bg| - bg.toTerminalRGB() - else - null, - - .custom_shaders = custom_shaders, - .links = links, - - .arena = arena, - }; - } - - pub fn deinit(self: *DerivedConfig) void { - const alloc = self.arena.allocator(); - self.links.deinit(alloc); - self.arena.deinit(); - } -}; - -pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL { - // Create the initial font shaper - var shaper = try font.Shaper.init(alloc, .{ - .features = options.config.font_features.items, - }); - errdefer shaper.deinit(); - - // For the remainder of the setup we lock our font grid data because - // we're reading it. - const grid = options.font_grid; - grid.lock.lockShared(); - defer grid.lock.unlockShared(); - - var gl_state = try GLState.init(alloc, options.config, grid); - errdefer gl_state.deinit(); - - return OpenGL{ +pub fn init(alloc: Allocator, opts: rendererpkg.Options) !OpenGL { + return .{ .alloc = alloc, - .config = options.config, - .cells_bg = .{}, - .cells = .{}, - .grid_metrics = grid.metrics, - .size = options.size, - .gl_state = gl_state, - .font_grid = grid, - .font_shaper = shaper, - .font_shaper_cache = font.ShaperCache.init(), - .draw_background = options.config.background, - .focused = true, - .foreground_color = null, - .default_foreground_color = options.config.foreground, - .background_color = null, - .default_background_color = options.config.background, - .cursor_color = null, - .default_cursor_color = options.config.cursor_color, - .cursor_invert = options.config.cursor_invert, - .surface_mailbox = options.surface_mailbox, - .deferred_font_size = .{ .metrics = grid.metrics }, - .deferred_config = .{}, + .blending = opts.config.blending, }; } pub fn deinit(self: *OpenGL) void { - self.font_shaper.deinit(); - self.font_shaper_cache.deinit(self.alloc); - - { - var it = self.images.iterator(); - while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); - self.images.deinit(self.alloc); - } - self.image_placements.deinit(self.alloc); - - if (self.gl_state) |*v| v.deinit(self.alloc); - - self.cells.deinit(self.alloc); - self.cells_bg.deinit(self.alloc); - - self.config.deinit(); - self.* = undefined; } /// Returns the hints that we want for this pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { + _ = config; return .{ - .context_version_major = 3, - .context_version_minor = 3, + .context_version_major = MIN_VERSION_MAJOR, + .context_version_minor = MIN_VERSION_MINOR, .opengl_profile = .opengl_core_profile, .opengl_forward_compat = true, - .cocoa_graphics_switching = builtin.os.tag == .macos, - .cocoa_retina_framebuffer = true, - .transparent_framebuffer = config.@"background-opacity" < 1, + .transparent_framebuffer = true, }; } +fn glDebugMessageCallback( + src: gl.c.GLenum, + typ: gl.c.GLenum, + id: gl.c.GLuint, + severity: gl.c.GLenum, + len: gl.c.GLsizei, + msg: [*c]const gl.c.GLchar, + user_param: ?*const anyopaque, +) callconv(.c) void { + _ = user_param; + + const src_str: []const u8 = switch (src) { + gl.c.GL_DEBUG_SOURCE_API => "OpenGL API", + gl.c.GL_DEBUG_SOURCE_WINDOW_SYSTEM => "Window System", + gl.c.GL_DEBUG_SOURCE_SHADER_COMPILER => "Shader Compiler", + gl.c.GL_DEBUG_SOURCE_THIRD_PARTY => "Third Party", + gl.c.GL_DEBUG_SOURCE_APPLICATION => "User", + gl.c.GL_DEBUG_SOURCE_OTHER => "Other", + else => "Unknown", + }; + + const typ_str: []const u8 = switch (typ) { + gl.c.GL_DEBUG_TYPE_ERROR => "Error", + gl.c.GL_DEBUG_TYPE_DEPRECATED_BEHAVIOR => "Deprecated Behavior", + gl.c.GL_DEBUG_TYPE_UNDEFINED_BEHAVIOR => "Undefined Behavior", + gl.c.GL_DEBUG_TYPE_PORTABILITY => "Portability Issue", + gl.c.GL_DEBUG_TYPE_PERFORMANCE => "Performance Issue", + gl.c.GL_DEBUG_TYPE_MARKER => "Marker", + gl.c.GL_DEBUG_TYPE_PUSH_GROUP => "Group Push", + gl.c.GL_DEBUG_TYPE_POP_GROUP => "Group Pop", + gl.c.GL_DEBUG_TYPE_OTHER => "Other", + else => "Unknown", + }; + + const msg_str = msg[0..@intCast(len)]; + + (switch (severity) { + gl.c.GL_DEBUG_SEVERITY_HIGH => log.err( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + gl.c.GL_DEBUG_SEVERITY_MEDIUM => log.warn( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + gl.c.GL_DEBUG_SEVERITY_LOW => log.info( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + gl.c.GL_DEBUG_SEVERITY_NOTIFICATION => log.debug( + "[{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + else => log.warn( + "UNKNOWN SEVERITY [{d}] ({s}: {s}) {s}", + .{ id, src_str, typ_str, msg_str }, + ), + }); +} + +/// Prepares the provided GL context, loading it with glad. +fn prepareContext(getProcAddress: anytype) !void { + const version = try gl.glad.load(getProcAddress); + const major = gl.glad.versionMajor(@intCast(version)); + const minor = gl.glad.versionMinor(@intCast(version)); + errdefer gl.glad.unload(); + log.info("loaded OpenGL {}.{}", .{ major, minor }); + + // Enable debug output for the context. + try gl.enable(gl.c.GL_DEBUG_OUTPUT); + + // Register our debug message callback with the OpenGL context. + gl.glad.context.DebugMessageCallback.?(glDebugMessageCallback, null); + + // Enable SRGB framebuffer for linear blending support. + try gl.enable(gl.c.GL_FRAMEBUFFER_SRGB); + + if (major < MIN_VERSION_MAJOR or + (major == MIN_VERSION_MAJOR and minor < MIN_VERSION_MINOR)) + { + log.warn( + "OpenGL version is too old. Ghostty requires OpenGL {d}.{d}", + .{ MIN_VERSION_MAJOR, MIN_VERSION_MINOR }, + ); + return error.OpenGLOutdated; + } +} + /// This is called early right after surface creation. pub fn surfaceInit(surface: *apprt.Surface) !void { // Treat this like a thread entry @@ -455,20 +162,8 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { switch (apprt.runtime) { else => @compileError("unsupported app runtime for OpenGL"), - apprt.gtk => { - // GTK uses global OpenGL context so we load from null. - const version = try gl.glad.load(null); - const major = gl.glad.versionMajor(@intCast(version)); - const minor = gl.glad.versionMinor(@intCast(version)); - errdefer gl.glad.unload(); - log.info("loaded OpenGL {}.{}", .{ major, minor }); - - // We require at least OpenGL 3.3 - if (major < 3 or (major == 3 and minor < 3)) { - log.warn("OpenGL version is too old. Ghostty requires OpenGL 3.3", .{}); - return error.OpenGLOutdated; - } - }, + // GTK uses global OpenGL context so we load from null. + apprt.gtk => try prepareContext(null), apprt.glfw => try self.threadEnter(surface), @@ -489,69 +184,19 @@ pub fn surfaceInit(surface: *apprt.Surface) !void { // } } -/// This is called just prior to spinning up the renderer thread for -/// final main thread setup requirements. +/// This is called just prior to spinning up the renderer +/// thread for final main thread setup requirements. pub fn finalizeSurfaceInit(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; _ = surface; - // For GLFW, we grabbed the OpenGL context in surfaceInit and we - // need to release it before we start the renderer thread. + // For GLFW, we grabbed the OpenGL context in surfaceInit and + // we need to release it before we start the renderer thread. if (apprt.runtime == apprt.glfw) { glfw.makeContextCurrent(null); } } -/// Called when the OpenGL context is made invalid, so we need to free -/// all previous resources and stop rendering. -pub fn displayUnrealized(self: *OpenGL) void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - if (self.gl_state) |*v| { - v.deinit(self.alloc); - self.gl_state = null; - } -} - -/// Called when the OpenGL is ready to be initialized. -pub fn displayRealize(self: *OpenGL) !void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Make our new state - var gl_state = gl_state: { - self.font_grid.lock.lockShared(); - defer self.font_grid.lock.unlockShared(); - break :gl_state try GLState.init( - self.alloc, - self.config, - self.font_grid, - ); - }; - errdefer gl_state.deinit(); - - // Unrealize if we have to - if (self.gl_state) |*v| v.deinit(self.alloc); - - // Set our new state - self.gl_state = gl_state; - - // Make sure we invalidate all the fields so that we - // reflush everything - self.gl_cells_size = 0; - self.gl_cells_written = 0; - self.texture_grayscale_modified = 0; - self.texture_color_modified = 0; - self.texture_grayscale_resized = 0; - self.texture_color_resized = 0; - - // We need to reset our uniforms - self.deferred_screen_size = .{ .size = self.size }; - self.deferred_font_size = .{ .metrics = self.grid_metrics }; - self.deferred_config = .{}; -} - /// Callback called by renderer.Thread when it begins. pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void { _ = self; @@ -568,22 +213,17 @@ pub fn threadEnter(self: *const OpenGL, surface: *apprt.Surface) !void { apprt.glfw => { // We need to make the OpenGL context current. OpenGL requires - // that a single thread own the a single OpenGL context (if any). This - // ensures that the context switches over to our thread. Important: - // the prior thread MUST have detached the context prior to calling - // this entrypoint. + // that a single thread own the a single OpenGL context (if any). + // This ensures that the context switches over to our thread. + // Important: the prior thread MUST have detached the context + // prior to calling this entrypoint. glfw.makeContextCurrent(surface.window); errdefer glfw.makeContextCurrent(null); glfw.swapInterval(1); // Load OpenGL bindings. This API is context-aware so this sets // a threadlocal context for these pointers. - const version = try gl.glad.load(&glfw.getProcAddress); - errdefer gl.glad.unload(); - log.info("loaded OpenGL {}.{}", .{ - gl.glad.versionMajor(@intCast(version)), - gl.glad.versionMinor(@intCast(version)), - }); + try prepareContext(&glfw.getProcAddress); }, apprt.embedded => { @@ -617,2068 +257,149 @@ pub fn threadExit(self: *const OpenGL) void { } } -/// True if our renderer has animations so that a higher frequency -/// timer is used. -pub fn hasAnimations(self: *const OpenGL) bool { - const state = self.gl_state orelse return false; - return state.custom != null; -} - -/// See Metal -pub fn hasVsync(self: *const OpenGL) bool { +pub fn displayRealized(self: *const OpenGL) void { _ = self; - // OpenGL currently never has vsync - return false; -} - -/// See Metal. -pub fn markDirty(self: *OpenGL) void { - // Do nothing, we don't have dirty tracking yet. - _ = self; -} - -/// Callback when the focus changes for the terminal this is rendering. -/// -/// Must be called on the render thread. -pub fn setFocus(self: *OpenGL, focus: bool) !void { - self.focused = focus; -} - -/// Callback when the window is visible or occluded. -/// -/// Must be called on the render thread. -pub fn setVisible(self: *OpenGL, visible: bool) void { - _ = self; - _ = visible; -} - -/// Set the new font grid. -/// -/// Must be called on the render thread. -pub fn setFontGrid(self: *OpenGL, grid: *font.SharedGrid) void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Reset our font grid - self.font_grid = grid; - self.grid_metrics = grid.metrics; - self.texture_grayscale_modified = 0; - self.texture_grayscale_resized = 0; - self.texture_color_modified = 0; - self.texture_color_resized = 0; - - // Reset our shaper cache. If our font changed (not just the size) then - // the data in the shaper cache may be invalid and cannot be used, so we - // always clear the cache just in case. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Update our screen size because the font grid can affect grid - // metrics which update uniforms. - self.deferred_screen_size = .{ .size = self.size }; - - // Defer our GPU updates - self.deferred_font_size = .{ .metrics = grid.metrics }; -} - -/// The primary render callback that is completely thread-safe. -pub fn updateFrame( - self: *OpenGL, - surface: *apprt.Surface, - state: *renderer.State, - cursor_blink_visible: bool, -) !void { - _ = surface; - - // Data we extract out of the critical area. - const Critical = struct { - full_rebuild: bool, - gl_bg: terminal.color.RGB, - screen: terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style: ?renderer.CursorStyle, - color_palette: terminal.color.Palette, - }; - - // Update all our data as tightly as possible within the mutex. - var critical: Critical = critical: { - state.mutex.lock(); - defer state.mutex.unlock(); - - // If we're in a synchronized output state, we pause all rendering. - if (state.terminal.modes.get(.synchronized_output)) { - log.debug("synchronized output started, skipping render", .{}); - return; - } - - // Swap bg/fg if the terminal is reversed - const bg = self.background_color orelse self.default_background_color; - const fg = self.foreground_color orelse self.default_foreground_color; - defer { - if (self.background_color) |*c| { - c.* = bg; - } else { - self.default_background_color = bg; - } - - if (self.foreground_color) |*c| { - c.* = fg; - } else { - self.default_foreground_color = fg; - } - } - - if (state.terminal.modes.get(.reverse_colors)) { - if (self.background_color) |*c| { - c.* = fg; - } else { - self.default_background_color = fg; - } - - if (self.foreground_color) |*c| { - c.* = bg; - } else { - self.default_foreground_color = bg; - } - } - - // Get the viewport pin so that we can compare it to the current. - const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; - - // We used to share terminal state, but we've since learned through - // analysis that it is faster to copy the terminal state than to - // hold the lock while rebuilding GPU cells. - var screen_copy = try state.terminal.screen.clone( - self.alloc, - .{ .viewport = .{} }, - null, - ); - errdefer screen_copy.deinit(); - - // Whether to draw our cursor or not. - const cursor_style = if (state.terminal.flags.password_input) - .lock - else - renderer.cursorStyle( - state, - self.focused, - cursor_blink_visible, - ); - - // Get our preedit state - const preedit: ?renderer.State.Preedit = preedit: { - if (cursor_style == null) break :preedit null; - const p = state.preedit orelse break :preedit null; - break :preedit try p.clone(self.alloc); - }; - errdefer if (preedit) |p| p.deinit(self.alloc); - - // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. - // We only do this if the Kitty image state is dirty meaning only if - // it changes. - // - // If we have any virtual references, we must also rebuild our - // kitty state on every frame because any cell change can move - // an image. - if (state.terminal.screen.kitty_images.dirty or - self.image_virtual) - { - // prepKittyGraphics touches self.images which is also used - // in drawFrame so if we're drawing on a separate thread we need - // to lock this. - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - try self.prepKittyGraphics(state.terminal); - } - - // If we have any terminal dirty flags set then we need to rebuild - // the entire screen. This can be optimized in the future. - const full_rebuild: bool = rebuild: { - { - const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.flags.dirty); - if (v > 0) break :rebuild true; - } - { - const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?; - const v: Int = @bitCast(state.terminal.screen.dirty); - if (v > 0) break :rebuild true; - } - - // If our viewport changed then we need to rebuild the entire - // screen because it means we scrolled. If we have no previous - // viewport then we must rebuild. - const prev_viewport = self.cells_viewport orelse break :rebuild true; - if (!prev_viewport.eql(viewport_pin)) break :rebuild true; - - break :rebuild false; - }; - - // Reset the dirty flags in the terminal and screen. We assume - // that our rebuild will be successful since so we optimize for - // success and reset while we hold the lock. This is much easier - // than coordinating row by row or as changes are persisted. - state.terminal.flags.dirty = .{}; - state.terminal.screen.dirty = .{}; - { - var it = state.terminal.screen.pages.pageIterator( - .right_down, - .{ .screen = .{} }, - null, - ); - while (it.next()) |chunk| { - var dirty_set = chunk.node.data.dirtyBitSet(); - dirty_set.unsetAll(); - } - } - - // Update our viewport pin for dirty tracking - self.cells_viewport = viewport_pin; - - break :critical .{ - .full_rebuild = full_rebuild, - .gl_bg = self.background_color orelse self.default_background_color, - .screen = screen_copy, - .screen_type = state.terminal.active_screen, - .mouse = state.mouse, - .preedit = preedit, - .cursor_style = cursor_style, - .color_palette = state.terminal.color_palette.colors, - }; - }; - defer { - critical.screen.deinit(); - if (critical.preedit) |p| p.deinit(self.alloc); - } - - // Grab our draw mutex if we have it and update our data - { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Set our draw data - self.draw_background = critical.gl_bg; - - // Build our GPU cells - try self.rebuildCells( - critical.full_rebuild, - &critical.screen, - critical.screen_type, - critical.mouse, - critical.preedit, - critical.cursor_style, - &critical.color_palette, - ); - - // Notify our shaper we're done for the frame. For some shapers like - // CoreText this triggers off-thread cleanup logic. - self.font_shaper.endFrame(); - } -} - -/// This goes through the Kitty graphic placements and accumulates the -/// placements we need to render on our viewport. It also ensures that -/// the visible images are loaded on the GPU. -fn prepKittyGraphics( - self: *OpenGL, - t: *terminal.Terminal, -) !void { - const storage = &t.screen.kitty_images; - defer storage.dirty = false; - - // We always clear our previous placements no matter what because - // we rebuild them from scratch. - self.image_placements.clearRetainingCapacity(); - self.image_virtual = false; - - // Go through our known images and if there are any that are no longer - // in use then mark them to be freed. - // - // This never conflicts with the below because a placement can't - // reference an image that doesn't exist. - { - var it = self.images.iterator(); - while (it.next()) |kv| { - if (storage.imageById(kv.key_ptr.*) == null) { - kv.value_ptr.image.markForUnload(); - } - } - } - - // The top-left and bottom-right corners of our viewport in screen - // points. This lets us determine offsets and containment of placements. - const top = t.screen.pages.getTopLeft(.viewport); - const bot = t.screen.pages.getBottomRight(.viewport).?; - const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; - const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; - - // Go through the placements and ensure the image is loaded on the GPU. - var it = storage.placements.iterator(); - while (it.next()) |kv| { - // Find the image in storage - const p = kv.value_ptr; - - // Special logic based on location - switch (p.location) { - .pin => {}, - .virtual => { - // We need to mark virtual placements on our renderer so that - // we know to rebuild in more scenarios since cell changes can - // now trigger placement changes. - self.image_virtual = true; - - // We also continue out because virtual placements are - // only triggered by the unicode placeholder, not by the - // placement itself. - continue; - }, - } - - const image = storage.imageById(kv.key_ptr.image_id) orelse { - log.warn( - "missing image for placement, ignoring image_id={}", - .{kv.key_ptr.image_id}, - ); - continue; - }; - - try self.prepKittyPlacement(t, top_y, bot_y, &image, p); - } - - // If we have virtual placements then we need to scan for placeholders. - if (self.image_virtual) { - var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); - while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( - t, - &virtual_p, - ); - } - - // Sort the placements by their Z value. - std.mem.sortUnstable( - gl_image.Placement, - self.image_placements.items, - {}, - struct { - fn lessThan( - ctx: void, - lhs: gl_image.Placement, - rhs: gl_image.Placement, - ) bool { - _ = ctx; - return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); - } - }.lessThan, - ); - - // Find our indices. The values are sorted by z so we can find the - // first placement out of bounds to find the limits. - var bg_end: ?u32 = null; - var text_end: ?u32 = null; - const bg_limit = std.math.minInt(i32) / 2; - for (self.image_placements.items, 0..) |p, i| { - if (bg_end == null and p.z >= bg_limit) { - bg_end = @intCast(i); - } - if (text_end == null and p.z >= 0) { - text_end = @intCast(i); - } - } - - self.image_bg_end = bg_end orelse 0; - self.image_text_end = text_end orelse self.image_bg_end; -} - -fn prepKittyVirtualPlacement( - self: *OpenGL, - t: *terminal.Terminal, - p: *const terminal.kitty.graphics.unicode.Placement, -) !void { - const storage = &t.screen.kitty_images; - const image = storage.imageById(p.image_id) orelse { - log.warn( - "missing image for virtual placement, ignoring image_id={}", - .{p.image_id}, - ); - return; - }; - - const rp = p.renderPlacement( - storage, - &image, - self.grid_metrics.cell_width, - self.grid_metrics.cell_height, - ) catch |err| { - log.warn("error rendering virtual placement err={}", .{err}); - return; - }; - - // If our placement is zero sized then we don't do anything. - if (rp.dest_width == 0 or rp.dest_height == 0) return; - - const viewport: terminal.point.Point = t.screen.pages.pointFromPin( - .viewport, - rp.top_left, - ) orelse { - // This is unreachable with virtual placements because we should - // only ever be looking at virtual placements that are in our - // viewport in the renderer and virtual placements only ever take - // up one row. - unreachable; - }; - - // Send our image to the GPU and store the placement for rendering. - try self.prepKittyImage(&image); - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rp.top_left.x), - .y = @intCast(viewport.viewport.y), - .z = -1, - .width = rp.dest_width, - .height = rp.dest_height, - .cell_offset_x = rp.offset_x, - .cell_offset_y = rp.offset_y, - .source_x = rp.source_x, - .source_y = rp.source_y, - .source_width = rp.source_width, - .source_height = rp.source_height, - }); -} - -fn prepKittyPlacement( - self: *OpenGL, - t: *terminal.Terminal, - top_y: u32, - bot_y: u32, - image: *const terminal.kitty.graphics.Image, - p: *const terminal.kitty.graphics.ImageStorage.Placement, -) !void { - // Get the rect for the placement. If this placement doesn't have - // a rect then its virtual or something so skip it. - const rect = p.rect(image.*, t) orelse return; - - // This is expensive but necessary. - const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; - const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; - - // If the selection isn't within our viewport then skip it. - if (img_top_y > bot_y) return; - if (img_bot_y < top_y) return; - - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - try self.prepKittyImage(image); - - // Calculate the dimensions of our image, taking in to - // account the rows / columns specified by the placement. - const dest_size = p.calculatedSize(image.*, t); - - // Calculate the source rectangle - const source_x = @min(image.width, p.source_x); - const source_y = @min(image.height, p.source_y); - const source_width = if (p.source_width > 0) - @min(image.width - source_x, p.source_width) - else - image.width; - const source_height = if (p.source_height > 0) - @min(image.height - source_y, p.source_height) - else - image.height; - - // Get the viewport-relative Y position of the placement. - const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); - - // Accumulate the placement - if (dest_size.width > 0 and dest_size.height > 0) { - try self.image_placements.append(self.alloc, .{ - .image_id = image.id, - .x = @intCast(rect.top_left.x), - .y = y_pos, - .z = p.z, - .width = dest_size.width, - .height = dest_size.height, - .cell_offset_x = p.x_offset, - .cell_offset_y = p.y_offset, - .source_x = source_x, - .source_y = source_y, - .source_width = source_width, - .source_height = source_height, - }); - } -} - -fn prepKittyImage( - self: *OpenGL, - image: *const terminal.kitty.graphics.Image, -) !void { - // We need to prep this image for upload if it isn't in the cache OR - // it is in the cache but the transmit time doesn't match meaning this - // image is different. - const gop = try self.images.getOrPut(self.alloc, image.id); - if (gop.found_existing and - gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) - { - return; - } - - // Copy the data into the pending state. - const data = try self.alloc.dupe(u8, image.data); - errdefer self.alloc.free(data); - - // Store it in the map - const pending: Image.Pending = .{ - .width = image.width, - .height = image.height, - .data = data.ptr, - }; - - const new_image: Image = switch (image.format) { - .gray => .{ .pending_gray = pending }, - .gray_alpha => .{ .pending_gray_alpha = pending }, - .rgb => .{ .pending_rgb = pending }, - .rgba => .{ .pending_rgba = pending }, - .png => unreachable, // should be decoded by now - }; - - if (!gop.found_existing) { - gop.value_ptr.* = .{ - .image = new_image, - .transmit_time = undefined, - }; - } else { - try gop.value_ptr.image.markForReplace( - self.alloc, - new_image, - ); - } - - gop.value_ptr.transmit_time = image.transmit_time; -} - -/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a -/// slow operation but ensures that the GPU state exactly matches the CPU state. -/// In steady-state operation, we use some GPU tricks to send down stale data -/// that is ignored. This accumulates more memory; rebuildCells clears it. -/// -/// Note this doesn't have to typically be manually called. Internally, -/// the renderer will do this when it needs more memory space. -pub fn rebuildCells( - self: *OpenGL, - rebuild: bool, - screen: *terminal.Screen, - screen_type: terminal.ScreenType, - mouse: renderer.State.Mouse, - preedit: ?renderer.State.Preedit, - cursor_style_: ?renderer.CursorStyle, - color_palette: *const terminal.color.Palette, -) !void { - _ = screen_type; - - // Bg cells at most will need space for the visible screen size - self.cells_bg.clearRetainingCapacity(); - self.cells.clearRetainingCapacity(); - - // Create an arena for all our temporary allocations while rebuilding - var arena = ArenaAllocator.init(self.alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // We've written no data to the GPU, refresh it all - self.gl_cells_written = 0; - - // Create our match set for the links. - var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( - arena_alloc, - screen, - mouse_pt, - mouse.mods, - ) else .{}; - - // Determine our x/y range for preedit. We don't want to render anything - // here because we will render the preedit separately. - const preedit_range: ?struct { - y: terminal.size.CellCountInt, - x: [2]terminal.size.CellCountInt, - cp_offset: usize, - } = if (preedit) |preedit_v| preedit: { - const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); - break :preedit .{ - .y = screen.cursor.y, - .x = .{ range.start, range.end }, - .cp_offset = range.cp_offset, - }; - } else null; - - // These are all the foreground cells underneath the cursor. - // - // We keep track of these so that we can invert the colors and move them - // in front of the block cursor so that the character remains visible. - // - // We init with a capacity of 4 to account for decorations such - // as underline and strikethrough, as well as combining chars. - var cursor_cells = try std.ArrayListUnmanaged(CellProgram.Cell).initCapacity(arena_alloc, 4); - defer cursor_cells.deinit(arena_alloc); - - if (rebuild) { - switch (self.config.padding_color) { - .background => {}, - - .extend, .@"extend-always" => { - self.padding_extend_top = true; - self.padding_extend_bottom = true; - }, - } - } - - const grid_size = self.size.grid(); - - // We rebuild the cells row-by-row because we do font shaping by row. - var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); - // If our cell contents buffer is shorter than the screen viewport, - // we render the rows that fit, starting from the bottom. If instead - // the viewport is shorter than the cell contents buffer, we align - // the top of the viewport with the top of the contents buffer. - var y: terminal.size.CellCountInt = @min( - screen.pages.rows, - grid_size.rows, - ); - while (row_it.next()) |row| { - // The viewport may have more rows than our cell contents, - // so we need to break from the loop early if we hit y = 0. - if (y == 0) break; - - y -= 1; - - // True if we want to do font shaping around the cursor. We want to - // do font shaping as long as the cursor is enabled. - const shape_cursor = screen.viewportIsBottom() and - y == screen.cursor.y; - - // If this is the row with our cursor, then we may have to modify - // the cell with the cursor. - const start_i: usize = self.cells.items.len; - defer if (shape_cursor and cursor_style_ == .block) { - const x = screen.cursor.x; - const wide = row.cells(.all)[x].wide; - const min_x = switch (wide) { - .narrow, .spacer_head, .wide => x, - .spacer_tail => x -| 1, - }; - const max_x = switch (wide) { - .narrow, .spacer_head, .spacer_tail => x, - .wide => x +| 1, - }; - for (self.cells.items[start_i..]) |cell| { - if (cell.grid_col < min_x or cell.grid_col > max_x) continue; - if (cell.mode.isFg()) { - cursor_cells.append(arena_alloc, cell) catch { - // We silently ignore if this fails because - // worst case scenario some combining glyphs - // aren't visible under the cursor '\_('-')_/' - }; - } - } - }; - - // We need to get this row's selection if there is one for proper - // run splitting. - const row_selection = sel: { - const sel = screen.selection orelse break :sel null; - const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse - break :sel null; - break :sel sel.containedRow(screen, pin) orelse null; - }; - - // On primary screen, we still apply vertical padding extension - // under certain conditions we feel are safe. This helps make some - // scenarios look better while avoiding scenarios we know do NOT look - // good. - switch (self.config.padding_color) { - // These already have the correct values set above. - .background, .@"extend-always" => {}, - - // Apply heuristics for padding extension. - .extend => if (y == 0) { - self.padding_extend_top = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - } else if (y == self.size.grid().rows - 1) { - self.padding_extend_bottom = !row.neverExtendBg( - color_palette, - self.background_color orelse self.default_background_color, - ); - }, - } - - // Iterator of runs for shaping. - var run_iter = self.font_shaper.runIterator( - self.font_grid, - screen, - row, - row_selection, - if (shape_cursor) screen.cursor.x else null, - ); - var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); - var shaper_cells: ?[]const font.shape.Cell = null; - var shaper_cells_i: usize = 0; - - const row_cells_all = row.cells(.all); - - // If our viewport is wider than our cell contents buffer, - // we still only process cells up to the width of the buffer. - const row_cells = row_cells_all[0..@min(row_cells_all.len, grid_size.columns)]; - - for (row_cells, 0..) |*cell, x| { - // If this cell falls within our preedit range then we - // skip this because preedits are setup separately. - if (preedit_range) |range| preedit: { - // We're not on the preedit line, no actions necessary. - if (range.y != y) break :preedit; - // We're before the preedit range, no actions necessary. - if (x < range.x[0]) break :preedit; - // We're in the preedit range, skip this cell. - if (x <= range.x[1]) continue; - // After exiting the preedit range we need to catch - // the run position up because of the missed cells. - // In all other cases, no action is necessary. - if (x != range.x[1] + 1) break :preedit; - - // Step the run iterator until we find a run that ends - // after the current cell, which will be the soonest run - // that might contain glyphs for our cell. - while (shaper_run) |run| { - if (run.offset + run.cells > x) break; - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - const run = shaper_run orelse break :preedit; - - // If we haven't shaped this run, do so now. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - // Advance our index until we reach or pass - // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x < x) { - shaper_cells_i += 1; - } - } - - const wide = cell.wide; - - const style = row.style(cell); - - const cell_pin: terminal.Pin = cell: { - var copy = row; - copy.x = @intCast(x); - break :cell copy; - }; - - // True if this cell is selected - const selected: bool = if (screen.selection) |sel| - sel.contains(screen, .{ - .node = row.node, - .y = row.y, - .x = @intCast( - // Spacer tails should show the selection - // state of the wide cell they belong to. - if (wide == .spacer_tail) - x -| 1 - else - x, - ), - }) - else - false; - - const bg_style = style.bg(cell, color_palette); - const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; - - // The final background color for the cell. - const bg = bg: { - if (selected) { - break :bg if (self.config.invert_selection_fg_bg) - if (style.flags.inverse) - // Cell is selected with invert selection fg/bg - // enabled, and the cell has the inverse style - // flag, so they cancel out and we get the normal - // bg color. - bg_style - else - // If it doesn't have the inverse style - // flag then we use the fg color instead. - fg_style - else - // If we don't have invert selection fg/bg set then we - // just use the selection background if set, otherwise - // the default fg color. - break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; - } - - // Not selected - break :bg if (style.flags.inverse != isCovering(cell.codepoint())) - // Two cases cause us to invert (use the fg color as the bg) - // - The "inverse" style flag. - // - A "covering" glyph; we use fg for bg in that case to - // help make sure that padding extension works correctly. - // If one of these is true (but not the other) - // then we use the fg style color for the bg. - fg_style - else - // Otherwise they cancel out. - bg_style; - }; - - const fg = fg: { - if (selected and !self.config.invert_selection_fg_bg) { - // If we don't have invert selection fg/bg set - // then we just use the selection foreground if - // set, otherwise the default bg color. - break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; - } - - // Whether we need to use the bg color as our fg color: - // - Cell is inverted and not selected - // - Cell is selected and not inverted - // Note: if selected then invert sel fg / bg must be - // false since we separately handle it if true above. - break :fg if (style.flags.inverse != selected) - bg_style orelse self.background_color orelse self.default_background_color - else - fg_style; - }; - - // Foreground alpha for this cell. - const alpha: u8 = if (style.flags.faint) 175 else 255; - - // If the cell has a background color, set it. - const bg_color: [4]u8 = if (bg) |rgb| bg: { - // Determine our background alpha. If we have transparency configured - // then this is dynamic depending on some situations. This is all - // in an attempt to make transparency look the best for various - // situations. See inline comments. - const bg_alpha: u8 = bg_alpha: { - const default: u8 = 255; - - if (self.config.background_opacity >= 1) break :bg_alpha default; - - // If we're selected, we do not apply background opacity - if (selected) break :bg_alpha default; - - // If we're reversed, do not apply background opacity - if (style.flags.inverse) break :bg_alpha default; - - // If we have a background and its not the default background - // then we apply background opacity - if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) { - break :bg_alpha default; - } - - // We apply background opacity. - var bg_alpha: f64 = @floatFromInt(default); - bg_alpha *= self.config.background_opacity; - bg_alpha = @ceil(bg_alpha); - break :bg_alpha @intFromFloat(bg_alpha); - }; - - try self.cells_bg.append(self.alloc, .{ - .mode = .bg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = cell.gridWidth(), - .glyph_x = 0, - .glyph_y = 0, - .glyph_width = 0, - .glyph_height = 0, - .glyph_offset_x = 0, - .glyph_offset_y = 0, - .r = rgb.r, - .g = rgb.g, - .b = rgb.b, - .a = bg_alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - }); - - break :bg .{ - rgb.r, rgb.g, rgb.b, bg_alpha, - }; - } else .{ - self.draw_background.r, - self.draw_background.g, - self.draw_background.b, - @intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))), - }; - - // If the invisible flag is set on this cell then we - // don't need to render any foreground elements, so - // we just skip all glyphs with this x coordinate. - // - // NOTE: This behavior matches xterm. Some other terminal - // emulators, e.g. Alacritty, still render text decorations - // and only make the text itself invisible. The decision - // has been made here to match xterm's behavior for this. - if (style.flags.invisible) { - continue; - } - - // Give links a single underline, unless they already have - // an underline, in which case use a double underline to - // distinguish them. - const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) - if (style.flags.underline == .single) - .double - else - .single - else - style.flags.underline; - - // We draw underlines first so that they layer underneath text. - // This improves readability when a colored underline is used - // which intersects parts of the text (descenders). - if (underline != .none) self.addUnderline( - @intCast(x), - @intCast(y), - underline, - style.underlineColor(color_palette) orelse fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding underline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - if (style.flags.overline) self.addOverline( - @intCast(x), - @intCast(y), - fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding overline to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - - // If we're at or past the end of our shaper run then - // we need to get the next run from the run iterator. - if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { - shaper_run = try run_iter.next(self.alloc); - shaper_cells = null; - shaper_cells_i = 0; - } - - if (shaper_run) |run| glyphs: { - // If we haven't shaped this run yet, do so. - shaper_cells = shaper_cells orelse - // Try to read the cells from the shaping cache if we can. - self.font_shaper_cache.get(run) orelse - cache: { - // Otherwise we have to shape them. - const cells = try self.font_shaper.shape(run); - - // Try to cache them. If caching fails for any reason we - // continue because it is just a performance optimization, - // not a correctness issue. - self.font_shaper_cache.put( - self.alloc, - run, - cells, - ) catch |err| { - log.warn( - "error caching font shaping results err={}", - .{err}, - ); - }; - - // The cells we get from direct shaping are always owned - // by the shaper and valid until the next shaping call so - // we can safely use them. - break :cache cells; - }; - - const cells = shaper_cells orelse break :glyphs; - - // If there are no shaper cells for this run, ignore it. - // This can occur for runs of empty cells, and is fine. - if (cells.len == 0) break :glyphs; - - // If we encounter a shaper cell to the left of the current - // cell then we have some problems. This logic relies on x - // position monotonically increasing. - assert(cells[shaper_cells_i].x >= x); - - // NOTE: An assumption is made here that a single cell will never - // be present in more than one shaper run. If that assumption is - // violated, this logic breaks. - - while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ - shaper_cells_i += 1; - }) { - self.addGlyph( - @intCast(x), - @intCast(y), - cell_pin, - cells[shaper_cells_i], - shaper_run.?, - fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding glyph to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Finally, draw a strikethrough if necessary. - if (style.flags.strikethrough) self.addStrikethrough( - @intCast(x), - @intCast(y), - fg, - alpha, - bg_color, - ) catch |err| { - log.warn( - "error adding strikethrough to cell, will be invalid x={} y={}, err={}", - .{ x, y, err }, - ); - }; - } - } - - // Add the cursor at the end so that it overlays everything. If we have - // a cursor cell then we invert the colors on that and add it in so - // that we can always see it. - if (cursor_style_) |cursor_style| cursor_style: { - // If we have a preedit, we try to render the preedit text on top - // of the cursor. - if (preedit) |preedit_v| { - const range = preedit_range.?; - var x = range.x[0]; - for (preedit_v.codepoints[range.cp_offset..]) |cp| { - self.addPreeditCell(cp, x, range.y) catch |err| { - log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ - x, - range.y, - err, - }); - }; - - x += if (cp.wide) 2 else 1; - } - - // Preedit hides the cursor - break :cursor_style; - } - - const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { - if (self.cursor_invert) { - // Use the foreground color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :color if (sty.flags.inverse) - // If the cell is reversed, use background color instead. - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) - else - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); - } else { - break :color self.foreground_color orelse self.default_foreground_color; - } - }; - - _ = try self.addCursor(screen, cursor_style, cursor_color); - for (cursor_cells.items) |*cell| { - if (cell.mode.isFg() and cell.mode != .fg_color) { - const cell_color = if (self.cursor_invert) blk: { - // Use the background color from the cell under the cursor, if any. - const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); - break :blk if (sty.flags.inverse) - // If the cell is reversed, use foreground color instead. - (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) - else - (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); - } else if (self.config.cursor_text) |txt| - txt - else - self.background_color orelse self.default_background_color; - - cell.r = cell_color.r; - cell.g = cell_color.g; - cell.b = cell_color.b; - cell.a = 255; - } - try self.cells.append(self.alloc, cell.*); - } - } - - // Free up memory, generally in case where surface has shrunk. - // If more than half of the capacity is unused, remove all unused capacity. - if (self.cells.items.len * 2 < self.cells.capacity) { - self.cells.shrinkAndFree(self.alloc, self.cells.items.len); - } - if (self.cells_bg.items.len * 2 < self.cells_bg.capacity) { - self.cells_bg.shrinkAndFree(self.alloc, self.cells_bg.items.len); - } - - // Some debug mode safety checks - if (std.debug.runtime_safety) { - for (self.cells_bg.items) |cell| assert(cell.mode == .bg); - for (self.cells.items) |cell| assert(cell.mode != .bg); - } -} - -fn addPreeditCell( - self: *OpenGL, - cp: renderer.State.Preedit.Codepoint, - x: usize, - y: usize, -) !void { - // Preedit is rendered inverted - const bg = self.foreground_color orelse self.default_foreground_color; - const fg = self.background_color orelse self.default_background_color; - - // Render the glyph for our preedit text - const render_ = self.font_grid.renderCodepoint( - self.alloc, - @intCast(cp.codepoint), - .regular, - .text, - .{ .grid_metrics = self.grid_metrics }, - ) catch |err| { - log.warn("error rendering preedit glyph err={}", .{err}); - return; - }; - const render = render_ orelse { - log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); - return; - }; - - // Add our opaque background cell - try self.cells_bg.append(self.alloc, .{ - .mode = .bg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = if (cp.wide) 2 else 1, - .glyph_x = 0, - .glyph_y = 0, - .glyph_width = 0, - .glyph_height = 0, - .glyph_offset_x = 0, - .glyph_offset_y = 0, - .r = bg.r, - .g = bg.g, - .b = bg.b, - .a = 255, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - }); - - // Add our text - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = if (cp.wide) 2 else 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = fg.r, - .g = fg.g, - .b = fg.b, - .a = 255, - .bg_r = bg.r, - .bg_g = bg.g, - .bg_b = bg.b, - .bg_a = 255, - }); -} - -fn addCursor( - self: *OpenGL, - screen: *terminal.Screen, - cursor_style: renderer.CursorStyle, - cursor_color: terminal.color.RGB, -) !?*const CellProgram.Cell { - // Add the cursor. We render the cursor over the wide character if - // we're on the wide character tail. - const wide, const x = cell: { - // The cursor goes over the screen cursor position. - const cell = screen.cursor.page_cell; - if (cell.wide != .spacer_tail or screen.cursor.x == 0) - break :cell .{ cell.wide == .wide, screen.cursor.x }; - - // If we're part of a wide character, we move the cursor back to - // the actual character. - const prev_cell = screen.cursorCellLeft(1); - break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; - }; - - const alpha: u8 = if (!self.focused) 255 else alpha: { - const alpha = 255 * self.config.cursor_opacity; - break :alpha @intFromFloat(@ceil(alpha)); - }; - - const render = switch (cursor_style) { - .block, - .block_hollow, - .bar, - .underline, - => render: { - const sprite: font.Sprite = switch (cursor_style) { - .block => .cursor_rect, - .block_hollow => .cursor_hollow_rect, - .bar => .cursor_bar, - .underline => .underline, - .lock => unreachable, - }; - - break :render self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return null; - }; - }, - - .lock => self.font_grid.renderCodepoint( - self.alloc, - 0xF023, // lock symbol - .regular, - .text, - .{ - .cell_width = if (wide) 2 else 1, - .grid_metrics = self.grid_metrics, - }, - ) catch |err| { - log.warn("error rendering cursor glyph err={}", .{err}); - return null; - } orelse { - // This should never happen because we embed nerd - // fonts so we just log and return instead of fallback. - log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); - return null; - }, - }; - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(screen.cursor.y), - .grid_width = if (wide) 2 else 1, - .r = cursor_color.r, - .g = cursor_color.g, - .b = cursor_color.b, - .a = alpha, - .bg_r = 0, - .bg_g = 0, - .bg_b = 0, - .bg_a = 0, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - }); - - return &self.cells.items[self.cells.items.len - 1]; -} - -/// Add an underline decoration to the specified cell -fn addUnderline( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - style: terminal.Attribute.Underline, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const sprite: font.Sprite = switch (style) { - .none => unreachable, - .single => .underline, - .double => .underline_double, - .dotted => .underline_dotted, - .dashed => .underline_dashed, - .curly => .underline_curly, - }; - - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(sprite), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -/// Add an overline decoration to the specified cell -fn addOverline( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.overline), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -/// Add a strikethrough decoration to the specified cell -fn addStrikethrough( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const render = try self.font_grid.renderGlyph( - self.alloc, - font.sprite_index, - @intFromEnum(font.Sprite.strikethrough), - .{ - .cell_width = 1, - .grid_metrics = self.grid_metrics, - }, - ); - - try self.cells.append(self.alloc, .{ - .mode = .fg, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = 1, - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x, - .glyph_offset_y = render.glyph.offset_y, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -// Add a glyph to the specified cell. -fn addGlyph( - self: *OpenGL, - x: terminal.size.CellCountInt, - y: terminal.size.CellCountInt, - cell_pin: terminal.Pin, - shaper_cell: font.shape.Cell, - shaper_run: font.shape.TextRun, - color: terminal.color.RGB, - alpha: u8, - bg: [4]u8, -) !void { - const rac = cell_pin.rowAndCell(); - const cell = rac.cell; - - // Render - const render = try self.font_grid.renderGlyph( - self.alloc, - shaper_run.font_index, - shaper_cell.glyph_index, - .{ - .cell_width = if (cell.wide == .wide) 2 else 1, - .grid_metrics = self.grid_metrics, - .thicken = self.config.font_thicken, - .thicken_strength = self.config.font_thicken_strength, - }, - ); - - // If the glyph is 0 width or height, it will be invisible - // when drawn, so don't bother adding it to the buffer. - if (render.glyph.width == 0 or render.glyph.height == 0) { - return; - } - - // If we're rendering a color font, we use the color atlas - const mode: CellProgram.CellMode = switch (try fgMode( - render.presentation, - cell_pin, - )) { - .normal => .fg, - .color => .fg_color, - .constrained => .fg_constrained, - .powerline => .fg_powerline, - }; - - try self.cells.append(self.alloc, .{ - .mode = mode, - .grid_col = @intCast(x), - .grid_row = @intCast(y), - .grid_width = cell.gridWidth(), - .glyph_x = render.glyph.atlas_x, - .glyph_y = render.glyph.atlas_y, - .glyph_width = render.glyph.width, - .glyph_height = render.glyph.height, - .glyph_offset_x = render.glyph.offset_x + shaper_cell.x_offset, - .glyph_offset_y = render.glyph.offset_y + shaper_cell.y_offset, - .r = color.r, - .g = color.g, - .b = color.b, - .a = alpha, - .bg_r = bg[0], - .bg_g = bg[1], - .bg_b = bg[2], - .bg_a = bg[3], - }); -} - -/// Update the configuration. -pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void { - // We always redo the font shaper in case font features changed. We - // could check to see if there was an actual config change but this is - // easier and rare enough to not cause performance issues. - { - var font_shaper = try font.Shaper.init(self.alloc, .{ - .features = config.font_features.items, - }); - errdefer font_shaper.deinit(); - self.font_shaper.deinit(); - self.font_shaper = font_shaper; - } - - // We also need to reset the shaper cache so shaper info - // from the previous font isn't re-used for the new font. - const font_shaper_cache = font.ShaperCache.init(); - self.font_shaper_cache.deinit(self.alloc); - self.font_shaper_cache = font_shaper_cache; - - // Set our new colors - self.default_background_color = config.background; - self.default_foreground_color = config.foreground; - self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; - self.cursor_invert = config.cursor_invert; - - // Update our uniforms - self.deferred_config = .{}; - - self.config.deinit(); - self.config = config.*; -} - -/// Set the screen size for rendering. This will update the projection -/// used for the shader so that the scaling of the grid is correct. -pub fn setScreenSize( - self: *OpenGL, - size: renderer.Size, -) !void { - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - - // Store our screen size - self.size = size; - - // Defer our OpenGL updates - self.deferred_screen_size = .{ .size = size }; - - log.debug("screen size size={}", .{size}); -} - -/// Updates the font texture atlas if it is dirty. -fn flushAtlas(self: *OpenGL) !void { - const gl_state = self.gl_state orelse return; - try flushAtlasSingle( - &self.font_grid.lock, - gl_state.texture, - &self.font_grid.atlas_grayscale, - &self.texture_grayscale_modified, - &self.texture_grayscale_resized, - .red, - .red, - ); - try flushAtlasSingle( - &self.font_grid.lock, - gl_state.texture_color, - &self.font_grid.atlas_color, - &self.texture_color_modified, - &self.texture_color_resized, - .rgba, - .bgra, - ); -} - -/// Flush a single atlas, grabbing all necessary locks, checking for -/// changes, etc. -fn flushAtlasSingle( - lock: *std.Thread.RwLock, - texture: gl.Texture, - atlas: *font.Atlas, - modified: *usize, - resized: *usize, - internal_format: gl.Texture.InternalFormat, - format: gl.Texture.Format, -) !void { - // If the texture isn't modified we do nothing - const new_modified = atlas.modified.load(.monotonic); - if (new_modified <= modified.*) return; - - // If it is modified we need to grab a read-lock - lock.lockShared(); - defer lock.unlockShared(); - - var texbind = try texture.bind(.@"2D"); - defer texbind.unbind(); - - const new_resized = atlas.resized.load(.monotonic); - if (new_resized > resized.*) { - try texbind.image2D( - 0, - internal_format, - @intCast(atlas.size), - @intCast(atlas.size), - 0, - format, - .UnsignedByte, - atlas.data.ptr, - ); - - // Only update the resized number after successful resize - resized.* = new_resized; - } else { - try texbind.subImage2D( - 0, - 0, - 0, - @intCast(atlas.size), - @intCast(atlas.size), - format, - .UnsignedByte, - atlas.data.ptr, - ); - } - - // Update our modified tracker after successful update - modified.* = atlas.modified.load(.monotonic); -} - -/// Render renders the current cell state. This will not modify any of -/// the cells. -pub fn drawFrame(self: *OpenGL, surface: *apprt.Surface) !void { - // If we're in single-threaded more we grab a lock since we use shared data. - if (single_threaded_draw) self.draw_mutex.lock(); - defer if (single_threaded_draw) self.draw_mutex.unlock(); - const gl_state: *GLState = if (self.gl_state) |*v| v else return; - - // Go through our images and see if we need to setup any textures. - { - var image_it = self.images.iterator(); - while (image_it.next()) |kv| { - switch (kv.value_ptr.image) { - .ready => {}, - - .pending_gray, - .pending_gray_alpha, - .pending_rgb, - .pending_rgba, - .replace_gray, - .replace_gray_alpha, - .replace_rgb, - .replace_rgba, - => try kv.value_ptr.image.upload(self.alloc), - - .unload_pending, - .unload_replace, - .unload_ready, - => { - kv.value_ptr.image.deinit(self.alloc); - self.images.removeByPtr(kv.key_ptr); - }, - } - } - } - - // In the "OpenGL Programming Guide for Mac" it explains that: "When you - // use an NSOpenGLView object with OpenGL calls that are issued from a - // thread other than the main one, you must set up mutex locking." - // This locks the context and avoids crashes that can happen due to - // races with the underlying Metal layer that Apple is using to - // implement OpenGL. - const is_darwin = builtin.target.os.tag.isDarwin(); - const ogl = if (comptime is_darwin) @cImport({ - @cInclude("OpenGL/OpenGL.h"); - }) else {}; - const cgl_ctx = if (comptime is_darwin) ogl.CGLGetCurrentContext(); - if (comptime is_darwin) _ = ogl.CGLLockContext(cgl_ctx); - defer _ = if (comptime is_darwin) ogl.CGLUnlockContext(cgl_ctx); - - // If our viewport size doesn't match the saved screen size then - // we need to update it. We rely on this over setScreenSize because - // we can pull it directly from the OpenGL context instead of relying - // on the eventual message. - { - var viewport: [4]gl.c.GLint = undefined; - gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport); - const screen: renderer.ScreenSize = .{ - .width = @intCast(viewport[2]), - .height = @intCast(viewport[3]), - }; - if (!screen.equals(self.size.screen)) { - self.size.screen = screen; - self.deferred_screen_size = .{ .size = self.size }; - } - } - - // Draw our terminal cells - try self.drawCellProgram(gl_state); - - // Draw our custom shaders - if (gl_state.custom) |*custom_state| { - try self.drawCustomPrograms(custom_state); - } - - // Swap our window buffers switch (apprt.runtime) { - apprt.glfw => surface.window.swapBuffers(), - apprt.gtk => {}, - apprt.embedded => {}, - else => @compileError("unsupported runtime"), + apprt.gtk => prepareContext(null) catch |err| { + log.warn( + "Error preparing GL context in displayRealized, err={}", + .{err}, + ); + }, + + else => @compileError("only GTK should be calling displayRealized"), } } -/// Draw the custom shaders. -fn drawCustomPrograms(self: *OpenGL, custom_state: *custom.State) !void { +pub fn initShaders( + self: *const OpenGL, + alloc: Allocator, + custom_shaders: []const [:0]const u8, +) !shaders.Shaders { + _ = alloc; + return try shaders.Shaders.init( + self.alloc, + custom_shaders, + ); +} + +/// Get the current size of the runtime surface. +pub fn surfaceSize(self: *const OpenGL) !struct { width: u32, height: u32 } { _ = self; - assert(custom_state.programs.len > 0); - - // Bind our state that is global to all custom shaders - const custom_bind = try custom_state.bind(); - defer custom_bind.unbind(); - - // Setup the new frame - try custom_state.newFrame(); - - // Go through each custom shader and draw it. - for (custom_state.programs) |program| { - const bind = try program.bind(); - defer bind.unbind(); - try bind.draw(); - try custom_state.copyFramebuffer(); - } -} - -/// Runs the cell program (shaders) to draw the terminal grid. -fn drawCellProgram( - self: *OpenGL, - gl_state: *const GLState, -) !void { - // Try to flush our atlas, this will only do something if there - // are changes to the atlas. - try self.flushAtlas(); - - // If we have custom shaders, then we draw to the custom - // shader framebuffer. - const fbobind: ?gl.Framebuffer.Binding = fbobind: { - const state = gl_state.custom orelse break :fbobind null; - break :fbobind try state.fbo.bind(.framebuffer); + var viewport: [4]gl.c.GLint = undefined; + gl.glad.context.GetIntegerv.?(gl.c.GL_VIEWPORT, &viewport); + return .{ + .width = @intCast(viewport[2]), + .height = @intCast(viewport[3]), }; - defer if (fbobind) |v| v.unbind(); +} - // Clear the surface - gl.clearColor( - @floatCast(@as(f32, @floatFromInt(self.draw_background.r)) / 255 * self.config.background_opacity), - @floatCast(@as(f32, @floatFromInt(self.draw_background.g)) / 255 * self.config.background_opacity), - @floatCast(@as(f32, @floatFromInt(self.draw_background.b)) / 255 * self.config.background_opacity), - @floatCast(self.config.background_opacity), - ); - gl.clear(gl.c.GL_COLOR_BUFFER_BIT); +/// Initialize a new render target which can be presented by this API. +pub fn initTarget(self: *const OpenGL, width: usize, height: usize) !Target { + return Target.init(.{ + .internal_format = if (self.blending.isLinear()) .srgba else .rgba, + .width = width, + .height = height, + }); +} - // If we have deferred operations, run them. - if (self.deferred_screen_size) |v| { - try v.apply(self); - self.deferred_screen_size = null; - } - if (self.deferred_font_size) |v| { - try v.apply(self); - self.deferred_font_size = null; - } - if (self.deferred_config) |v| { - try v.apply(self); - self.deferred_config = null; - } +/// Present the provided target. +pub fn present(self: *OpenGL, target: Target) !void { + // In order to present a target we blit it to the default framebuffer. - // Apply our padding extension fields - { - const program = gl_state.cell_program; - const bind = try program.program.use(); - defer bind.unbind(); - try program.program.setUniform( - "padding_vertical_top", - self.padding_extend_top, - ); - try program.program.setUniform( - "padding_vertical_bottom", - self.padding_extend_bottom, - ); - } + // We disable GL_FRAMEBUFFER_SRGB while doing this blit, otherwise the + // values may be linearized as they're copied, but even though the draw + // framebuffer has a linear internal format, the values in it should be + // sRGB, not linear! + try gl.disable(gl.c.GL_FRAMEBUFFER_SRGB); + defer gl.enable(gl.c.GL_FRAMEBUFFER_SRGB) catch |err| { + log.err("Error re-enabling GL_FRAMEBUFFER_SRGB, err={}", .{err}); + }; - // Draw background images first - try self.drawImages( - gl_state, - self.image_placements.items[0..self.image_bg_end], + // Bind the target for reading. + const fbobind = try target.framebuffer.bind(.read); + defer fbobind.unbind(); + + // Blit + gl.glad.context.BlitFramebuffer.?( + 0, + 0, + @intCast(target.width), + @intCast(target.height), + 0, + 0, + @intCast(target.width), + @intCast(target.height), + gl.c.GL_COLOR_BUFFER_BIT, + gl.c.GL_NEAREST, ); - // Draw our background - try self.drawCells(gl_state, self.cells_bg); + // Keep track of this target in case we need to repeat it. + self.last_target = target; +} - // Then draw images under text - try self.drawImages( - gl_state, - self.image_placements.items[self.image_bg_end..self.image_text_end], - ); +/// Present the last presented target again. +pub fn repeat(self: *OpenGL) !void { + if (self.last_target) |target| try self.present(target); +} - // Drag foreground - try self.drawCells(gl_state, self.cells); +/// Returns the options to use when constructing buffers. +pub inline fn bufferOptions(self: OpenGL) bufferpkg.Options { + _ = self; + return .{ + .target = .array, + .usage = .dynamic_draw, + }; +} - // Draw remaining images - try self.drawImages( - gl_state, - self.image_placements.items[self.image_text_end..], +pub const instanceBufferOptions = bufferOptions; +pub const uniformBufferOptions = bufferOptions; +pub const fgBufferOptions = bufferOptions; +pub const bgBufferOptions = bufferOptions; +pub const imageBufferOptions = bufferOptions; + +/// Returns the options to use when constructing textures. +pub inline fn textureOptions(self: OpenGL) Texture.Options { + _ = self; + return .{ + .format = .rgba, + .internal_format = .srgba, + .target = .@"2D", + }; +} + +/// Initializes a Texture suitable for the provided font atlas. +pub fn initAtlasTexture(self: *const OpenGL, atlas: *const font.Atlas) !Texture { + _ = self; + const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat = + switch (atlas.format) { + .grayscale => .{ .red, .red }, + .rgba => .{ .rgba, .srgba }, + else => @panic("unsupported atlas format for OpenGL texture"), + }; + + return Texture.init( + .{ + .format = format, + .internal_format = internal_format, + .target = .Rectangle, + }, + atlas.size, + atlas.size, + null, ); } -/// Runs the image program to draw images. -fn drawImages( - self: *OpenGL, - gl_state: *const GLState, - placements: []const gl_image.Placement, -) !void { - if (placements.len == 0) return; - - // Bind our image program - const bind = try gl_state.image_program.bind(); - defer bind.unbind(); - - // For each placement we need to bind the texture - for (placements) |p| { - // Get the image and image texture - const image = self.images.get(p.image_id) orelse { - log.warn("image not found for placement image_id={}", .{p.image_id}); - continue; - }; - - const texture = switch (image.image) { - .ready => |t| t, - else => { - log.warn("image not ready for placement image_id={}", .{p.image_id}); - continue; - }, - }; - - // Bind the texture - try gl.Texture.active(gl.c.GL_TEXTURE0); - var texbind = try texture.bind(.@"2D"); - defer texbind.unbind(); - - // Setup our data - try bind.vbo.setData(ImageProgram.Input{ - .grid_col = p.x, - .grid_row = p.y, - .cell_offset_x = p.cell_offset_x, - .cell_offset_y = p.cell_offset_y, - .source_x = p.source_x, - .source_y = p.source_y, - .source_width = p.source_width, - .source_height = p.source_height, - .dest_width = p.width, - .dest_height = p.height, - }, .static_draw); - - try gl.drawElementsInstanced( - gl.c.GL_TRIANGLES, - 6, - gl.c.GL_UNSIGNED_BYTE, - 1, - ); - } +/// Begin a frame. +pub inline fn beginFrame( + self: *const OpenGL, + /// Once the frame has been completed, the `frameCompleted` method + /// on the renderer is called with the health status of the frame. + renderer: *Renderer, + /// The target is presented via the provided renderer's API when completed. + target: *Target, +) !Frame { + _ = self; + return try Frame.begin(.{}, renderer, target); } - -/// Loads some set of cell data into our buffer and issues a draw call. -/// This expects all the OpenGL state to be setup. -/// -/// Future: when we move to multiple shaders, this will go away and -/// we'll have a draw call per-shader. -fn drawCells( - self: *OpenGL, - gl_state: *const GLState, - cells: std.ArrayListUnmanaged(CellProgram.Cell), -) !void { - // If we have no cells to render, then we render nothing. - if (cells.items.len == 0) return; - - // Todo: get rid of this completely - self.gl_cells_written = 0; - - // Bind our cell program state, buffers - const bind = try gl_state.cell_program.bind(); - defer bind.unbind(); - - // Bind our textures - try gl.Texture.active(gl.c.GL_TEXTURE0); - var texbind = try gl_state.texture.bind(.@"2D"); - defer texbind.unbind(); - - try gl.Texture.active(gl.c.GL_TEXTURE1); - var texbind1 = try gl_state.texture_color.bind(.@"2D"); - defer texbind1.unbind(); - - // Our allocated buffer on the GPU is smaller than our capacity. - // We reallocate a new buffer with the full new capacity. - if (self.gl_cells_size < cells.capacity) { - log.info("reallocating GPU buffer old={} new={}", .{ - self.gl_cells_size, - cells.capacity, - }); - - try bind.vbo.setDataNullManual( - @sizeOf(CellProgram.Cell) * cells.capacity, - .static_draw, - ); - - self.gl_cells_size = cells.capacity; - self.gl_cells_written = 0; - } - - // If we have data to write to the GPU, send it. - if (self.gl_cells_written < cells.items.len) { - const data = cells.items[self.gl_cells_written..]; - // log.info("sending {} cells to GPU", .{data.len}); - try bind.vbo.setSubData(self.gl_cells_written * @sizeOf(CellProgram.Cell), data); - - self.gl_cells_written += data.len; - assert(data.len > 0); - assert(self.gl_cells_written <= cells.items.len); - } - - try gl.drawElementsInstanced( - gl.c.GL_TRIANGLES, - 6, - gl.c.GL_UNSIGNED_BYTE, - cells.items.len, - ); -} - -/// The OpenGL objects that are associated with a renderer. This makes it -/// easy to create/destroy these as a set in situations i.e. where the -/// OpenGL context is replaced. -const GLState = struct { - cell_program: CellProgram, - image_program: ImageProgram, - texture: gl.Texture, - texture_color: gl.Texture, - custom: ?custom.State, - - pub fn init( - alloc: Allocator, - config: DerivedConfig, - font_grid: *font.SharedGrid, - ) !GLState { - var arena = ArenaAllocator.init(alloc); - defer arena.deinit(); - const arena_alloc = arena.allocator(); - - // Load our custom shaders - const custom_state: ?custom.State = custom: { - const shaders: []const [:0]const u8 = shadertoy.loadFromFiles( - arena_alloc, - config.custom_shaders, - .glsl, - ) catch |err| err: { - log.warn("error loading custom shaders err={}", .{err}); - break :err &.{}; - }; - if (shaders.len == 0) break :custom null; - - break :custom custom.State.init( - alloc, - shaders, - ) catch |err| err: { - log.warn("error initializing custom shaders err={}", .{err}); - break :err null; - }; - }; - - // Blending for text. We use GL_ONE here because we should be using - // premultiplied alpha for all our colors in our fragment shaders. - // This avoids having a blurry border where transparency is expected on - // pixels. - try gl.enable(gl.c.GL_BLEND); - try gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA); - - // Build our texture - const tex = try gl.Texture.create(); - errdefer tex.destroy(); - { - const texbind = try tex.bind(.@"2D"); - try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); - try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); - try texbind.image2D( - 0, - .red, - @intCast(font_grid.atlas_grayscale.size), - @intCast(font_grid.atlas_grayscale.size), - 0, - .red, - .UnsignedByte, - font_grid.atlas_grayscale.data.ptr, - ); - } - - // Build our color texture - const tex_color = try gl.Texture.create(); - errdefer tex_color.destroy(); - { - const texbind = try tex_color.bind(.@"2D"); - try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); - try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); - try texbind.image2D( - 0, - .rgba, - @intCast(font_grid.atlas_color.size), - @intCast(font_grid.atlas_color.size), - 0, - .bgra, - .UnsignedByte, - font_grid.atlas_color.data.ptr, - ); - } - - // Build our cell renderer - const cell_program = try CellProgram.init(); - errdefer cell_program.deinit(); - - // Build our image renderer - const image_program = try ImageProgram.init(); - errdefer image_program.deinit(); - - return .{ - .cell_program = cell_program, - .image_program = image_program, - .texture = tex, - .texture_color = tex_color, - .custom = custom_state, - }; - } - - pub fn deinit(self: *GLState, alloc: Allocator) void { - if (self.custom) |v| v.deinit(alloc); - self.texture.destroy(); - self.texture_color.destroy(); - self.image_program.deinit(); - self.cell_program.deinit(); - } -}; diff --git a/src/renderer/Options.zig b/src/renderer/Options.zig index e7d9b3a42..85ff8e310 100644 --- a/src/renderer/Options.zig +++ b/src/renderer/Options.zig @@ -20,3 +20,6 @@ surface_mailbox: apprt.surface.Mailbox, /// The apprt surface. rt_surface: *apprt.Surface, + +/// The renderer thread. +thread: *renderer.Thread, diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 52f599549..03ca7b5e1 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -20,6 +20,16 @@ const log = std.log.scoped(.renderer_thread); const DRAW_INTERVAL = 8; // 120 FPS const CURSOR_BLINK_INTERVAL = 600; +/// Whether calls to `drawFrame` must be done from the app thread. +/// +/// If this is `true` then we send a `redraw_surface` message to the apprt +/// whenever we need to draw instead of calling `drawFrame` directly. +const must_draw_from_app_thread = + if (@hasDecl(apprt.App, "must_draw_from_app_thread")) + apprt.App.must_draw_from_app_thread + else + false; + /// The type used for sending messages to the IO thread. For now this is /// hardcoded with a capacity. We can make this a comptime parameter in /// the future if we want it configurable. @@ -314,6 +324,16 @@ fn stopDrawTimer(self: *Thread) void { /// Drain the mailbox. fn drainMailbox(self: *Thread) !void { + // There's probably a more elegant way to do this... + // + // This is effectively an @autoreleasepool{} block, which we need in + // order to ensure that autoreleased objects are properly released. + const pool = if (builtin.os.tag.isDarwin()) + @import("objc").AutoreleasePool.init() + else + void; + defer if (builtin.os.tag.isDarwin()) pool.deinit(); + while (self.mailbox.pop()) |message| { log.debug("mailbox message={}", .{message}); switch (message) { @@ -432,7 +452,7 @@ fn drainMailbox(self: *Thread) !void { self.renderer.markDirty(); }, - .resize => |v| try self.renderer.setScreenSize(v), + .resize => {}, //|v| try self.renderer.setScreenSize(v), .change_config => |config| { defer config.alloc.destroy(config.thread); @@ -468,20 +488,16 @@ fn drawFrame(self: *Thread, now: bool) void { if (!self.flags.visible) return; // If the renderer is managing a vsync on its own, we only draw - // when we're forced to via now. + // when we're forced to via `now`. if (!now and self.renderer.hasVsync()) return; - // If we're doing single-threaded GPU calls then we just wake up the - // app thread to redraw at this point. - if (rendererpkg.Renderer == rendererpkg.OpenGL and - rendererpkg.OpenGL.single_threaded_draw) - { + if (must_draw_from_app_thread) { _ = self.app_mailbox.push( .{ .redraw_surface = self.surface }, .{ .instant = {} }, ); } else { - self.renderer.drawFrame(self.surface) catch |err| + self.renderer.drawFrame(false) catch |err| log.warn("error drawing err={}", .{err}); } } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig new file mode 100644 index 000000000..359f5f1b3 --- /dev/null +++ b/src/renderer/generic.zig @@ -0,0 +1,2866 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const glfw = @import("glfw"); +const xev = @import("xev"); +const apprt = @import("../apprt.zig"); +const configpkg = @import("../config.zig"); +const font = @import("../font/main.zig"); +const os = @import("../os/main.zig"); +const terminal = @import("../terminal/main.zig"); +const renderer = @import("../renderer.zig"); +const math = @import("../math.zig"); +const Surface = @import("../Surface.zig"); +const link = @import("link.zig"); +const fgMode = @import("cell.zig").fgMode; +const isCovering = @import("cell.zig").isCovering; +const shadertoy = @import("shadertoy.zig"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const ArenaAllocator = std.heap.ArenaAllocator; +const Terminal = terminal.Terminal; +const Health = renderer.Health; + +const macos = switch (builtin.os.tag) { + .macos => @import("macos"), + else => void, +}; + +const DisplayLink = switch (builtin.os.tag) { + .macos => *macos.video.DisplayLink, + else => void, +}; + +const log = std.log.scoped(.generic_renderer); + +/// Create a renderer type with the provided graphics API wrapper. +/// +/// The graphics API wrapper must provide the interface outlined below. +/// Specific details for the interfaces are documented on the existing +/// implementations (`Metal` and `OpenGL`). +/// +/// Hierarchy of graphics abstractions: +/// +/// [ GraphicsAPI ] - Responsible for configuring the runtime surface +/// | | and providing render `Target`s that draw to it, +/// | | as well as `Frame`s and `Pipeline`s. +/// | V +/// | [ Target ] - Represents an abstract target for rendering, which +/// | could be a surface directly but is also used as an +/// | abstraction for off-screen frame buffers. +/// V +/// [ Frame ] - Represents the context for drawing a given frame, +/// | provides `RenderPass`es for issuing draw commands +/// | to, and reports the frame health when complete. +/// V +/// [ RenderPass ] - Represents a render pass in a frame, consisting of +/// : one or more `Step`s applied to the same target(s), +/// [ Step ] - - - - each describing the input buffers and textures and +/// : the vertex/fragment functions and geometry to use. +/// :_ _ _ _ _ _ _ _ _ _/ +/// v +/// [ Pipeline ] - Describes a vertex and fragment function to be used +/// for a `Step`; the `GraphicsAPI` is responsible for +/// these and they should be constructed and cached +/// ahead of time. +/// +/// [ Buffer ] - An abstraction over a GPU buffer. +/// +/// [ Texture ] - An abstraction over a GPU texture. +/// +pub fn Renderer(comptime GraphicsAPI: type) type { + const Target = GraphicsAPI.Target; + const Buffer = GraphicsAPI.Buffer; + const Texture = GraphicsAPI.Texture; + const RenderPass = GraphicsAPI.RenderPass; + const shaderpkg = GraphicsAPI.shaders; + + const cellpkg = GraphicsAPI.cellpkg; + const imagepkg = GraphicsAPI.imagepkg; + const Image = imagepkg.Image; + const ImageMap = imagepkg.ImageMap; + + const Shaders = shaderpkg.Shaders; + + const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); + + return struct { + const Self = @This(); + + /// Allocator that can be used + alloc: std.mem.Allocator, + + /// This mutex must be held whenever any state used in `drawFrame` is + /// being modified, and also when it's being accessed in `drawFrame`. + draw_mutex: std.Thread.Mutex = .{}, + + /// The configuration we need derived from the main config. + config: DerivedConfig, + + /// The mailbox for communicating with the window. + surface_mailbox: apprt.surface.Mailbox, + + /// Current font metrics defining our grid. + grid_metrics: font.Metrics, + + /// The size of everything. + size: renderer.Size, + + /// True if the window is focused + focused: bool, + + /// The foreground color set by an OSC 10 sequence. If unset then + /// default_foreground_color is used. + foreground_color: ?terminal.color.RGB, + + /// Foreground color set in the user's config file. + default_foreground_color: terminal.color.RGB, + + /// The background color set by an OSC 11 sequence. If unset then + /// default_background_color is used. + background_color: ?terminal.color.RGB, + + /// Background color set in the user's config file. + default_background_color: terminal.color.RGB, + + /// The cursor color set by an OSC 12 sequence. If unset then + /// default_cursor_color is used. + cursor_color: ?terminal.color.RGB, + + /// Default cursor color when no color is set explicitly by an OSC 12 command. + /// This is cursor color as set in the user's config, if any. If no cursor color + /// is set in the user's config, then the cursor color is determined by the + /// current foreground color. + default_cursor_color: ?terminal.color.RGB, + + /// When `cursor_color` is null, swap the foreground and background colors of + /// the cell under the cursor for the cursor color. Otherwise, use the default + /// foreground color as the cursor color. + cursor_invert: bool, + + /// The current set of cells to render. This is rebuilt on every frame + /// but we keep this around so that we don't reallocate. Each set of + /// cells goes into a separate shader. + cells: cellpkg.Contents, + + /// The last viewport that we based our rebuild off of. If this changes, + /// then we do a full rebuild of the cells. The pointer values in this pin + /// are NOT SAFE to read because they may be modified, freed, etc from the + /// termio thread. We treat the pointers as integers for comparison only. + cells_viewport: ?terminal.Pin = null, + + /// Set to true after rebuildCells is called. This can be used + /// to determine if any possible changes have been made to the + /// cells for the draw call. + cells_rebuilt: bool = false, + + /// The current GPU uniform values. + uniforms: shaderpkg.Uniforms, + + /// The font structures. + font_grid: *font.SharedGrid, + font_shaper: font.Shaper, + font_shaper_cache: font.ShaperCache, + + /// The images that we may render. + images: ImageMap = .{}, + image_placements: ImagePlacementList = .{}, + image_bg_end: u32 = 0, + image_text_end: u32 = 0, + image_virtual: bool = false, + + /// Graphics API state. + api: GraphicsAPI, + + /// The CVDisplayLink used to drive the rendering loop in + /// sync with the display. This is void on platforms that + /// don't support a display link. + display_link: ?DisplayLink = null, + + /// Health of the most recently completed frame. + health: std.atomic.Value(Health) = .{ .raw = .healthy }, + + /// Our swap chain (multiple buffering) + swap_chain: SwapChain, + + /// This value is used to force-update swap chain targets in the + /// event of a config change that requires it (such as blending mode). + target_config_modified: usize = 0, + + /// If something happened that requires us to reinitialize our shaders, + /// this is set to true so that we can do that whenever possible. + reinitialize_shaders: bool = false, + + /// Whether or not we have custom shaders. + has_custom_shaders: bool = false, + + /// Our shader pipelines. + shaders: Shaders, + + /// Swap chain which maintains multiple copies of the state needed to + /// render a frame, so that we can start building the next frame while + /// the previous frame is still being processed on the GPU. + const SwapChain = struct { + // The count of buffers we use for double/triple buffering. + // If this is one then we don't do any double+ buffering at all. + // This is comptime because there isn't a good reason to change + // this at runtime and there is a lot of complexity to support it. + // For comptime, this is useful for debugging. + const buf_count = 3; + + /// `buf_count` structs that can hold the + /// data needed by the GPU to draw a frame. + frames: [buf_count]FrameState, + /// Index of the most recently used frame state struct. + frame_index: std.math.IntFittingRange(0, buf_count) = 0, + /// Semaphore that we wait on to make sure we have an available + /// frame state struct so we can start working on a new frame. + frame_sema: std.Thread.Semaphore = .{ .permits = buf_count }, + + /// Set to true when deinited, if you try to deinit a defunct + /// swap chain it will just be ignored, to prevent double-free. + defunct: bool = false, + + pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain { + var result: SwapChain = .{ .frames = undefined }; + + // Initialize all of our frame state. + for (&result.frames) |*frame| { + frame.* = try FrameState.init(api, custom_shaders); + } + + return result; + } + + pub fn deinit(self: *SwapChain) void { + if (self.defunct) return; + self.defunct = true; + + // Wait for all of our inflight draws to complete + // so that we can cleanly deinit our GPU state. + for (0..buf_count) |_| self.frame_sema.wait(); + for (&self.frames) |*frame| frame.deinit(); + } + + /// Get the next frame state to draw to. This will wait on the + /// semaphore to ensure that the frame is available. This must + /// always be paired with a call to releaseFrame. + pub fn nextFrame(self: *SwapChain) error{Defunct}!*FrameState { + if (self.defunct) return error.Defunct; + + self.frame_sema.wait(); + errdefer self.frame_sema.post(); + self.frame_index = (self.frame_index + 1) % buf_count; + return &self.frames[self.frame_index]; + } + + /// This should be called when the frame has completed drawing. + pub fn releaseFrame(self: *SwapChain) void { + self.frame_sema.post(); + } + }; + + /// State we need duplicated for every frame. Any state that could be + /// in a data race between the GPU and CPU while a frame is being drawn + /// should be in this struct. + /// + /// While a draw is in-process, we "lock" the state (via a semaphore) + /// and prevent the CPU from updating the state until our graphics API + /// reports that the frame is complete. + /// + /// This is used to implement double/triple buffering. + const FrameState = struct { + uniforms: UniformBuffer, + cells: CellTextBuffer, + cells_bg: CellBgBuffer, + + grayscale: Texture, + grayscale_modified: usize = 0, + color: Texture, + color_modified: usize = 0, + + target: Target, + /// See property of same name on Renderer for explanation. + target_config_modified: usize = 0, + + /// Custom shader state, this is null if we have no custom shaders. + custom_shader_state: ?CustomShaderState = null, + + /// A buffer containing the uniform data. + const UniformBuffer = Buffer(shaderpkg.Uniforms); + const CellBgBuffer = Buffer(shaderpkg.CellBg); + const CellTextBuffer = Buffer(shaderpkg.CellText); + + pub fn init(api: GraphicsAPI, custom_shaders: bool) !FrameState { + // Uniform buffer contains exactly 1 uniform struct. The + // uniform data will be undefined so this must be set before + // a frame is drawn. + var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1); + errdefer uniforms.deinit(); + + // Create the buffers for our vertex data. The preallocation size + // is likely too small but our first frame update will resize it. + var cells = try CellTextBuffer.init(api.fgBufferOptions(), 10 * 10); + errdefer cells.deinit(); + var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 10 * 10); + errdefer cells_bg.deinit(); + + // Initialize our textures for our font atlas. + const grayscale = try api.initAtlasTexture(&.{ + .data = undefined, + .size = 8, + .format = .grayscale, + }); + errdefer grayscale.deinit(); + const color = try api.initAtlasTexture(&.{ + .data = undefined, + .size = 8, + .format = .rgba, + }); + errdefer color.deinit(); + + var custom_shader_state = + if (custom_shaders) + try CustomShaderState.init(api) + else + null; + errdefer if (custom_shader_state) |*state| state.deinit(); + + // Initialize the target at 1x1 px, this is slightly + // wasteful but it's only done once so whatever. + const target = try api.initTarget(1, 1); + + return .{ + .uniforms = uniforms, + .cells = cells, + .cells_bg = cells_bg, + .grayscale = grayscale, + .color = color, + .target = target, + .custom_shader_state = custom_shader_state, + }; + } + + pub fn deinit(self: *FrameState) void { + self.uniforms.deinit(); + self.cells.deinit(); + self.cells_bg.deinit(); + self.grayscale.deinit(); + self.color.deinit(); + if (self.custom_shader_state) |*state| state.deinit(); + } + + pub fn resize( + self: *FrameState, + api: GraphicsAPI, + width: usize, + height: usize, + ) !void { + if (self.custom_shader_state) |*state| { + try state.resize(api, width, height); + } + const target = try api.initTarget(width, height); + self.target.deinit(); + self.target = target; + } + }; + + /// State relevant to our custom shaders if we have any. + const CustomShaderState = struct { + /// When we have a custom shader state, we maintain a front + /// and back texture which we use as a swap chain to render + /// between when multiple custom shaders are defined. + front_texture: Texture, + back_texture: Texture, + + uniforms: shaderpkg.PostUniforms, + + /// The first time a frame was drawn. + /// This is used to update the time uniform. + first_frame_time: std.time.Instant, + + /// The last time a frame was drawn. + /// This is used to update the time uniform. + last_frame_time: std.time.Instant, + + /// Swap the front and back textures. + pub fn swap(self: *CustomShaderState) void { + std.mem.swap(Texture, &self.front_texture, &self.back_texture); + } + + pub fn init(api: GraphicsAPI) !CustomShaderState { + // Initialize the front and back textures at 1x1 px, this + // is slightly wasteful but it's only done once so whatever. + const front_texture = try Texture.init( + api.textureOptions(), + 1, + 1, + null, + ); + errdefer front_texture.deinit(); + const back_texture = try Texture.init( + api.textureOptions(), + 1, + 1, + null, + ); + errdefer back_texture.deinit(); + return .{ + .front_texture = front_texture, + .back_texture = back_texture, + + .uniforms = .{ + .resolution = .{ 0, 0, 1 }, + .time = 1, + .time_delta = 1, + .frame_rate = 1, + .frame = 1, + .channel_time = @splat(@splat(0)), + .channel_resolution = @splat(@splat(0)), + .mouse = .{ 0, 0, 0, 0 }, + .date = .{ 0, 0, 0, 0 }, + .sample_rate = 1, + }, + + .first_frame_time = try std.time.Instant.now(), + .last_frame_time = try std.time.Instant.now(), + }; + } + + pub fn deinit(self: *CustomShaderState) void { + self.front_texture.deinit(); + self.back_texture.deinit(); + } + + pub fn resize( + self: *CustomShaderState, + api: GraphicsAPI, + width: usize, + height: usize, + ) !void { + const front_texture = try Texture.init( + api.textureOptions(), + @intCast(width), + @intCast(height), + null, + ); + errdefer front_texture.deinit(); + const back_texture = try Texture.init( + api.textureOptions(), + @intCast(width), + @intCast(height), + null, + ); + errdefer back_texture.deinit(); + + self.front_texture.deinit(); + self.back_texture.deinit(); + + self.front_texture = front_texture; + self.back_texture = back_texture; + + self.uniforms.resolution = .{ + @floatFromInt(width), + @floatFromInt(height), + 1, + }; + self.uniforms.channel_resolution[0] = .{ + @floatFromInt(width), + @floatFromInt(height), + 1, + 0, + }; + } + }; + + /// The configuration for this renderer that is derived from the main + /// configuration. This must be exported so that we don't need to + /// pass around Config pointers which makes memory management a pain. + pub const DerivedConfig = struct { + arena: ArenaAllocator, + + font_thicken: bool, + font_thicken_strength: u8, + font_features: std.ArrayListUnmanaged([:0]const u8), + font_styles: font.CodepointResolver.StyleStatus, + cursor_color: ?terminal.color.RGB, + cursor_invert: bool, + cursor_opacity: f64, + cursor_text: ?terminal.color.RGB, + background: terminal.color.RGB, + background_opacity: f64, + foreground: terminal.color.RGB, + selection_background: ?terminal.color.RGB, + selection_foreground: ?terminal.color.RGB, + invert_selection_fg_bg: bool, + bold_is_bright: bool, + min_contrast: f32, + padding_color: configpkg.WindowPaddingColor, + custom_shaders: configpkg.RepeatablePath, + links: link.Set, + vsync: bool, + colorspace: configpkg.Config.WindowColorspace, + blending: configpkg.Config.AlphaBlending, + + pub fn init( + alloc_gpa: Allocator, + config: *const configpkg.Config, + ) !DerivedConfig { + var arena = ArenaAllocator.init(alloc_gpa); + errdefer arena.deinit(); + const alloc = arena.allocator(); + + // Copy our shaders + const custom_shaders = try config.@"custom-shader".clone(alloc); + + // Copy our font features + const font_features = try config.@"font-feature".clone(alloc); + + // Get our font styles + var font_styles = font.CodepointResolver.StyleStatus.initFill(true); + font_styles.set(.bold, config.@"font-style-bold" != .false); + font_styles.set(.italic, config.@"font-style-italic" != .false); + font_styles.set(.bold_italic, config.@"font-style-bold-italic" != .false); + + // Our link configs + const links = try link.Set.fromConfig( + alloc, + config.link.links.items, + ); + + const cursor_invert = config.@"cursor-invert-fg-bg"; + + return .{ + .background_opacity = @max(0, @min(1, config.@"background-opacity")), + .font_thicken = config.@"font-thicken", + .font_thicken_strength = config.@"font-thicken-strength", + .font_features = font_features.list, + .font_styles = font_styles, + + .cursor_color = if (!cursor_invert and config.@"cursor-color" != null) + config.@"cursor-color".?.toTerminalRGB() + else + null, + + .cursor_invert = cursor_invert, + + .cursor_text = if (config.@"cursor-text") |txt| + txt.toTerminalRGB() + else + null, + + .cursor_opacity = @max(0, @min(1, config.@"cursor-opacity")), + + .background = config.background.toTerminalRGB(), + .foreground = config.foreground.toTerminalRGB(), + .invert_selection_fg_bg = config.@"selection-invert-fg-bg", + .bold_is_bright = config.@"bold-is-bright", + .min_contrast = @floatCast(config.@"minimum-contrast"), + .padding_color = config.@"window-padding-color", + + .selection_background = if (config.@"selection-background") |bg| + bg.toTerminalRGB() + else + null, + + .selection_foreground = if (config.@"selection-foreground") |bg| + bg.toTerminalRGB() + else + null, + + .custom_shaders = custom_shaders, + .links = links, + .vsync = config.@"window-vsync", + .colorspace = config.@"window-colorspace", + .blending = config.@"alpha-blending", + .arena = arena, + }; + } + + pub fn deinit(self: *DerivedConfig) void { + const alloc = self.arena.allocator(); + self.links.deinit(alloc); + self.arena.deinit(); + } + }; + + /// Returns the hints that we want for this window. + pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { + // If our graphics API provides hints, use them, + // otherwise fall back to generic hints. + if (@hasDecl(GraphicsAPI, "glfwWindowHints")) { + return GraphicsAPI.glfwWindowHints(config); + } + + return .{ + .client_api = .no_api, + .transparent_framebuffer = config.@"background-opacity" < 1, + }; + } + + pub fn init(alloc: Allocator, options: renderer.Options) !Self { + // Initialize our graphics API wrapper, this will prepare the + // surface provided by the apprt and set up any API-specific + // GPU resources. + var api = try GraphicsAPI.init(alloc, options); + errdefer api.deinit(); + + const has_custom_shaders = options.config.custom_shaders.value.items.len > 0; + + // Prepare our swap chain + var swap_chain = try SwapChain.init( + api, + has_custom_shaders, + ); + errdefer swap_chain.deinit(); + + // Create the font shaper. + var font_shaper = try font.Shaper.init(alloc, .{ + .features = options.config.font_features.items, + }); + errdefer font_shaper.deinit(); + + // Initialize all the data that requires a critical font section. + const font_critical: struct { + metrics: font.Metrics, + } = font_critical: { + const grid = options.font_grid; + grid.lock.lockShared(); + defer grid.lock.unlockShared(); + break :font_critical .{ + .metrics = grid.metrics, + }; + }; + + const display_link: ?DisplayLink = switch (builtin.os.tag) { + .macos => if (options.config.vsync) + try macos.video.DisplayLink.createWithActiveCGDisplays() + else + null, + else => null, + }; + errdefer if (display_link) |v| v.release(); + + var result: Self = .{ + .alloc = alloc, + .config = options.config, + .surface_mailbox = options.surface_mailbox, + .grid_metrics = font_critical.metrics, + .size = options.size, + .focused = true, + .foreground_color = null, + .default_foreground_color = options.config.foreground, + .background_color = null, + .default_background_color = options.config.background, + .cursor_color = null, + .default_cursor_color = options.config.cursor_color, + .cursor_invert = options.config.cursor_invert, + + // Render state + .cells = .{}, + .uniforms = .{ + .projection_matrix = undefined, + .cell_size = undefined, + .grid_size = undefined, + .grid_padding = undefined, + .padding_extend = .{}, + .min_contrast = options.config.min_contrast, + .cursor_pos = .{ std.math.maxInt(u16), std.math.maxInt(u16) }, + .cursor_color = undefined, + .bg_color = .{ + options.config.background.r, + options.config.background.g, + options.config.background.b, + @intFromFloat(@round(options.config.background_opacity * 255.0)), + }, + .bools = .{ + .cursor_wide = false, + .use_display_p3 = options.config.colorspace == .@"display-p3", + .use_linear_blending = options.config.blending.isLinear(), + .use_linear_correction = options.config.blending == .@"linear-corrected", + }, + }, + + // Fonts + .font_grid = options.font_grid, + .font_shaper = font_shaper, + .font_shaper_cache = font.ShaperCache.init(), + + // Shaders (initialized below) + .shaders = undefined, + + // Graphics API stuff + .api = api, + .swap_chain = swap_chain, + .display_link = display_link, + }; + + try result.initShaders(); + + // Ensure our undefined values above are correctly initialized. + result.updateFontGridUniforms(); + result.updateScreenSizeUniforms(); + + return result; + } + + pub fn deinit(self: *Self) void { + self.swap_chain.deinit(); + + if (DisplayLink != void) { + if (self.display_link) |display_link| { + display_link.stop() catch {}; + display_link.release(); + } + } + + self.cells.deinit(self.alloc); + + self.font_shaper.deinit(); + self.font_shaper_cache.deinit(self.alloc); + + self.config.deinit(); + + { + var it = self.images.iterator(); + while (it.next()) |kv| kv.value_ptr.image.deinit(self.alloc); + self.images.deinit(self.alloc); + } + self.image_placements.deinit(self.alloc); + + self.deinitShaders(); + + self.api.deinit(); + + self.* = undefined; + } + + fn deinitShaders(self: *Self) void { + self.shaders.deinit(self.alloc); + } + + fn initShaders(self: *Self) !void { + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Load our custom shaders + const custom_shaders: []const [:0]const u8 = shadertoy.loadFromFiles( + arena_alloc, + self.config.custom_shaders, + GraphicsAPI.custom_shader_target, + ) catch |err| err: { + log.warn("error loading custom shaders err={}", .{err}); + break :err &.{}; + }; + + const has_custom_shaders = custom_shaders.len > 0; + + var shaders = try self.api.initShaders( + self.alloc, + custom_shaders, + ); + errdefer shaders.deinit(self.alloc); + + self.shaders = shaders; + self.has_custom_shaders = has_custom_shaders; + } + + /// This is called early right after surface creation. + pub fn surfaceInit(surface: *apprt.Surface) !void { + // If our API has to do things here, let it. + if (@hasDecl(GraphicsAPI, "surfaceInit")) { + try GraphicsAPI.surfaceInit(surface); + } + } + + /// This is called just prior to spinning up the renderer thread for + /// final main thread setup requirements. + pub fn finalizeSurfaceInit(self: *Self, surface: *apprt.Surface) !void { + // If our API has to do things to finalize surface init, let it. + if (@hasDecl(GraphicsAPI, "finalizeSurfaceInit")) { + try self.api.finalizeSurfaceInit(surface); + } + } + + /// Callback called by renderer.Thread when it begins. + pub fn threadEnter(self: *const Self, surface: *apprt.Surface) !void { + // If our API has to do things on thread enter, let it. + if (@hasDecl(GraphicsAPI, "threadEnter")) { + try self.api.threadEnter(surface); + } + } + + /// Callback called by renderer.Thread when it exits. + pub fn threadExit(self: *const Self) void { + // If our API has to do things on thread exit, let it. + if (@hasDecl(GraphicsAPI, "threadExit")) { + self.api.threadExit(); + } + } + + /// Called by renderer.Thread when it starts the main loop. + pub fn loopEnter(self: *Self, thr: *renderer.Thread) !void { + // If our API has to do things on loop enter, let it. + if (@hasDecl(GraphicsAPI, "loopEnter")) { + self.api.loopEnter(); + } + + // If we don't support a display link we have no work to do. + if (comptime DisplayLink == void) return; + + // This is when we know our "self" pointer is stable so we can + // setup the display link. To setup the display link we set our + // callback and we can start it immediately. + const display_link = self.display_link orelse return; + try display_link.setOutputCallback( + xev.Async, + &displayLinkCallback, + &thr.draw_now, + ); + display_link.start() catch {}; + } + + /// Called by renderer.Thread when it exits the main loop. + pub fn loopExit(self: *Self) void { + // If our API has to do things on loop exit, let it. + if (@hasDecl(GraphicsAPI, "loopExit")) { + self.api.loopExit(); + } + + // If we don't support a display link we have no work to do. + if (comptime DisplayLink == void) return; + + // Stop our display link. If this fails its okay it just means + // that we either never started it or the view its attached to + // is gone which is fine. + const display_link = self.display_link orelse return; + display_link.stop() catch {}; + } + + /// This is called by the GTK apprt after the surface is + /// reinitialized due to any of the events mentioned in + /// the doc comment for `displayUnrealized`. + pub fn displayRealized(self: *Self) !void { + // If our API has to do things on realize, let it. + if (@hasDecl(GraphicsAPI, "displayRealized")) { + self.api.displayRealized(); + } + + // Lock the draw mutex so that we can + // safely reinitialize our GPU resources. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We assume that the swap chain was deinited in + // `displayUnrealized`, in which case it should be + // marked defunct. If not, we have a problem. + assert(self.swap_chain.defunct); + + // We reinitialize our shaders and our swap chain. + try self.initShaders(); + self.swap_chain = try SwapChain.init( + self.api, + self.has_custom_shaders, + ); + self.reinitialize_shaders = false; + self.target_config_modified = 1; + } + + /// This is called by the GTK apprt when the surface is being destroyed. + /// This can happen because the surface is being closed but also when + /// moving the window between displays or splitting. + pub fn displayUnrealized(self: *Self) void { + // If our API has to do things on unrealize, let it. + if (@hasDecl(GraphicsAPI, "displayUnrealized")) { + self.api.displayUnrealized(); + } + + // Lock the draw mutex so that we can + // safely deinitialize our GPU resources. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We deinit our swap chain and shaders. + // + // This will mark them as defunct so that they + // can't be double-freed or used in draw calls. + self.swap_chain.deinit(); + self.shaders.deinit(self.alloc); + } + + fn displayLinkCallback( + _: *macos.video.DisplayLink, + ud: ?*xev.Async, + ) void { + const draw_now = ud orelse return; + draw_now.notify() catch |err| { + log.err("error notifying draw_now err={}", .{err}); + }; + } + + /// Mark the full screen as dirty so that we redraw everything. + pub fn markDirty(self: *Self) void { + self.cells_viewport = null; + } + + /// Called when we get an updated display ID for our display link. + pub fn setMacOSDisplayID(self: *Self, id: u32) !void { + if (comptime DisplayLink == void) return; + const display_link = self.display_link orelse return; + log.info("updating display link display id={}", .{id}); + display_link.setCurrentCGDisplay(id) catch |err| { + log.warn("error setting display link display id err={}", .{err}); + }; + } + + /// True if our renderer has animations so that a higher frequency + /// timer is used. + pub fn hasAnimations(self: *const Self) bool { + return self.has_custom_shaders; + } + + /// True if our renderer is using vsync. If true, the renderer or apprt + /// is responsible for triggering draw_now calls to the render thread. + /// That is the only way to trigger a drawFrame. + pub fn hasVsync(self: *const Self) bool { + if (comptime DisplayLink == void) return false; + const display_link = self.display_link orelse return false; + return display_link.isRunning(); + } + + /// Callback when the focus changes for the terminal this is rendering. + /// + /// Must be called on the render thread. + pub fn setFocus(self: *Self, focus: bool) !void { + self.focused = focus; + + // If we're not focused, then we want to stop the display link + // because it is a waste of resources and we can move to pure + // change-driven updates. + if (comptime DisplayLink != void) link: { + const display_link = self.display_link orelse break :link; + if (focus) { + display_link.start() catch {}; + } else { + display_link.stop() catch {}; + } + } + } + + /// Callback when the window is visible or occluded. + /// + /// Must be called on the render thread. + pub fn setVisible(self: *Self, visible: bool) void { + // If we're not visible, then we want to stop the display link + // because it is a waste of resources and we can move to pure + // change-driven updates. + if (comptime DisplayLink != void) link: { + const display_link = self.display_link orelse break :link; + if (visible and self.focused) { + display_link.start() catch {}; + } else { + display_link.stop() catch {}; + } + } + } + + /// Set the new font grid. + /// + /// Must be called on the render thread. + pub fn setFontGrid(self: *Self, grid: *font.SharedGrid) void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // Update our grid + self.font_grid = grid; + + // Update all our textures so that they sync on the next frame. + // We can modify this without a lock because the GPU does not + // touch this data. + for (&self.swap_chain.frames) |*frame| { + frame.grayscale_modified = 0; + frame.color_modified = 0; + } + + // Get our metrics from the grid. This doesn't require a lock because + // the metrics are never recalculated. + const metrics = grid.metrics; + self.grid_metrics = metrics; + + // Reset our shaper cache. If our font changed (not just the size) then + // the data in the shaper cache may be invalid and cannot be used, so we + // always clear the cache just in case. + const font_shaper_cache = font.ShaperCache.init(); + self.font_shaper_cache.deinit(self.alloc); + self.font_shaper_cache = font_shaper_cache; + + // Update cell size. + self.size.cell = .{ + .width = metrics.cell_width, + .height = metrics.cell_height, + }; + + // Update relevant uniforms + self.updateFontGridUniforms(); + } + + /// Update uniforms that are based on the font grid. + /// + /// Caller must hold the draw mutex. + fn updateFontGridUniforms(self: *Self) void { + self.uniforms.cell_size = .{ + @floatFromInt(self.grid_metrics.cell_width), + @floatFromInt(self.grid_metrics.cell_height), + }; + } + + /// Update the frame data. + pub fn updateFrame( + self: *Self, + surface: *apprt.Surface, + state: *renderer.State, + cursor_blink_visible: bool, + ) !void { + _ = surface; + + // Data we extract out of the critical area. + const Critical = struct { + bg: terminal.color.RGB, + screen: terminal.Screen, + screen_type: terminal.ScreenType, + mouse: renderer.State.Mouse, + preedit: ?renderer.State.Preedit, + cursor_style: ?renderer.CursorStyle, + color_palette: terminal.color.Palette, + + /// If true, rebuild the full screen. + full_rebuild: bool, + }; + + // Update all our data as tightly as possible within the mutex. + var critical: Critical = critical: { + // const start = try std.time.Instant.now(); + // const start_micro = std.time.microTimestamp(); + // defer { + // const end = std.time.Instant.now() catch unreachable; + // // "[updateFrame critical time] \t" + // std.log.err("[updateFrame critical time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); + // } + + state.mutex.lock(); + defer state.mutex.unlock(); + + // If we're in a synchronized output state, we pause all rendering. + if (state.terminal.modes.get(.synchronized_output)) { + log.debug("synchronized output started, skipping render", .{}); + return; + } + + // Swap bg/fg if the terminal is reversed + const bg = self.background_color orelse self.default_background_color; + const fg = self.foreground_color orelse self.default_foreground_color; + defer { + if (self.background_color) |*c| { + c.* = bg; + } else { + self.default_background_color = bg; + } + + if (self.foreground_color) |*c| { + c.* = fg; + } else { + self.default_foreground_color = fg; + } + } + + if (state.terminal.modes.get(.reverse_colors)) { + if (self.background_color) |*c| { + c.* = fg; + } else { + self.default_background_color = fg; + } + + if (self.foreground_color) |*c| { + c.* = bg; + } else { + self.default_foreground_color = bg; + } + } + + // Get the viewport pin so that we can compare it to the current. + const viewport_pin = state.terminal.screen.pages.pin(.{ .viewport = .{} }).?; + + // We used to share terminal state, but we've since learned through + // analysis that it is faster to copy the terminal state than to + // hold the lock while rebuilding GPU cells. + var screen_copy = try state.terminal.screen.clone( + self.alloc, + .{ .viewport = .{} }, + null, + ); + errdefer screen_copy.deinit(); + + // Whether to draw our cursor or not. + const cursor_style = if (state.terminal.flags.password_input) + .lock + else + renderer.cursorStyle( + state, + self.focused, + cursor_blink_visible, + ); + + // Get our preedit state + const preedit: ?renderer.State.Preedit = preedit: { + if (cursor_style == null) break :preedit null; + const p = state.preedit orelse break :preedit null; + break :preedit try p.clone(self.alloc); + }; + errdefer if (preedit) |p| p.deinit(self.alloc); + + // If we have Kitty graphics data, we enter a SLOW SLOW SLOW path. + // We only do this if the Kitty image state is dirty meaning only if + // it changes. + // + // If we have any virtual references, we must also rebuild our + // kitty state on every frame because any cell change can move + // an image. + if (state.terminal.screen.kitty_images.dirty or + self.image_virtual) + { + try self.prepKittyGraphics(state.terminal); + } + + // If we have any terminal dirty flags set then we need to rebuild + // the entire screen. This can be optimized in the future. + const full_rebuild: bool = rebuild: { + { + const Int = @typeInfo(terminal.Terminal.Dirty).@"struct".backing_integer.?; + const v: Int = @bitCast(state.terminal.flags.dirty); + if (v > 0) break :rebuild true; + } + { + const Int = @typeInfo(terminal.Screen.Dirty).@"struct".backing_integer.?; + const v: Int = @bitCast(state.terminal.screen.dirty); + if (v > 0) break :rebuild true; + } + + // If our viewport changed then we need to rebuild the entire + // screen because it means we scrolled. If we have no previous + // viewport then we must rebuild. + const prev_viewport = self.cells_viewport orelse break :rebuild true; + if (!prev_viewport.eql(viewport_pin)) break :rebuild true; + + break :rebuild false; + }; + + // Reset the dirty flags in the terminal and screen. We assume + // that our rebuild will be successful since so we optimize for + // success and reset while we hold the lock. This is much easier + // than coordinating row by row or as changes are persisted. + state.terminal.flags.dirty = .{}; + state.terminal.screen.dirty = .{}; + { + var it = state.terminal.screen.pages.pageIterator( + .right_down, + .{ .screen = .{} }, + null, + ); + while (it.next()) |chunk| { + var dirty_set = chunk.node.data.dirtyBitSet(); + dirty_set.unsetAll(); + } + } + + // Update our viewport pin + self.cells_viewport = viewport_pin; + + break :critical .{ + .bg = self.background_color orelse self.default_background_color, + .screen = screen_copy, + .screen_type = state.terminal.active_screen, + .mouse = state.mouse, + .preedit = preedit, + .cursor_style = cursor_style, + .color_palette = state.terminal.color_palette.colors, + .full_rebuild = full_rebuild, + }; + }; + defer { + critical.screen.deinit(); + if (critical.preedit) |p| p.deinit(self.alloc); + } + + // Build our GPU cells + try self.rebuildCells( + critical.full_rebuild, + &critical.screen, + critical.screen_type, + critical.mouse, + critical.preedit, + critical.cursor_style, + &critical.color_palette, + ); + + // Notify our shaper we're done for the frame. For some shapers, + // such as CoreText, this triggers off-thread cleanup logic. + self.font_shaper.endFrame(); + + // Acquire the draw mutex because we're modifying state here. + { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // Update our background color + self.uniforms.bg_color = .{ + critical.bg.r, + critical.bg.g, + critical.bg.b, + @intFromFloat(@round(self.config.background_opacity * 255.0)), + }; + } + } + + /// Draw the frame to the screen. + /// + /// If `sync` is true, this will synchronously block until + /// the frame is finished drawing and has been presented. + pub fn drawFrame( + self: *Self, + sync: bool, + ) !void { + // We hold a the draw mutex to prevent changes to any + // data we access while we're in the middle of drawing. + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // There's probably a more elegant way to do this... + // + // This is effectively an @autoreleasepool{} block, which we need in + // order to ensure that autoreleased objects are properly released. + const pool = if (builtin.os.tag.isDarwin()) + @import("objc").AutoreleasePool.init() + else + void; + defer if (builtin.os.tag.isDarwin()) pool.deinit(); + + // Retrieve the most up-to-date surface size from the Graphics API + const surface_size = try self.api.surfaceSize(); + + // If either of our surface dimensions is zero + // then drawing is absurd, so we just return. + if (surface_size.width == 0 or surface_size.height == 0) return; + + const size_changed = + self.size.screen.width != surface_size.width or + self.size.screen.height != surface_size.height; + + // Conditions under which we need to draw the frame, otherwise we + // don't need to since the previous frame should be identical. + const needs_redraw = + size_changed or + self.cells_rebuilt or + self.hasAnimations() or + sync; + + if (!needs_redraw) { + // We still need to present the last target again, because the + // apprt may be swapping buffers and display an outdated frame + // if we don't draw something new. + try self.api.repeat(); + return; + } + self.cells_rebuilt = false; + + // Wait for a frame to be available. + const frame = try self.swap_chain.nextFrame(); + errdefer self.swap_chain.releaseFrame(); + // log.debug("drawing frame index={}", .{self.swap_chain.frame_index}); + + // If we need to reinitialize our shaders, do so. + if (self.reinitialize_shaders) { + self.reinitialize_shaders = false; + self.shaders.deinit(self.alloc); + try self.initShaders(); + } + + // Our shaders should not be defunct at this point. + assert(!self.shaders.defunct); + + // If we have custom shaders, make sure we have the + // custom shader state in our frame state, otherwise + // if we have a state but don't need it we remove it. + if (self.has_custom_shaders) { + if (frame.custom_shader_state == null) { + frame.custom_shader_state = try .init(self.api); + try frame.custom_shader_state.?.resize( + self.api, + surface_size.width, + surface_size.height, + ); + } + } else if (frame.custom_shader_state) |*state| { + state.deinit(); + frame.custom_shader_state = null; + } + + // If our stored size doesn't match the + // surface size we need to update it. + if (size_changed) { + self.size.screen = .{ + .width = surface_size.width, + .height = surface_size.height, + }; + self.updateScreenSizeUniforms(); + } + + // If this frame's target isn't the correct size, or the target + // config has changed (such as when the blending mode changes), + // remove it and replace it with a new one with the right values. + if (frame.target.width != self.size.screen.width or + frame.target.height != self.size.screen.height or + frame.target_config_modified != self.target_config_modified) + { + try frame.resize( + self.api, + self.size.screen.width, + self.size.screen.height, + ); + frame.target_config_modified = self.target_config_modified; + } + + // Upload images to the GPU as necessary. + { + var image_it = self.images.iterator(); + while (image_it.next()) |kv| { + switch (kv.value_ptr.image) { + .ready => {}, + + .pending_gray, + .pending_gray_alpha, + .pending_rgb, + .pending_rgba, + .replace_gray, + .replace_gray_alpha, + .replace_rgb, + .replace_rgba, + => try kv.value_ptr.image.upload(self.alloc, &self.api), + + .unload_pending, + .unload_replace, + .unload_ready, + => { + kv.value_ptr.image.deinit(self.alloc); + self.images.removeByPtr(kv.key_ptr); + }, + } + } + } + + // Setup our frame data + try frame.uniforms.sync(&.{self.uniforms}); + try frame.cells_bg.sync(self.cells.bg_cells); + const fg_count = try frame.cells.syncFromArrayLists(self.cells.fg_rows.lists); + + // If we have custom shaders, update the animation time. + if (frame.custom_shader_state) |*state| { + const now = std.time.Instant.now() catch state.first_frame_time; + const since_ns: f32 = @floatFromInt(now.since(state.first_frame_time)); + const delta_ns: f32 = @floatFromInt(now.since(state.last_frame_time)); + state.uniforms.time = since_ns / std.time.ns_per_s; + state.uniforms.time_delta = delta_ns / std.time.ns_per_s; + state.last_frame_time = now; + } + + // If our font atlas changed, sync the texture data + texture: { + const modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); + if (modified <= frame.grayscale_modified) break :texture; + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + frame.grayscale_modified = self.font_grid.atlas_grayscale.modified.load(.monotonic); + try self.syncAtlasTexture(&self.font_grid.atlas_grayscale, &frame.grayscale); + } + texture: { + const modified = self.font_grid.atlas_color.modified.load(.monotonic); + if (modified <= frame.color_modified) break :texture; + self.font_grid.lock.lockShared(); + defer self.font_grid.lock.unlockShared(); + frame.color_modified = self.font_grid.atlas_color.modified.load(.monotonic); + try self.syncAtlasTexture(&self.font_grid.atlas_color, &frame.color); + } + + // Get a frame context from the graphics API. + var frame_ctx = try self.api.beginFrame(self, &frame.target); + defer frame_ctx.complete(sync); + + { + var pass = frame_ctx.renderPass(&.{.{ + .target = if (frame.custom_shader_state) |state| + .{ .texture = state.back_texture } + else + .{ .target = frame.target }, + .clear_color = .{ 0.0, 0.0, 0.0, 0.0 }, + }}); + defer pass.complete(); + + // bg images + try self.drawImagePlacements(&pass, self.image_placements.items[0..self.image_bg_end]); + // bg + pass.step(.{ + .pipeline = self.shaders.cell_bg_pipeline, + .uniforms = frame.uniforms.buffer, + .buffers = &.{ null, frame.cells_bg.buffer }, + .draw = .{ + .type = .triangle, + .vertex_count = 3, + }, + }); + // mg images + try self.drawImagePlacements(&pass, self.image_placements.items[self.image_bg_end..self.image_text_end]); + // text + pass.step(.{ + .pipeline = self.shaders.cell_text_pipeline, + .uniforms = frame.uniforms.buffer, + .buffers = &.{ + frame.cells.buffer, + frame.cells_bg.buffer, + }, + .textures = &.{ + frame.grayscale, + frame.color, + }, + .draw = .{ + .type = .triangle_strip, + .vertex_count = 4, + .instance_count = fg_count, + }, + }); + // fg images + try self.drawImagePlacements(&pass, self.image_placements.items[self.image_text_end..]); + } + + // If we have custom shaders, then we render them. + if (frame.custom_shader_state) |*state| { + // We create a buffer on the GPU for our post uniforms. + // TODO: This should be a part of the frame state tbqh. + const PostBuffer = Buffer(shaderpkg.PostUniforms); + const uniform_buffer = try PostBuffer.initFill( + self.api.bufferOptions(), + &.{state.uniforms}, + ); + defer uniform_buffer.deinit(); + + for (self.shaders.post_pipelines, 0..) |pipeline, i| { + defer state.swap(); + + var pass = frame_ctx.renderPass(&.{.{ + .target = if (i < self.shaders.post_pipelines.len - 1) + .{ .texture = state.front_texture } + else + .{ .target = frame.target }, + .clear_color = .{ 0.0, 0.0, 0.0, 0.0 }, + }}); + defer pass.complete(); + + pass.step(.{ + .pipeline = pipeline, + .uniforms = uniform_buffer.buffer, + .textures = &.{state.back_texture}, + .draw = .{ + .type = .triangle, + .vertex_count = 3, + }, + }); + } + } + } + + // Callback from the graphics API when a frame is completed. + pub fn frameCompleted( + self: *Self, + health: Health, + ) void { + // If our health value hasn't changed, then we do nothing. We don't + // do a cmpxchg here because strict atomicity isn't important. + if (self.health.load(.seq_cst) != health) { + self.health.store(health, .seq_cst); + + // Our health value changed, so we notify the surface so that it + // can do something about it. + _ = self.surface_mailbox.push(.{ + .renderer_health = health, + }, .{ .forever = {} }); + } + + // Always release our semaphore + self.swap_chain.releaseFrame(); + } + + fn drawImagePlacements( + self: *Self, + pass: *RenderPass, + placements: []const imagepkg.Placement, + ) !void { + if (placements.len == 0) return; + + for (placements) |p| { + + // Look up the image + const image = self.images.get(p.image_id) orelse { + log.warn("image not found for placement image_id={}", .{p.image_id}); + return; + }; + + // Get the texture + const texture = switch (image.image) { + .ready => |t| t, + else => { + log.warn("image not ready for placement image_id={}", .{p.image_id}); + return; + }, + }; + + // Create our vertex buffer, which is always exactly one item. + // future(mitchellh): we can group rendering multiple instances of a single image + var buf = try Buffer(shaderpkg.Image).initFill( + self.api.imageBufferOptions(), + &.{.{ + .grid_pos = .{ + @as(f32, @floatFromInt(p.x)), + @as(f32, @floatFromInt(p.y)), + }, + + .cell_offset = .{ + @as(f32, @floatFromInt(p.cell_offset_x)), + @as(f32, @floatFromInt(p.cell_offset_y)), + }, + + .source_rect = .{ + @as(f32, @floatFromInt(p.source_x)), + @as(f32, @floatFromInt(p.source_y)), + @as(f32, @floatFromInt(p.source_width)), + @as(f32, @floatFromInt(p.source_height)), + }, + + .dest_size = .{ + @as(f32, @floatFromInt(p.width)), + @as(f32, @floatFromInt(p.height)), + }, + }}, + ); + defer buf.deinit(); + + pass.step(.{ + .pipeline = self.shaders.image_pipeline, + .buffers = &.{buf.buffer}, + .textures = &.{texture}, + .draw = .{ + .type = .triangle_strip, + .vertex_count = 4, + }, + }); + } + } + + /// This goes through the Kitty graphic placements and accumulates the + /// placements we need to render on our viewport. It also ensures that + /// the visible images are loaded on the GPU. + fn prepKittyGraphics( + self: *Self, + t: *terminal.Terminal, + ) !void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + const storage = &t.screen.kitty_images; + defer storage.dirty = false; + + // We always clear our previous placements no matter what because + // we rebuild them from scratch. + self.image_placements.clearRetainingCapacity(); + self.image_virtual = false; + + // Go through our known images and if there are any that are no longer + // in use then mark them to be freed. + // + // This never conflicts with the below because a placement can't + // reference an image that doesn't exist. + { + var it = self.images.iterator(); + while (it.next()) |kv| { + if (storage.imageById(kv.key_ptr.*) == null) { + kv.value_ptr.image.markForUnload(); + } + } + } + + // The top-left and bottom-right corners of our viewport in screen + // points. This lets us determine offsets and containment of placements. + const top = t.screen.pages.getTopLeft(.viewport); + const bot = t.screen.pages.getBottomRight(.viewport).?; + const top_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y; + const bot_y = t.screen.pages.pointFromPin(.screen, bot).?.screen.y; + + // Go through the placements and ensure the image is loaded on the GPU. + var it = storage.placements.iterator(); + while (it.next()) |kv| { + const p = kv.value_ptr; + + // Special logic based on location + switch (p.location) { + .pin => {}, + .virtual => { + // We need to mark virtual placements on our renderer so that + // we know to rebuild in more scenarios since cell changes can + // now trigger placement changes. + self.image_virtual = true; + + // We also continue out because virtual placements are + // only triggered by the unicode placeholder, not by the + // placement itself. + continue; + }, + } + + // Get the image for the placement + const image = storage.imageById(kv.key_ptr.image_id) orelse { + log.warn( + "missing image for placement, ignoring image_id={}", + .{kv.key_ptr.image_id}, + ); + continue; + }; + + try self.prepKittyPlacement(t, top_y, bot_y, &image, p); + } + + // If we have virtual placements then we need to scan for placeholders. + if (self.image_virtual) { + var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot); + while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement( + t, + &virtual_p, + ); + } + + // Sort the placements by their Z value. + std.mem.sortUnstable( + imagepkg.Placement, + self.image_placements.items, + {}, + struct { + fn lessThan( + ctx: void, + lhs: imagepkg.Placement, + rhs: imagepkg.Placement, + ) bool { + _ = ctx; + return lhs.z < rhs.z or (lhs.z == rhs.z and lhs.image_id < rhs.image_id); + } + }.lessThan, + ); + + // Find our indices. The values are sorted by z so we can find the + // first placement out of bounds to find the limits. + var bg_end: ?u32 = null; + var text_end: ?u32 = null; + const bg_limit = std.math.minInt(i32) / 2; + for (self.image_placements.items, 0..) |p, i| { + if (bg_end == null and p.z >= bg_limit) { + bg_end = @intCast(i); + } + if (text_end == null and p.z >= 0) { + text_end = @intCast(i); + } + } + + self.image_bg_end = bg_end orelse 0; + self.image_text_end = text_end orelse self.image_bg_end; + } + + fn prepKittyVirtualPlacement( + self: *Self, + t: *terminal.Terminal, + p: *const terminal.kitty.graphics.unicode.Placement, + ) !void { + const storage = &t.screen.kitty_images; + const image = storage.imageById(p.image_id) orelse { + log.warn( + "missing image for virtual placement, ignoring image_id={}", + .{p.image_id}, + ); + return; + }; + + const rp = p.renderPlacement( + storage, + &image, + self.grid_metrics.cell_width, + self.grid_metrics.cell_height, + ) catch |err| { + log.warn("error rendering virtual placement err={}", .{err}); + return; + }; + + // If our placement is zero sized then we don't do anything. + if (rp.dest_width == 0 or rp.dest_height == 0) return; + + const viewport: terminal.point.Point = t.screen.pages.pointFromPin( + .viewport, + rp.top_left, + ) orelse { + // This is unreachable with virtual placements because we should + // only ever be looking at virtual placements that are in our + // viewport in the renderer and virtual placements only ever take + // up one row. + unreachable; + }; + + // Send our image to the GPU and store the placement for rendering. + try self.prepKittyImage(&image); + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rp.top_left.x), + .y = @intCast(viewport.viewport.y), + .z = -1, + .width = rp.dest_width, + .height = rp.dest_height, + .cell_offset_x = rp.offset_x, + .cell_offset_y = rp.offset_y, + .source_x = rp.source_x, + .source_y = rp.source_y, + .source_width = rp.source_width, + .source_height = rp.source_height, + }); + } + + fn prepKittyPlacement( + self: *Self, + t: *terminal.Terminal, + top_y: u32, + bot_y: u32, + image: *const terminal.kitty.graphics.Image, + p: *const terminal.kitty.graphics.ImageStorage.Placement, + ) !void { + // Get the rect for the placement. If this placement doesn't have + // a rect then its virtual or something so skip it. + const rect = p.rect(image.*, t) orelse return; + + // This is expensive but necessary. + const img_top_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y; + const img_bot_y = t.screen.pages.pointFromPin(.screen, rect.bottom_right).?.screen.y; + + // If the selection isn't within our viewport then skip it. + if (img_top_y > bot_y) return; + if (img_bot_y < top_y) return; + + // We need to prep this image for upload if it isn't in the cache OR + // it is in the cache but the transmit time doesn't match meaning this + // image is different. + try self.prepKittyImage(image); + + // Calculate the dimensions of our image, taking in to + // account the rows / columns specified by the placement. + const dest_size = p.calculatedSize(image.*, t); + + // Calculate the source rectangle + const source_x = @min(image.width, p.source_x); + const source_y = @min(image.height, p.source_y); + const source_width = if (p.source_width > 0) + @min(image.width - source_x, p.source_width) + else + image.width; + const source_height = if (p.source_height > 0) + @min(image.height - source_y, p.source_height) + else + image.height; + + // Get the viewport-relative Y position of the placement. + const y_pos: i32 = @as(i32, @intCast(img_top_y)) - @as(i32, @intCast(top_y)); + + // Accumulate the placement + if (dest_size.width > 0 and dest_size.height > 0) { + try self.image_placements.append(self.alloc, .{ + .image_id = image.id, + .x = @intCast(rect.top_left.x), + .y = y_pos, + .z = p.z, + .width = dest_size.width, + .height = dest_size.height, + .cell_offset_x = p.x_offset, + .cell_offset_y = p.y_offset, + .source_x = source_x, + .source_y = source_y, + .source_width = source_width, + .source_height = source_height, + }); + } + } + + fn prepKittyImage( + self: *Self, + image: *const terminal.kitty.graphics.Image, + ) !void { + // If this image exists and its transmit time is the same we assume + // it is the identical image so we don't need to send it to the GPU. + const gop = try self.images.getOrPut(self.alloc, image.id); + if (gop.found_existing and + gop.value_ptr.transmit_time.order(image.transmit_time) == .eq) + { + return; + } + + // Copy the data into the pending state. + const data = try self.alloc.dupe(u8, image.data); + errdefer self.alloc.free(data); + + // Store it in the map + const pending: Image.Pending = .{ + .width = image.width, + .height = image.height, + .data = data.ptr, + }; + + const new_image: Image = switch (image.format) { + .gray => .{ .pending_gray = pending }, + .gray_alpha => .{ .pending_gray_alpha = pending }, + .rgb => .{ .pending_rgb = pending }, + .rgba => .{ .pending_rgba = pending }, + .png => unreachable, // should be decoded by now + }; + + if (!gop.found_existing) { + gop.value_ptr.* = .{ + .image = new_image, + .transmit_time = undefined, + }; + } else { + try gop.value_ptr.image.markForReplace( + self.alloc, + new_image, + ); + } + + gop.value_ptr.transmit_time = image.transmit_time; + } + + /// Update the configuration. + pub fn changeConfig(self: *Self, config: *DerivedConfig) !void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We always redo the font shaper in case font features changed. We + // could check to see if there was an actual config change but this is + // easier and rare enough to not cause performance issues. + { + var font_shaper = try font.Shaper.init(self.alloc, .{ + .features = config.font_features.items, + }); + errdefer font_shaper.deinit(); + self.font_shaper.deinit(); + self.font_shaper = font_shaper; + } + + // We also need to reset the shaper cache so shaper info + // from the previous font isn't re-used for the new font. + const font_shaper_cache = font.ShaperCache.init(); + self.font_shaper_cache.deinit(self.alloc); + self.font_shaper_cache = font_shaper_cache; + + // Set our new minimum contrast + self.uniforms.min_contrast = config.min_contrast; + + // Set our new color space and blending + self.uniforms.bools.use_display_p3 = config.colorspace == .@"display-p3"; + self.uniforms.bools.use_linear_blending = config.blending.isLinear(); + self.uniforms.bools.use_linear_correction = config.blending == .@"linear-corrected"; + + // Set our new colors + self.default_background_color = config.background; + self.default_foreground_color = config.foreground; + self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null; + self.cursor_invert = config.cursor_invert; + + const old_blending = self.config.blending; + const custom_shaders_changed = !self.config.custom_shaders.equal(config.custom_shaders); + + self.config.deinit(); + self.config = config.*; + + // Reset our viewport to force a rebuild, in case of a font change. + self.cells_viewport = null; + + const blending_changed = old_blending != config.blending; + + if (blending_changed) { + // We update our API's blending mode. + self.api.blending = config.blending; + // And indicate that we need to reinitialize our shaders. + self.reinitialize_shaders = true; + // And indicate that our swap chain targets need to + // be re-created to account for the new blending mode. + self.target_config_modified +%= 1; + } + + if (custom_shaders_changed) { + self.reinitialize_shaders = true; + } + } + + /// Resize the screen. + pub fn setScreenSize( + self: *Self, + size: renderer.Size, + ) void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // We only actually need the padding from this, + // everything else is derived elsewhere. + self.size.padding = size.padding; + + self.updateScreenSizeUniforms(); + + log.debug("screen size size={}", .{size}); + } + + /// Update uniforms that are based on the screen size. + /// + /// Caller must hold the draw mutex. + fn updateScreenSizeUniforms(self: *Self) void { + const terminal_size = self.size.terminal(); + + // Blank space around the grid. + const blank: renderer.Padding = self.size.screen.blankPadding( + self.size.padding, + .{ + .columns = self.cells.size.columns, + .rows = self.cells.size.rows, + }, + .{ + .width = self.grid_metrics.cell_width, + .height = self.grid_metrics.cell_height, + }, + ).add(self.size.padding); + + // Setup our uniforms + self.uniforms.projection_matrix = math.ortho2d( + -1 * @as(f32, @floatFromInt(self.size.padding.left)), + @floatFromInt(terminal_size.width + self.size.padding.right), + @floatFromInt(terminal_size.height + self.size.padding.bottom), + -1 * @as(f32, @floatFromInt(self.size.padding.top)), + ); + self.uniforms.grid_padding = .{ + @floatFromInt(blank.top), + @floatFromInt(blank.right), + @floatFromInt(blank.bottom), + @floatFromInt(blank.left), + }; + } + + /// Convert the terminal state to GPU cells stored in CPU memory. These + /// are then synced to the GPU in the next frame. This only updates CPU + /// memory and doesn't touch the GPU. + fn rebuildCells( + self: *Self, + wants_rebuild: bool, + screen: *terminal.Screen, + screen_type: terminal.ScreenType, + mouse: renderer.State.Mouse, + preedit: ?renderer.State.Preedit, + cursor_style_: ?renderer.CursorStyle, + color_palette: *const terminal.color.Palette, + ) !void { + self.draw_mutex.lock(); + defer self.draw_mutex.unlock(); + + // const start = try std.time.Instant.now(); + // const start_micro = std.time.microTimestamp(); + // defer { + // const end = std.time.Instant.now() catch unreachable; + // // "[rebuildCells time] \t" + // std.log.warn("[rebuildCells time] {}\t{}", .{start_micro, end.since(start) / std.time.ns_per_us}); + // } + + _ = screen_type; // we might use this again later so not deleting it yet + + // Create an arena for all our temporary allocations while rebuilding + var arena = ArenaAllocator.init(self.alloc); + defer arena.deinit(); + const arena_alloc = arena.allocator(); + + // Create our match set for the links. + var link_match_set: link.MatchSet = if (mouse.point) |mouse_pt| try self.config.links.matchSet( + arena_alloc, + screen, + mouse_pt, + mouse.mods, + ) else .{}; + + // Determine our x/y range for preedit. We don't want to render anything + // here because we will render the preedit separately. + const preedit_range: ?struct { + y: terminal.size.CellCountInt, + x: [2]terminal.size.CellCountInt, + cp_offset: usize, + } = if (preedit) |preedit_v| preedit: { + const range = preedit_v.range(screen.cursor.x, screen.pages.cols - 1); + break :preedit .{ + .y = screen.cursor.y, + .x = .{ range.start, range.end }, + .cp_offset = range.cp_offset, + }; + } else null; + + const grid_size_diff = + self.cells.size.rows != screen.pages.rows or + self.cells.size.columns != screen.pages.cols; + + if (grid_size_diff) { + var new_size = self.cells.size; + new_size.rows = screen.pages.rows; + new_size.columns = screen.pages.cols; + try self.cells.resize(self.alloc, new_size); + + // Update our uniforms accordingly, otherwise + // our background cells will be out of place. + self.uniforms.grid_size = .{ new_size.columns, new_size.rows }; + } + + const rebuild = wants_rebuild or grid_size_diff; + + if (rebuild) { + // If we are doing a full rebuild, then we clear the entire cell buffer. + self.cells.reset(); + + // We also reset our padding extension depending on the screen type + switch (self.config.padding_color) { + .background => {}, + + // For extension, assume we are extending in all directions. + // For "extend" this may be disabled due to heuristics below. + .extend, .@"extend-always" => { + self.uniforms.padding_extend = .{ + .up = true, + .down = true, + .left = true, + .right = true, + }; + }, + } + } + + // We rebuild the cells row-by-row because we + // do font shaping and dirty tracking by row. + var row_it = screen.pages.rowIterator(.left_up, .{ .viewport = .{} }, null); + // If our cell contents buffer is shorter than the screen viewport, + // we render the rows that fit, starting from the bottom. If instead + // the viewport is shorter than the cell contents buffer, we align + // the top of the viewport with the top of the contents buffer. + var y: terminal.size.CellCountInt = @min( + screen.pages.rows, + self.cells.size.rows, + ); + while (row_it.next()) |row| { + // The viewport may have more rows than our cell contents, + // so we need to break from the loop early if we hit y = 0. + if (y == 0) break; + + y -= 1; + + if (!rebuild) { + // Only rebuild if we are doing a full rebuild or this row is dirty. + if (!row.isDirty()) continue; + + // Clear the cells if the row is dirty + self.cells.clear(y); + } + + // True if we want to do font shaping around the cursor. + // We want to do font shaping as long as the cursor is enabled. + const shape_cursor = screen.viewportIsBottom() and + y == screen.cursor.y; + + // We need to get this row's selection, if + // there is one, for proper run splitting. + const row_selection = sel: { + const sel = screen.selection orelse break :sel null; + const pin = screen.pages.pin(.{ .viewport = .{ .y = y } }) orelse + break :sel null; + break :sel sel.containedRow(screen, pin) orelse null; + }; + + // On primary screen, we still apply vertical padding + // extension under certain conditions we feel are safe. + // + // This helps make some scenarios look better while + // avoiding scenarios we know do NOT look good. + switch (self.config.padding_color) { + // These already have the correct values set above. + .background, .@"extend-always" => {}, + + // Apply heuristics for padding extension. + .extend => if (y == 0) { + self.uniforms.padding_extend.up = !row.neverExtendBg( + color_palette, + self.background_color orelse self.default_background_color, + ); + } else if (y == self.cells.size.rows - 1) { + self.uniforms.padding_extend.down = !row.neverExtendBg( + color_palette, + self.background_color orelse self.default_background_color, + ); + }, + } + + // Iterator of runs for shaping. + var run_iter = self.font_shaper.runIterator( + self.font_grid, + screen, + row, + row_selection, + if (shape_cursor) screen.cursor.x else null, + ); + var shaper_run: ?font.shape.TextRun = try run_iter.next(self.alloc); + var shaper_cells: ?[]const font.shape.Cell = null; + var shaper_cells_i: usize = 0; + + const row_cells_all = row.cells(.all); + + // If our viewport is wider than our cell contents buffer, + // we still only process cells up to the width of the buffer. + const row_cells = row_cells_all[0..@min(row_cells_all.len, self.cells.size.columns)]; + + for (row_cells, 0..) |*cell, x| { + // If this cell falls within our preedit range then we + // skip this because preedits are setup separately. + if (preedit_range) |range| preedit: { + // We're not on the preedit line, no actions necessary. + if (range.y != y) break :preedit; + // We're before the preedit range, no actions necessary. + if (x < range.x[0]) break :preedit; + // We're in the preedit range, skip this cell. + if (x <= range.x[1]) continue; + // After exiting the preedit range we need to catch + // the run position up because of the missed cells. + // In all other cases, no action is necessary. + if (x != range.x[1] + 1) break :preedit; + + // Step the run iterator until we find a run that ends + // after the current cell, which will be the soonest run + // that might contain glyphs for our cell. + while (shaper_run) |run| { + if (run.offset + run.cells > x) break; + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + const run = shaper_run orelse break :preedit; + + // If we haven't shaped this run, do so now. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + + // Advance our index until we reach or pass + // our current x position in the shaper cells. + while (shaper_cells.?[shaper_cells_i].x < x) { + shaper_cells_i += 1; + } + } + + const wide = cell.wide; + + const style = row.style(cell); + + const cell_pin: terminal.Pin = cell: { + var copy = row; + copy.x = @intCast(x); + break :cell copy; + }; + + // True if this cell is selected + const selected: bool = if (screen.selection) |sel| + sel.contains(screen, .{ + .node = row.node, + .y = row.y, + .x = @intCast( + // Spacer tails should show the selection + // state of the wide cell they belong to. + if (wide == .spacer_tail) + x -| 1 + else + x, + ), + }) + else + false; + + const bg_style = style.bg(cell, color_palette); + const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color; + + // The final background color for the cell. + const bg = bg: { + if (selected) { + break :bg if (self.config.invert_selection_fg_bg) + if (style.flags.inverse) + // Cell is selected with invert selection fg/bg + // enabled, and the cell has the inverse style + // flag, so they cancel out and we get the normal + // bg color. + bg_style + else + // If it doesn't have the inverse style + // flag then we use the fg color instead. + fg_style + else + // If we don't have invert selection fg/bg set then we + // just use the selection background if set, otherwise + // the default fg color. + break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color; + } + + // Not selected + break :bg if (style.flags.inverse != isCovering(cell.codepoint())) + // Two cases cause us to invert (use the fg color as the bg) + // - The "inverse" style flag. + // - A "covering" glyph; we use fg for bg in that + // case to help make sure that padding extension + // works correctly. + // + // If one of these is true (but not the other) + // then we use the fg style color for the bg. + fg_style + else + // Otherwise they cancel out. + bg_style; + }; + + const fg = fg: { + if (selected and !self.config.invert_selection_fg_bg) { + // If we don't have invert selection fg/bg set + // then we just use the selection foreground if + // set, otherwise the default bg color. + break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color; + } + + // Whether we need to use the bg color as our fg color: + // - Cell is inverted and not selected + // - Cell is selected and not inverted + // Note: if selected then invert sel fg / bg must be + // false since we separately handle it if true above. + break :fg if (style.flags.inverse != selected) + bg_style orelse self.background_color orelse self.default_background_color + else + fg_style; + }; + + // Foreground alpha for this cell. + const alpha: u8 = if (style.flags.faint) 175 else 255; + + // Set the cell's background color. + { + const rgb = bg orelse self.background_color orelse self.default_background_color; + + // Determine our background alpha. If we have transparency configured + // then this is dynamic depending on some situations. This is all + // in an attempt to make transparency look the best for various + // situations. See inline comments. + const bg_alpha: u8 = bg_alpha: { + const default: u8 = 255; + + if (self.config.background_opacity >= 1) break :bg_alpha default; + + // Cells that are selected should be fully opaque. + if (selected) break :bg_alpha default; + + // Cells that are reversed should be fully opaque. + if (style.flags.inverse) break :bg_alpha default; + + // Cells that have an explicit bg color should be fully opaque. + if (bg_style != null) { + break :bg_alpha default; + } + + // Otherwise, we use the configured background opacity. + break :bg_alpha @intFromFloat(@round(self.config.background_opacity * 255.0)); + }; + + self.cells.bgCell(y, x).* = .{ + rgb.r, rgb.g, rgb.b, bg_alpha, + }; + } + + // If the invisible flag is set on this cell then we + // don't need to render any foreground elements, so + // we just skip all glyphs with this x coordinate. + // + // NOTE: This behavior matches xterm. Some other terminal + // emulators, e.g. Alacritty, still render text decorations + // and only make the text itself invisible. The decision + // has been made here to match xterm's behavior for this. + if (style.flags.invisible) { + continue; + } + + // Give links a single underline, unless they already have + // an underline, in which case use a double underline to + // distinguish them. + const underline: terminal.Attribute.Underline = if (link_match_set.contains(screen, cell_pin)) + if (style.flags.underline == .single) + .double + else + .single + else + style.flags.underline; + + // We draw underlines first so that they layer underneath text. + // This improves readability when a colored underline is used + // which intersects parts of the text (descenders). + if (underline != .none) self.addUnderline( + @intCast(x), + @intCast(y), + underline, + style.underlineColor(color_palette) orelse fg, + alpha, + ) catch |err| { + log.warn( + "error adding underline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + if (style.flags.overline) self.addOverline(@intCast(x), @intCast(y), fg, alpha) catch |err| { + log.warn( + "error adding overline to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + + // If we're at or past the end of our shaper run then + // we need to get the next run from the run iterator. + if (shaper_cells != null and shaper_cells_i >= shaper_cells.?.len) { + shaper_run = try run_iter.next(self.alloc); + shaper_cells = null; + shaper_cells_i = 0; + } + + if (shaper_run) |run| glyphs: { + // If we haven't shaped this run yet, do so. + shaper_cells = shaper_cells orelse + // Try to read the cells from the shaping cache if we can. + self.font_shaper_cache.get(run) orelse + cache: { + // Otherwise we have to shape them. + const cells = try self.font_shaper.shape(run); + + // Try to cache them. If caching fails for any reason we + // continue because it is just a performance optimization, + // not a correctness issue. + self.font_shaper_cache.put( + self.alloc, + run, + cells, + ) catch |err| { + log.warn( + "error caching font shaping results err={}", + .{err}, + ); + }; + + // The cells we get from direct shaping are always owned + // by the shaper and valid until the next shaping call so + // we can safely use them. + break :cache cells; + }; + + const cells = shaper_cells orelse break :glyphs; + + // If there are no shaper cells for this run, ignore it. + // This can occur for runs of empty cells, and is fine. + if (cells.len == 0) break :glyphs; + + // If we encounter a shaper cell to the left of the current + // cell then we have some problems. This logic relies on x + // position monotonically increasing. + assert(cells[shaper_cells_i].x >= x); + + // NOTE: An assumption is made here that a single cell will never + // be present in more than one shaper run. If that assumption is + // violated, this logic breaks. + + while (shaper_cells_i < cells.len and cells[shaper_cells_i].x == x) : ({ + shaper_cells_i += 1; + }) { + self.addGlyph( + @intCast(x), + @intCast(y), + cell_pin, + cells[shaper_cells_i], + shaper_run.?, + fg, + alpha, + ) catch |err| { + log.warn( + "error adding glyph to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Finally, draw a strikethrough if necessary. + if (style.flags.strikethrough) self.addStrikethrough( + @intCast(x), + @intCast(y), + fg, + alpha, + ) catch |err| { + log.warn( + "error adding strikethrough to cell, will be invalid x={} y={}, err={}", + .{ x, y, err }, + ); + }; + } + } + + // Setup our cursor rendering information. + cursor: { + // By default, we don't handle cursor inversion on the shader. + self.cells.setCursor(null); + self.uniforms.cursor_pos = .{ + std.math.maxInt(u16), + std.math.maxInt(u16), + }; + + // If we have preedit text, we don't setup a cursor + if (preedit != null) break :cursor; + + // Prepare the cursor cell contents. + const style = cursor_style_ orelse break :cursor; + const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: { + if (self.cursor_invert) { + // Use the foreground color from the cell under the cursor, if any. + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); + break :color if (sty.flags.inverse) + // If the cell is reversed, use background color instead. + (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color) + else + (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color); + } else { + break :color self.foreground_color orelse self.default_foreground_color; + } + }; + + self.addCursor(screen, style, cursor_color); + + // If the cursor is visible then we set our uniforms. + if (style == .block and screen.viewportIsBottom()) { + const wide = screen.cursor.page_cell.wide; + + self.uniforms.cursor_pos = .{ + // If we are a spacer tail of a wide cell, our cursor needs + // to move back one cell. The saturate is to ensure we don't + // overflow but this shouldn't happen with well-formed input. + switch (wide) { + .narrow, .spacer_head, .wide => screen.cursor.x, + .spacer_tail => screen.cursor.x -| 1, + }, + screen.cursor.y, + }; + + self.uniforms.bools.cursor_wide = switch (wide) { + .narrow, .spacer_head => false, + .wide, .spacer_tail => true, + }; + + const uniform_color = if (self.cursor_invert) blk: { + // Use the background color from the cell under the cursor, if any. + const sty = screen.cursor.page_pin.style(screen.cursor.page_cell); + break :blk if (sty.flags.inverse) + // If the cell is reversed, use foreground color instead. + (sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color) + else + (sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color); + } else if (self.config.cursor_text) |txt| + txt + else + self.background_color orelse self.default_background_color; + + self.uniforms.cursor_color = .{ + uniform_color.r, + uniform_color.g, + uniform_color.b, + 255, + }; + } + } + + // Setup our preedit text. + if (preedit) |preedit_v| { + const range = preedit_range.?; + var x = range.x[0]; + for (preedit_v.codepoints[range.cp_offset..]) |cp| { + self.addPreeditCell(cp, .{ .x = x, .y = range.y }) catch |err| { + log.warn("error building preedit cell, will be invalid x={} y={}, err={}", .{ + x, + range.y, + err, + }); + }; + + x += if (cp.wide) 2 else 1; + } + } + + // Update that our cells rebuilt + self.cells_rebuilt = true; + + // Log some things + // log.debug("rebuildCells complete cached_runs={}", .{ + // self.font_shaper_cache.count(), + // }); + } + + /// Add an underline decoration to the specified cell + fn addUnderline( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + style: terminal.Attribute.Underline, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const sprite: font.Sprite = switch (style) { + .none => unreachable, + .single => .underline, + .double => .underline_double, + .dotted => .underline_dotted, + .dashed => .underline_dashed, + .curly => .underline_curly, + }; + + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .underline, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + /// Add a overline decoration to the specified cell + fn addOverline( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.overline), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .overline, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + /// Add a strikethrough decoration to the specified cell + fn addStrikethrough( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const render = try self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(font.Sprite.strikethrough), + .{ + .cell_width = 1, + .grid_metrics = self.grid_metrics, + }, + ); + + try self.cells.add(self.alloc, .strikethrough, .{ + .mode = .fg, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = 1, + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + // Add a glyph to the specified cell. + fn addGlyph( + self: *Self, + x: terminal.size.CellCountInt, + y: terminal.size.CellCountInt, + cell_pin: terminal.Pin, + shaper_cell: font.shape.Cell, + shaper_run: font.shape.TextRun, + color: terminal.color.RGB, + alpha: u8, + ) !void { + const rac = cell_pin.rowAndCell(); + const cell = rac.cell; + + // Render + const render = try self.font_grid.renderGlyph( + self.alloc, + shaper_run.font_index, + shaper_cell.glyph_index, + .{ + .grid_metrics = self.grid_metrics, + .thicken = self.config.font_thicken, + .thicken_strength = self.config.font_thicken_strength, + }, + ); + + // If the glyph is 0 width or height, it will be invisible + // when drawn, so don't bother adding it to the buffer. + if (render.glyph.width == 0 or render.glyph.height == 0) { + return; + } + + const mode: shaderpkg.CellText.Mode = switch (try fgMode( + render.presentation, + cell_pin, + )) { + .normal => .fg, + .color => .fg_color, + .constrained => .fg_constrained, + .powerline => .fg_powerline, + }; + + try self.cells.add(self.alloc, .text, .{ + .mode = mode, + .grid_pos = .{ @intCast(x), @intCast(y) }, + .constraint_width = cell.gridWidth(), + .color = .{ color.r, color.g, color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x + shaper_cell.x_offset), + @intCast(render.glyph.offset_y + shaper_cell.y_offset), + }, + }); + } + + fn addCursor( + self: *Self, + screen: *terminal.Screen, + cursor_style: renderer.CursorStyle, + cursor_color: terminal.color.RGB, + ) void { + // Add the cursor. We render the cursor over the wide character if + // we're on the wide character tail. + const wide, const x = cell: { + // The cursor goes over the screen cursor position. + const cell = screen.cursor.page_cell; + if (cell.wide != .spacer_tail or screen.cursor.x == 0) + break :cell .{ cell.wide == .wide, screen.cursor.x }; + + // If we're part of a wide character, we move the cursor back to + // the actual character. + const prev_cell = screen.cursorCellLeft(1); + break :cell .{ prev_cell.wide == .wide, screen.cursor.x - 1 }; + }; + + const alpha: u8 = if (!self.focused) 255 else alpha: { + const alpha = 255 * self.config.cursor_opacity; + break :alpha @intFromFloat(@ceil(alpha)); + }; + + const render = switch (cursor_style) { + .block, + .block_hollow, + .bar, + .underline, + => render: { + const sprite: font.Sprite = switch (cursor_style) { + .block => .cursor_rect, + .block_hollow => .cursor_hollow_rect, + .bar => .cursor_bar, + .underline => .underline, + .lock => unreachable, + }; + + break :render self.font_grid.renderGlyph( + self.alloc, + font.sprite_index, + @intFromEnum(sprite), + .{ + .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ) catch |err| { + log.warn("error rendering cursor glyph err={}", .{err}); + return; + }; + }, + + .lock => self.font_grid.renderCodepoint( + self.alloc, + 0xF023, // lock symbol + .regular, + .text, + .{ + .cell_width = if (wide) 2 else 1, + .grid_metrics = self.grid_metrics, + }, + ) catch |err| { + log.warn("error rendering cursor glyph err={}", .{err}); + return; + } orelse { + // This should never happen because we embed nerd + // fonts so we just log and return instead of fallback. + log.warn("failed to find lock symbol for cursor codepoint=0xF023", .{}); + return; + }, + }; + + self.cells.setCursor(.{ + .mode = .cursor, + .grid_pos = .{ x, screen.cursor.y }, + .color = .{ cursor_color.r, cursor_color.g, cursor_color.b, alpha }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + fn addPreeditCell( + self: *Self, + cp: renderer.State.Preedit.Codepoint, + coord: terminal.Coordinate, + ) !void { + // Preedit is rendered inverted + const bg = self.foreground_color orelse self.default_foreground_color; + const fg = self.background_color orelse self.default_background_color; + + // Render the glyph for our preedit text + const render_ = self.font_grid.renderCodepoint( + self.alloc, + @intCast(cp.codepoint), + .regular, + .text, + .{ .grid_metrics = self.grid_metrics }, + ) catch |err| { + log.warn("error rendering preedit glyph err={}", .{err}); + return; + }; + const render = render_ orelse { + log.warn("failed to find font for preedit codepoint={X}", .{cp.codepoint}); + return; + }; + + // Add our opaque background cell + self.cells.bgCell(coord.y, coord.x).* = .{ + bg.r, bg.g, bg.b, 255, + }; + if (cp.wide and coord.x < self.cells.size.columns - 1) { + self.cells.bgCell(coord.y, coord.x + 1).* = .{ + bg.r, bg.g, bg.b, 255, + }; + } + + // Add our text + try self.cells.add(self.alloc, .text, .{ + .mode = .fg, + .grid_pos = .{ @intCast(coord.x), @intCast(coord.y) }, + .color = .{ fg.r, fg.g, fg.b, 255 }, + .glyph_pos = .{ render.glyph.atlas_x, render.glyph.atlas_y }, + .glyph_size = .{ render.glyph.width, render.glyph.height }, + .bearings = .{ + @intCast(render.glyph.offset_x), + @intCast(render.glyph.offset_y), + }, + }); + } + + /// Sync the atlas data to the given texture. This copies the bytes + /// associated with the atlas to the given texture. If the atlas no + /// longer fits into the texture, the texture will be resized. + fn syncAtlasTexture( + self: *const Self, + atlas: *const font.Atlas, + texture: *Texture, + ) !void { + if (atlas.size > texture.width) { + // Free our old texture + texture.*.deinit(); + + // Reallocate + texture.* = try self.api.initAtlasTexture(atlas); + } + + try texture.replaceRegion(0, 0, atlas.size, atlas.size, atlas.data); + } + }; +} diff --git a/src/renderer/metal/Frame.zig b/src/renderer/metal/Frame.zig new file mode 100644 index 000000000..81b38e7b6 --- /dev/null +++ b/src/renderer/metal/Frame.zig @@ -0,0 +1,137 @@ +//! Wrapper for handling render passes. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const objc = @import("objc"); + +const mtl = @import("api.zig"); +const Renderer = @import("../generic.zig").Renderer(Metal); +const Metal = @import("../Metal.zig"); +const Target = @import("Target.zig"); +const Pipeline = @import("Pipeline.zig"); +const RenderPass = @import("RenderPass.zig"); +const Buffer = @import("buffer.zig").Buffer; + +const Health = @import("../../renderer.zig").Health; + +const log = std.log.scoped(.metal); + +/// Options for beginning a frame. +pub const Options = struct { + /// MTLCommandQueue + queue: objc.Object, +}; + +/// MTLCommandBuffer +buffer: objc.Object, + +block: CompletionBlock, + +/// Begin encoding a frame. +pub fn begin( + opts: Options, + /// Once the frame has been completed, the `frameCompleted` method + /// on the renderer is called with the health status of the frame. + renderer: *Renderer, + /// The target is presented via the provided renderer's API when completed. + target: *Target, +) !Self { + const buffer = opts.queue.msgSend( + objc.Object, + objc.sel("commandBuffer"), + .{}, + ); + + // Create our block to register for completion updates. + // The block is deallocated by the objC runtime on success. + const block = try CompletionBlock.init( + .{ + .renderer = renderer, + .target = target, + .sync = false, + }, + &bufferCompleted, + ); + errdefer block.deinit(); + + return .{ .buffer = buffer, .block = block }; +} + +/// This is the block type used for the addCompletedHandler callback. +const CompletionBlock = objc.Block(struct { + renderer: *Renderer, + target: *Target, + sync: bool, +}, .{ + objc.c.id, // MTLCommandBuffer +}, void); + +fn bufferCompleted( + block: *const CompletionBlock.Context, + buffer_id: objc.c.id, +) callconv(.c) void { + const buffer = objc.Object.fromId(buffer_id); + + // Get our command buffer status to pass back to the generic renderer. + const status = buffer.getProperty(mtl.MTLCommandBufferStatus, "status"); + const health: Health = switch (status) { + .@"error" => .unhealthy, + else => .healthy, + }; + + // If the frame is healthy, present it. + if (health == .healthy) { + block.renderer.api.present( + block.target.*, + block.sync, + ) catch |err| { + log.err("Failed to present render target: err={}", .{err}); + }; + } + + block.renderer.frameCompleted(health); +} + +/// Add a render pass to this frame with the provided attachments. +/// Returns a RenderPass which allows render steps to be added. +pub inline fn renderPass( + self: *const Self, + attachments: []const RenderPass.Options.Attachment, +) RenderPass { + return RenderPass.begin(.{ + .attachments = attachments, + .command_buffer = self.buffer, + }); +} + +/// Complete this frame and present the target. +/// +/// If `sync` is true, this will block until the frame is presented. +pub inline fn complete(self: *Self, sync: bool) void { + // If we don't need to complete synchronously, + // we add our block as a completion handler. + // + // It will be deallocated by the objc runtime on success. + if (!sync) { + self.buffer.msgSend( + void, + objc.sel("addCompletedHandler:"), + .{self.block.context}, + ); + } + + self.buffer.msgSend(void, objc.sel("commit"), .{}); + + // If we need to complete synchronously, we wait until + // the buffer is completed and call the callback directly, + // deiniting the block after we're done. + if (sync) { + self.buffer.msgSend(void, "waitUntilCompleted", .{}); + self.block.context.sync = true; + bufferCompleted(self.block.context, self.buffer.value); + self.block.deinit(); + } +} diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig new file mode 100644 index 000000000..4c51a55c2 --- /dev/null +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -0,0 +1,187 @@ +//! A wrapper around a CALayer with a utility method +//! for settings its `contents` to an IOSurface. +const IOSurfaceLayer = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const objc = @import("objc"); +const macos = @import("macos"); + +const IOSurface = macos.iosurface.IOSurface; + +const log = std.log.scoped(.IOSurfaceLayer); + +/// We subclass CALayer with a custom display handler, we only need +/// to make the subclass once, and then we can use it as a singleton. +var Subclass: ?objc.Class = null; + +/// The underlying CALayer +layer: objc.Object, + +pub fn init() !IOSurfaceLayer { + const layer = (try getSubclass()).msgSend( + objc.Object, + objc.sel("layer"), + .{}, + ); + errdefer layer.release(); + + // The layer gravity is set to top-left so that the contents aren't + // stretched during resize operations before a new frame has been drawn. + layer.setProperty("contentsGravity", macos.animation.kCAGravityTopLeft); + + layer.setInstanceVariable("display_cb", .{ .value = null }); + layer.setInstanceVariable("display_ctx", .{ .value = null }); + + return .{ .layer = layer }; +} + +pub fn release(self: *IOSurfaceLayer) void { + self.layer.release(); +} + +/// Sets the layer's `contents` to the provided IOSurface. +/// +/// Makes sure to do so on the main thread to avoid visual artifacts. +pub inline fn setSurface(self: *IOSurfaceLayer, surface: *IOSurface) !void { + // We retain the surface to make sure it's not GC'd + // before we can set it as the contents of the layer. + // + // We release in the callback after setting the contents. + surface.retain(); + // We also need to retain the layer itself to make sure it + // isn't destroyed before the callback completes, since if + // that happens it will try to interact with a deallocated + // object. + _ = self.layer.retain(); + + var block = try SetSurfaceBlock.init(.{ + .layer = self.layer.value, + .surface = surface, + }, &setSurfaceCallback); + + // We check if we're on the main thread and run the block directly if so. + const NSThread = objc.getClass("NSThread").?; + if (NSThread.msgSend(bool, "isMainThread", .{})) { + setSurfaceCallback(block.context); + block.deinit(); + } else { + // NOTE: The block will automatically be deallocated by the objc + // runtime once it's executed, so there's no need to deinit it. + + macos.dispatch.dispatch_async( + @ptrCast(macos.dispatch.queue.getMain()), + @ptrCast(block.context), + ); + } +} + +/// Sets the layer's `contents` to the provided IOSurface. +/// +/// Does not ensure this happens on the main thread. +pub inline fn setSurfaceSync(self: *IOSurfaceLayer, surface: *IOSurface) void { + self.layer.setProperty("contents", surface); +} + +const SetSurfaceBlock = objc.Block(struct { + layer: objc.c.id, + surface: *IOSurface, +}, .{}, void); + +fn setSurfaceCallback( + block: *const SetSurfaceBlock.Context, +) callconv(.c) void { + const layer = objc.Object.fromId(block.layer); + const surface: *IOSurface = block.surface; + + // See explanation of why we retain and release in `setSurface`. + defer { + surface.release(); + layer.release(); + } + + // We check to see if the surface is the appropriate size for + // the layer, if it's not then we discard it. This is because + // asynchronously drawn frames can sometimes finish just after + // a synchronously drawn frame during a resize, and if we don't + // discard the improperly sized surface it creates jank. + const bounds = layer.getProperty(macos.graphics.Rect, "bounds"); + const scale = layer.getProperty(f64, "contentsScale"); + const width: usize = @intFromFloat(bounds.size.width * scale); + const height: usize = @intFromFloat(bounds.size.height * scale); + if (width != surface.getWidth() or height != surface.getHeight()) { + log.debug( + "setSurfaceCallback(): surface is wrong size for layer, discarding. surface = {d}x{d}, layer = {d}x{d}", + .{ surface.getWidth(), surface.getHeight(), width, height }, + ); + return; + } + + layer.setProperty("contents", surface); +} + +pub const DisplayCallback = ?*align(8) const fn (?*anyopaque) void; + +pub fn setDisplayCallback( + self: *IOSurfaceLayer, + display_cb: DisplayCallback, + display_ctx: ?*anyopaque, +) void { + self.layer.setInstanceVariable( + "display_cb", + objc.Object.fromId(@constCast(display_cb)), + ); + self.layer.setInstanceVariable( + "display_ctx", + objc.Object.fromId(display_ctx), + ); +} + +fn getSubclass() error{ObjCFailed}!objc.Class { + if (Subclass) |c| return c; + + const CALayer = + objc.getClass("CALayer") orelse return error.ObjCFailed; + + var subclass = + objc.allocateClassPair(CALayer, "IOSurfaceLayer") orelse return error.ObjCFailed; + errdefer objc.disposeClassPair(subclass); + + if (!subclass.addIvar("display_cb")) return error.ObjCFailed; + if (!subclass.addIvar("display_ctx")) return error.ObjCFailed; + + subclass.replaceMethod("display", struct { + fn display(target: objc.c.id, sel: objc.c.SEL) callconv(.c) void { + _ = sel; + const self = objc.Object.fromId(target); + const display_cb: DisplayCallback = @ptrFromInt(@intFromPtr( + self.getInstanceVariable("display_cb").value, + )); + if (display_cb) |cb| cb( + @ptrCast(self.getInstanceVariable("display_ctx").value), + ); + } + }.display); + + // Disable all animations for this layer by returning null for all actions. + subclass.replaceMethod("actionForKey:", struct { + fn actionForKey( + target: objc.c.id, + sel: objc.c.SEL, + key: objc.c.id, + ) callconv(.c) objc.c.id { + _ = target; + _ = sel; + _ = key; + return objc.getClass("NSNull").?.msgSend(objc.c.id, "null", .{}); + } + }.actionForKey); + + objc.registerClassPair(subclass); + + Subclass = subclass; + + return subclass; +} diff --git a/src/renderer/metal/Pipeline.zig b/src/renderer/metal/Pipeline.zig new file mode 100644 index 000000000..f72aeb2e1 --- /dev/null +++ b/src/renderer/metal/Pipeline.zig @@ -0,0 +1,203 @@ +//! Wrapper for handling render pipelines. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const macos = @import("macos"); +const objc = @import("objc"); + +const mtl = @import("api.zig"); +const Texture = @import("Texture.zig"); +const Metal = @import("../Metal.zig"); + +const log = std.log.scoped(.metal); + +/// Options for initializing a render pipeline. +pub const Options = struct { + /// MTLDevice + device: objc.Object, + + /// Name of the vertex function + vertex_fn: []const u8, + /// Name of the fragment function + fragment_fn: []const u8, + + /// MTLLibrary to get the vertex function from + vertex_library: objc.Object, + /// MTLLibrary to get the fragment function from + fragment_library: objc.Object, + + /// Vertex step function + step_fn: mtl.MTLVertexStepFunction = .per_vertex, + + /// Info about the color attachments used by this render pipeline. + attachments: []const Attachment, + + /// Describes a color attachment. + pub const Attachment = struct { + pixel_format: mtl.MTLPixelFormat, + blending_enabled: bool = true, + }; +}; + +/// MTLRenderPipelineState +state: objc.Object, + +pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self { + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLRenderPipelineDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer desc.msgSend(void, objc.sel("release"), .{}); + + // Get our vertex and fragment functions and add them to the descriptor. + { + const str = try macos.foundation.String.createWithBytes( + opts.vertex_fn, + .utf8, + false, + ); + defer str.release(); + + const ptr = opts.vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + const func_vert = objc.Object.fromId(ptr.?); + defer func_vert.msgSend(void, objc.sel("release"), .{}); + + desc.setProperty("vertexFunction", func_vert); + } + { + const str = try macos.foundation.String.createWithBytes( + opts.fragment_fn, + .utf8, + false, + ); + defer str.release(); + + const ptr = opts.fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); + const func_frag = objc.Object.fromId(ptr.?); + defer func_frag.msgSend(void, objc.sel("release"), .{}); + + desc.setProperty("fragmentFunction", func_frag); + } + + // If we have vertex attributes, create and add a vertex descriptor. + if (VertexAttributes) |V| { + const vertex_desc = init: { + const Class = objc.getClass("MTLVertexDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + defer vertex_desc.msgSend(void, objc.sel("release"), .{}); + + // Our attributes are the fields of the input + const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes")); + autoAttribute(V, attrs); + + // The layout describes how and when we fetch the next vertex input. + const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts")); + { + const layout = layouts.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, 0)}, + ); + + layout.setProperty("stepFunction", @intFromEnum(opts.step_fn)); + layout.setProperty("stride", @as(c_ulong, @sizeOf(V))); + } + + desc.setProperty("vertexDescriptor", vertex_desc); + } + + // Set our color attachment + const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); + for (opts.attachments, 0..) |at, i| { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, i)}, + ); + + attachment.setProperty("pixelFormat", @intFromEnum(at.pixel_format)); + + attachment.setProperty("blendingEnabled", at.blending_enabled); + // We always use premultiplied alpha blending for now. + if (at.blending_enabled) { + attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); + attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); + attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); + } + } + + // Make our state + var err: ?*anyopaque = null; + const pipeline_state = opts.device.msgSend( + objc.Object, + objc.sel("newRenderPipelineStateWithDescriptor:error:"), + .{ desc, &err }, + ); + try checkError(err); + errdefer pipeline_state.release(); + + return .{ .state = pipeline_state }; +} + +pub fn deinit(self: *const Self) void { + self.state.release(); +} + +fn autoAttribute(T: type, attrs: objc.Object) void { + inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| { + const offset = @offsetOf(T, field.name); + + const FT = switch (@typeInfo(field.type)) { + .@"enum" => |e| e.tag_type, + else => field.type, + }; + + // Very incomplete list, expand as necessary. + const format = switch (FT) { + [4]u8 => mtl.MTLVertexFormat.uchar4, + [2]u16 => mtl.MTLVertexFormat.ushort2, + [2]i16 => mtl.MTLVertexFormat.short2, + [2]f32 => mtl.MTLVertexFormat.float2, + [4]f32 => mtl.MTLVertexFormat.float4, + [2]i32 => mtl.MTLVertexFormat.int2, + u32 => mtl.MTLVertexFormat.uint, + [2]u32 => mtl.MTLVertexFormat.uint2, + [4]u32 => mtl.MTLVertexFormat.uint4, + u8 => mtl.MTLVertexFormat.uchar, + else => comptime unreachable, + }; + + const attr = attrs.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, i)}, + ); + + attr.setProperty("format", @intFromEnum(format)); + attr.setProperty("offset", @as(c_ulong, offset)); + attr.setProperty("bufferIndex", @as(c_ulong, 0)); + } +} + +fn checkError(err_: ?*anyopaque) !void { + const nserr = objc.Object.fromId(err_ orelse return); + const str = @as( + *macos.foundation.String, + @ptrCast(nserr.getProperty(?*anyopaque, "localizedDescription").?), + ); + + log.err("metal error={s}", .{str.cstringPtr(.ascii).?}); + return error.MetalFailed; +} diff --git a/src/renderer/metal/RenderPass.zig b/src/renderer/metal/RenderPass.zig new file mode 100644 index 000000000..e48bc4c00 --- /dev/null +++ b/src/renderer/metal/RenderPass.zig @@ -0,0 +1,220 @@ +//! Wrapper for handling render passes. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const objc = @import("objc"); + +const mtl = @import("api.zig"); +const Pipeline = @import("Pipeline.zig"); +const Texture = @import("Texture.zig"); +const Target = @import("Target.zig"); +const Metal = @import("../Metal.zig"); +const Buffer = @import("buffer.zig").Buffer; + +const log = std.log.scoped(.metal); + +/// Options for beginning a render pass. +pub const Options = struct { + /// MTLCommandBuffer + command_buffer: objc.Object, + /// Color attachments for this render pass. + attachments: []const Attachment, + + /// Describes a color attachment. + pub const Attachment = struct { + target: union(enum) { + texture: Texture, + target: Target, + }, + clear_color: ?[4]f64 = null, + }; +}; + +/// Describes a step in a render pass. +pub const Step = struct { + pipeline: Pipeline, + /// MTLBuffer + uniforms: ?objc.Object = null, + /// MTLBuffer + buffers: []const ?objc.Object = &.{}, + textures: []const ?Texture = &.{}, + draw: Draw, + + /// Describes the draw call for this step. + pub const Draw = struct { + type: mtl.MTLPrimitiveType, + vertex_count: usize, + instance_count: usize = 1, + }; +}; + +/// MTLRenderCommandEncoder +encoder: objc.Object, + +/// Begin a render pass. +pub fn begin( + opts: Options, +) Self { + // Create a pass descriptor + const desc = desc: { + const MTLRenderPassDescriptor = objc.getClass("MTLRenderPassDescriptor").?; + const desc = MTLRenderPassDescriptor.msgSend( + objc.Object, + objc.sel("renderPassDescriptor"), + .{}, + ); + + // Set our color attachment to be our drawable surface. + const attachments = objc.Object.fromId( + desc.getProperty(?*anyopaque, "colorAttachments"), + ); + for (opts.attachments, 0..) |at, i| { + const attachment = attachments.msgSend( + objc.Object, + objc.sel("objectAtIndexedSubscript:"), + .{@as(c_ulong, i)}, + ); + + attachment.setProperty( + "loadAction", + @intFromEnum(@as( + mtl.MTLLoadAction, + if (at.clear_color != null) + .clear + else + .load, + )), + ); + attachment.setProperty( + "storeAction", + @intFromEnum(mtl.MTLStoreAction.store), + ); + attachment.setProperty("texture", switch (at.target) { + .texture => |t| t.texture.value, + .target => |t| t.texture.value, + }); + if (at.clear_color) |c| attachment.setProperty( + "clearColor", + mtl.MTLClearColor{ + .red = c[0], + .green = c[1], + .blue = c[2], + .alpha = c[3], + }, + ); + } + + break :desc desc; + }; + + // MTLRenderCommandEncoder + const encoder = opts.command_buffer.msgSend( + objc.Object, + objc.sel("renderCommandEncoderWithDescriptor:"), + .{desc.value}, + ); + + return .{ .encoder = encoder }; +} + +/// Add a step to this render pass. +pub fn step(self: *const Self, s: Step) void { + if (s.draw.instance_count == 0) return; + + // Set pipeline state + self.encoder.msgSend( + void, + objc.sel("setRenderPipelineState:"), + .{s.pipeline.state.value}, + ); + + if (s.buffers.len > 0) { + // We reserve index 0 for the vertex buffer, this isn't very + // flexible but it lines up with the API we have for OpenGL. + if (s.buffers[0]) |buf| { + self.encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + self.encoder.msgSend( + void, + objc.sel("setFragmentBuffer:offset:atIndex:"), + .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 0) }, + ); + } + + // Set the rest of the buffers starting at index 2, this is + // so that we can use index 1 for the uniforms if present. + // + // Also, we set buffers (and textures) for both stages. + // + // Again, not very flexible, but it's consistent and predictable, + // and we need to treat the uniforms as special because of OpenGL. + // + // TODO: Maybe in the future add info to the pipeline struct which + // allows it to define a mapping between provided buffers and + // what index they get set at for the vertex / fragment stage. + for (s.buffers[1..], 2..) |b, i| if (b) |buf| { + self.encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) }, + ); + self.encoder.msgSend( + void, + objc.sel("setFragmentBuffer:offset:atIndex:"), + .{ buf.value, @as(c_ulong, 0), @as(c_ulong, i) }, + ); + }; + } + + // Set the uniforms as buffer index 1 if present. + if (s.uniforms) |buf| { + self.encoder.msgSend( + void, + objc.sel("setVertexBuffer:offset:atIndex:"), + .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) }, + ); + self.encoder.msgSend( + void, + objc.sel("setFragmentBuffer:offset:atIndex:"), + .{ buf.value, @as(c_ulong, 0), @as(c_ulong, 1) }, + ); + } + + // Set textures. + for (s.textures, 0..) |t, i| if (t) |tex| { + self.encoder.msgSend( + void, + objc.sel("setVertexTexture:atIndex:"), + .{ tex.texture.value, @as(c_ulong, i) }, + ); + self.encoder.msgSend( + void, + objc.sel("setFragmentTexture:atIndex:"), + .{ tex.texture.value, @as(c_ulong, i) }, + ); + }; + + // Draw! + self.encoder.msgSend( + void, + objc.sel("drawPrimitives:vertexStart:vertexCount:instanceCount:"), + .{ + @intFromEnum(s.draw.type), + @as(c_ulong, 0), + @as(c_ulong, s.draw.vertex_count), + @as(c_ulong, s.draw.instance_count), + }, + ); +} + +/// Complete this render pass. +/// This struct can no longer be used after calling this. +pub fn complete(self: *const Self) void { + self.encoder.msgSend(void, objc.sel("endEncoding"), .{}); +} diff --git a/src/renderer/metal/Target.zig b/src/renderer/metal/Target.zig new file mode 100644 index 000000000..fa62d3014 --- /dev/null +++ b/src/renderer/metal/Target.zig @@ -0,0 +1,110 @@ +//! Represents a render target. +//! +//! In this case, an IOSurface-backed MTLTexture. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const objc = @import("objc"); +const macos = @import("macos"); +const graphics = macos.graphics; +const IOSurface = macos.iosurface.IOSurface; + +const mtl = @import("api.zig"); + +const log = std.log.scoped(.metal); + +/// Options for initializing a Target +pub const Options = struct { + /// MTLDevice + device: objc.Object, + + /// Desired width + width: usize, + /// Desired height + height: usize, + + /// Pixel format for the MTLTexture + pixel_format: mtl.MTLPixelFormat, + /// Storage mode for the MTLTexture + storage_mode: mtl.MTLResourceOptions.StorageMode, +}; + +/// The underlying IOSurface. +surface: *IOSurface, + +/// The underlying MTLTexture. +texture: objc.Object, + +/// Current width of this target. +width: usize, +/// Current height of this target. +height: usize, + +pub fn init(opts: Options) !Self { + // We set our surface's color space to Display P3. + // This allows us to have "Apple-style" alpha blending, + // since it seems to be the case that Apple apps like + // Terminal and TextEdit render text in the display's + // color space using converted colors, which reduces, + // but does not fully eliminate blending artifacts. + const colorspace = try graphics.ColorSpace.createNamed(.displayP3); + defer colorspace.release(); + + const surface = try IOSurface.init(.{ + .width = @intCast(opts.width), + .height = @intCast(opts.height), + .pixel_format = .@"32BGRA", + .bytes_per_element = 4, + .colorspace = colorspace, + }); + + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLTextureDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + errdefer desc.msgSend(void, objc.sel("release"), .{}); + + // Set our properties + desc.setProperty("width", @as(c_ulong, @intCast(opts.width))); + desc.setProperty("height", @as(c_ulong, @intCast(opts.height))); + desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format)); + desc.setProperty("usage", mtl.MTLTextureUsage{ .render_target = true }); + desc.setProperty( + "resourceOptions", + mtl.MTLResourceOptions{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = opts.storage_mode, + }, + ); + + const id = opts.device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:iosurface:plane:"), + .{ + desc, + surface, + @as(c_ulong, 0), + }, + ) orelse return error.MetalFailed; + + const texture = objc.Object.fromId(id); + + return .{ + .surface = surface, + .texture = texture, + .width = opts.width, + .height = opts.height, + }; +} + +pub fn deinit(self: *Self) void { + self.surface.deinit(); + self.texture.release(); +} diff --git a/src/renderer/metal/Texture.zig b/src/renderer/metal/Texture.zig new file mode 100644 index 000000000..6e3ae78c7 --- /dev/null +++ b/src/renderer/metal/Texture.zig @@ -0,0 +1,196 @@ +//! Wrapper for handling textures. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const objc = @import("objc"); + +const mtl = @import("api.zig"); +const Metal = @import("../Metal.zig"); + +const log = std.log.scoped(.metal); + +/// Options for initializing a texture. +pub const Options = struct { + /// MTLDevice + device: objc.Object, + pixel_format: mtl.MTLPixelFormat, + resource_options: mtl.MTLResourceOptions, +}; + +/// The underlying MTLTexture Object. +texture: objc.Object, + +/// The width of this texture. +width: usize, +/// The height of this texture. +height: usize, + +/// Bytes per pixel for this texture. +bpp: usize, + +/// Initialize a texture +pub fn init( + opts: Options, + width: usize, + height: usize, + data: ?[]const u8, +) !Self { + // Create our descriptor + const desc = init: { + const Class = objc.getClass("MTLTextureDescriptor").?; + const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); + const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); + break :init id_init; + }; + errdefer desc.msgSend(void, objc.sel("release"), .{}); + + // Set our properties + desc.setProperty("pixelFormat", @intFromEnum(opts.pixel_format)); + desc.setProperty("width", @as(c_ulong, width)); + desc.setProperty("height", @as(c_ulong, height)); + desc.setProperty("resourceOptions", opts.resource_options); + + // Initialize + const id = opts.device.msgSend( + ?*anyopaque, + objc.sel("newTextureWithDescriptor:"), + .{desc}, + ) orelse return error.MetalFailed; + + const self: Self = .{ + .texture = objc.Object.fromId(id), + .width = width, + .height = height, + .bpp = bppOf(opts.pixel_format), + }; + + // If we have data, we set it here. + if (data) |d| { + assert(d.len == width * height * self.bpp); + try self.replaceRegion(0, 0, width, height, d); + } + + return self; +} + +pub fn deinit(self: Self) void { + self.texture.release(); +} + +/// Replace a region of the texture with the provided data. +/// +/// Does NOT check the dimensions of the data to ensure correctness. +pub fn replaceRegion( + self: Self, + x: usize, + y: usize, + width: usize, + height: usize, + data: []const u8, +) !void { + self.texture.msgSend( + void, + objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), + .{ + mtl.MTLRegion{ + .origin = .{ .x = x, .y = y, .z = 0 }, + .size = .{ + .width = @intCast(width), + .height = @intCast(height), + .depth = 1, + }, + }, + @as(c_ulong, 0), + @as(*const anyopaque, data.ptr), + @as(c_ulong, self.bpp * width), + }, + ); +} + +/// Returns the bytes per pixel for the provided pixel format +fn bppOf(pixel_format: mtl.MTLPixelFormat) usize { + return switch (pixel_format) { + // Invalid + .invalid => @panic("invalid pixel format"), + + // Weird formats I was too lazy to get the sizes of + else => @panic("pixel format size unknown (unlikely that this format was actually used, could be memory corruption)"), + + // 8-bit pixel formats + .a8unorm, + .r8unorm, + .r8unorm_srgb, + .r8snorm, + .r8uint, + .r8sint, + .rg8unorm, + .rg8unorm_srgb, + .rg8snorm, + .rg8uint, + .rg8sint, + .stencil8, + => 1, + + // 16-bit pixel formats + .r16unorm, + .r16snorm, + .r16uint, + .r16sint, + .r16float, + .rg16unorm, + .rg16snorm, + .rg16uint, + .rg16sint, + .rg16float, + .b5g6r5unorm, + .a1bgr5unorm, + .abgr4unorm, + .bgr5a1unorm, + .depth16unorm, + => 2, + + // 32-bit pixel formats + .rgba8unorm, + .rgba8unorm_srgb, + .rgba8snorm, + .rgba8uint, + .rgba8sint, + .bgra8unorm, + .bgra8unorm_srgb, + .rgb10a2unorm, + .rgb10a2uint, + .rg11b10float, + .rgb9e5float, + .bgr10a2unorm, + .bgr10_xr, + .bgr10_xr_srgb, + .r32uint, + .r32sint, + .r32float, + .depth32float, + .depth24unorm_stencil8, + => 4, + + // 64-bit pixel formats + .rg32uint, + .rg32sint, + .rg32float, + .rgba16unorm, + .rgba16snorm, + .rgba16uint, + .rgba16sint, + .rgba16float, + .bgra10_xr, + .bgra10_xr_srgb, + => 8, + + // 128-bit pixel formats, + .rgba32uint, + .rgba32sint, + .rgba32float, + => 128, + }; +} diff --git a/src/renderer/metal/api.zig b/src/renderer/metal/api.zig index 90a1a65ab..e1daa6848 100644 --- a/src/renderer/metal/api.zig +++ b/src/renderer/metal/api.zig @@ -366,7 +366,7 @@ pub const MTLTextureUsage = packed struct(c_ulong) { /// https://developer.apple.com/documentation/metal/mtltextureusage/shaderatomic?language=objc shader_atomic: bool = false, // TextureUsageShaderAtomic = 32, - __reserved: @Type(.{ .Int = .{ + __reserved: @Type(.{ .int = .{ .signedness = .unsigned, .bits = @bitSizeOf(c_ulong) - 6, } }) = 0, @@ -375,6 +375,22 @@ pub const MTLTextureUsage = packed struct(c_ulong) { const unknown: MTLTextureUsage = @bitCast(0); // TextureUsageUnknown = 0, }; +/// https://developer.apple.com/documentation/metal/mtlbarrierscope?language=objc +pub const MTLBarrierScope = enum(c_ulong) { + buffers = 1, + textures = 2, + render_targets = 4, +}; + +/// https://developer.apple.com/documentation/metal/mtlrenderstages?language=objc +pub const MTLRenderStage = enum(c_ulong) { + vertex = 1, + fragment = 2, + tile = 4, + object = 8, + mesh = 16, +}; + pub const MTLClearColor = extern struct { red: f64, green: f64, diff --git a/src/renderer/metal/buffer.zig b/src/renderer/metal/buffer.zig index 4128e297b..43320a60b 100644 --- a/src/renderer/metal/buffer.zig +++ b/src/renderer/metal/buffer.zig @@ -5,9 +5,17 @@ const objc = @import("objc"); const macos = @import("macos"); const mtl = @import("api.zig"); +const Metal = @import("../Metal.zig"); const log = std.log.scoped(.metal); +/// Options for initializing a buffer. +pub const Options = struct { + /// MTLDevice + device: objc.Object, + resource_options: mtl.MTLResourceOptions, +}; + /// Metal data storage for a certain set of equal types. This is usually /// used for vertex buffers, etc. This helpful wrapper makes it easy to /// prealloc, shrink, grow, sync, buffers with Metal. @@ -15,74 +23,57 @@ pub fn Buffer(comptime T: type) type { return struct { const Self = @This(); - /// The resource options for this buffer. - options: mtl.MTLResourceOptions, + /// The options this buffer was initialized with. + opts: Options, - buffer: objc.Object, // MTLBuffer + /// The underlying MTLBuffer object. + buffer: objc.Object, + + /// The allocated length of the buffer. + /// Note that this is the number + /// of `T`s not the size in bytes. + len: usize, /// Initialize a buffer with the given length pre-allocated. - pub fn init( - device: objc.Object, - len: usize, - options: mtl.MTLResourceOptions, - ) !Self { - const buffer = device.msgSend( + pub fn init(opts: Options, len: usize) !Self { + const buffer = opts.device.msgSend( objc.Object, objc.sel("newBufferWithLength:options:"), .{ @as(c_ulong, @intCast(len * @sizeOf(T))), - options, + opts.resource_options, }, ); - return .{ .buffer = buffer, .options = options }; + return .{ .buffer = buffer, .opts = opts, .len = len }; } /// Init the buffer filled with the given data. - pub fn initFill( - device: objc.Object, - data: []const T, - options: mtl.MTLResourceOptions, - ) !Self { - const buffer = device.msgSend( + pub fn initFill(opts: Options, data: []const T) !Self { + const buffer = opts.device.msgSend( objc.Object, objc.sel("newBufferWithBytes:length:options:"), .{ @as(*const anyopaque, @ptrCast(data.ptr)), @as(c_ulong, @intCast(data.len * @sizeOf(T))), - options, + opts.resource_options, }, ); - return .{ .buffer = buffer, .options = options }; + return .{ .buffer = buffer, .opts = opts, .len = data.len }; } - pub fn deinit(self: *Self) void { + pub fn deinit(self: *const Self) void { self.buffer.msgSend(void, objc.sel("release"), .{}); } - /// Get the buffer contents as a slice of T. The contents are - /// mutable. The contents may or may not be automatically synced - /// depending on the buffer storage mode. See the Metal docs. - pub fn contents(self: *Self) ![]T { - const len_bytes = self.buffer.getProperty(c_ulong, "length"); - assert(@mod(len_bytes, @sizeOf(T)) == 0); - const len = @divExact(len_bytes, @sizeOf(T)); - const ptr = self.buffer.msgSend( - ?[*]T, - objc.sel("contents"), - .{}, - ).?; - return ptr[0..len]; - } - /// Sync new contents to the buffer. The data is expected to be the /// complete contents of the buffer. If the amount of data is larger /// than the buffer length, the buffer will be reallocated. /// /// If the amount of data is smaller than the buffer length, the /// remaining data in the buffer is left untouched. - pub fn sync(self: *Self, device: objc.Object, data: []const T) !void { + pub fn sync(self: *Self, data: []const T) !void { // If we need more bytes than our buffer has, we need to reallocate. const req_bytes = data.len * @sizeOf(T); const avail_bytes = self.buffer.getProperty(c_ulong, "length"); @@ -92,12 +83,12 @@ pub fn Buffer(comptime T: type) type { // Allocate a new buffer with enough to hold double what we require. const size = req_bytes * 2; - self.buffer = device.msgSend( + self.buffer = self.opts.device.msgSend( objc.Object, objc.sel("newBufferWithLength:options:"), .{ @as(c_ulong, @intCast(size * @sizeOf(T))), - self.options, + self.opts.resource_options, }, ); } @@ -123,7 +114,7 @@ pub fn Buffer(comptime T: type) type { // we need to signal Metal to synchronize the buffer data. // // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc - if (self.options.storage_mode == .managed) { + if (self.opts.resource_options.storage_mode == .managed) { self.buffer.msgSend( void, "didModifyRange:", @@ -134,7 +125,7 @@ pub fn Buffer(comptime T: type) type { /// Like Buffer.sync but takes data from an array of ArrayLists, /// rather than a single array. Returns the number of items synced. - pub fn syncFromArrayLists(self: *Self, device: objc.Object, lists: []std.ArrayListUnmanaged(T)) !usize { + pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize { var total_len: usize = 0; for (lists) |list| { total_len += list.items.len; @@ -149,12 +140,12 @@ pub fn Buffer(comptime T: type) type { // Allocate a new buffer with enough to hold double what we require. const size = req_bytes * 2; - self.buffer = device.msgSend( + self.buffer = self.opts.device.msgSend( objc.Object, objc.sel("newBufferWithLength:options:"), .{ @as(c_ulong, @intCast(size * @sizeOf(T))), - self.options, + self.opts.resource_options, }, ); } @@ -181,7 +172,7 @@ pub fn Buffer(comptime T: type) type { // we need to signal Metal to synchronize the buffer data. // // Ref: https://developer.apple.com/documentation/metal/synchronizing-a-managed-resource-in-macos?language=objc - if (self.options.storage_mode == .managed) { + if (self.opts.resource_options.storage_mode == .managed) { self.buffer.msgSend( void, "didModifyRange:", diff --git a/src/renderer/metal/image.zig b/src/renderer/metal/image.zig index 7d2599308..1bfa3c621 100644 --- a/src/renderer/metal/image.zig +++ b/src/renderer/metal/image.zig @@ -4,6 +4,9 @@ const assert = std.debug.assert; const objc = @import("objc"); const wuffs = @import("wuffs"); +const Metal = @import("../Metal.zig"); +const Texture = Metal.Texture; + const mtl = @import("api.zig"); /// Represents a single image placement on the grid. A placement is a @@ -61,15 +64,15 @@ pub const Image = union(enum) { replace_rgba: Replace, /// The image is uploaded and ready to be used. - ready: objc.Object, // MTLTexture + ready: Texture, /// The image is uploaded but is scheduled to be unloaded. unload_pending: []u8, - unload_ready: objc.Object, // MTLTexture - unload_replace: struct { []u8, objc.Object }, + unload_ready: Texture, + unload_replace: struct { []u8, Texture }, pub const Replace = struct { - texture: objc.Object, + texture: Texture, pending: Pending, }; @@ -101,32 +104,32 @@ pub const Image = union(enum) { .replace_gray => |r| { alloc.free(r.pending.dataSlice(1)); - r.texture.msgSend(void, objc.sel("release"), .{}); + r.texture.deinit(); }, .replace_gray_alpha => |r| { alloc.free(r.pending.dataSlice(2)); - r.texture.msgSend(void, objc.sel("release"), .{}); + r.texture.deinit(); }, .replace_rgb => |r| { alloc.free(r.pending.dataSlice(3)); - r.texture.msgSend(void, objc.sel("release"), .{}); + r.texture.deinit(); }, .replace_rgba => |r| { alloc.free(r.pending.dataSlice(4)); - r.texture.msgSend(void, objc.sel("release"), .{}); + r.texture.deinit(); }, .unload_replace => |r| { alloc.free(r[0]); - r[1].msgSend(void, objc.sel("release"), .{}); + r[1].deinit(); }, .ready, .unload_ready, - => |obj| obj.msgSend(void, objc.sel("release"), .{}), + => |t| t.deinit(), } } @@ -170,7 +173,7 @@ pub const Image = union(enum) { // Get our existing texture. This switch statement will also handle // scenarios where there is no existing texture and we can modify // the self pointer directly. - const existing: objc.Object = switch (self.*) { + const existing: Texture = switch (self.*) { // For pending, we can free the old data and become pending // ourselves. .pending_gray => |p| { @@ -357,10 +360,11 @@ pub const Image = union(enum) { pub fn upload( self: *Image, alloc: Allocator, - device: objc.Object, - /// Storage mode for the MTLTexture object - storage_mode: mtl.MTLResourceOptions.StorageMode, + metal: *const Metal, ) !void { + const device = metal.device; + const storage_mode = metal.default_storage_mode; + // Convert our data if we have to try self.convert(alloc); @@ -368,27 +372,19 @@ pub const Image = union(enum) { const p = self.pending().?; // Create our texture - const texture = try initTexture(p, device, storage_mode); - errdefer texture.msgSend(void, objc.sel("release"), .{}); - - // Upload our data - const d = self.depth(); - texture.msgSend( - void, - objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), + const texture = try Texture.init( .{ - mtl.MTLRegion{ - .origin = .{ .x = 0, .y = 0, .z = 0 }, - .size = .{ - .width = @intCast(p.width), - .height = @intCast(p.height), - .depth = 1, - }, + .device = device, + .pixel_format = .rgba8unorm_srgb, + .resource_options = .{ + // Indicate that the CPU writes to this resource but never reads it. + .cpu_cache_mode = .write_combined, + .storage_mode = storage_mode, }, - @as(c_ulong, 0), - @as(*const anyopaque, p.data), - @as(c_ulong, d * p.width), }, + @intCast(p.width), + @intCast(p.height), + p.data[0 .. p.width * p.height * self.depth()], ); // Uploaded. We can now clear our data and change our state. @@ -425,42 +421,4 @@ pub const Image = union(enum) { else => null, }; } - - fn initTexture( - p: Pending, - device: objc.Object, - /// Storage mode for the MTLTexture object - storage_mode: mtl.MTLResourceOptions.StorageMode, - ) !objc.Object { - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLTextureDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - - // Set our properties - desc.setProperty("pixelFormat", @intFromEnum(mtl.MTLPixelFormat.rgba8unorm_srgb)); - desc.setProperty("width", @as(c_ulong, @intCast(p.width))); - desc.setProperty("height", @as(c_ulong, @intCast(p.height))); - - desc.setProperty( - "resourceOptions", - mtl.MTLResourceOptions{ - // Indicate that the CPU writes to this resource but never reads it. - .cpu_cache_mode = .write_combined, - .storage_mode = storage_mode, - }, - ); - - // Initialize - const id = device.msgSend( - ?*anyopaque, - objc.sel("newTextureWithDescriptor:"), - .{desc}, - ) orelse return error.MetalFailed; - - return objc.Object.fromId(id); - } }; diff --git a/src/renderer/metal/sampler.zig b/src/renderer/metal/sampler.zig deleted file mode 100644 index c7a04df3a..000000000 --- a/src/renderer/metal/sampler.zig +++ /dev/null @@ -1,38 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const assert = std.debug.assert; -const objc = @import("objc"); - -const mtl = @import("api.zig"); - -pub const Sampler = struct { - sampler: objc.Object, - - pub fn init(device: objc.Object) !Sampler { - const desc = init: { - const Class = objc.getClass("MTLSamplerDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer desc.msgSend(void, objc.sel("release"), .{}); - desc.setProperty("rAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); - desc.setProperty("sAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); - desc.setProperty("tAddressMode", @intFromEnum(mtl.MTLSamplerAddressMode.clamp_to_edge)); - desc.setProperty("minFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear)); - desc.setProperty("magFilter", @intFromEnum(mtl.MTLSamplerMinMagFilter.linear)); - - const sampler = device.msgSend( - objc.Object, - objc.sel("newSamplerStateWithDescriptor:"), - .{desc}, - ); - errdefer sampler.msgSend(void, objc.sel("release"), .{}); - - return .{ .sampler = sampler }; - } - - pub fn deinit(self: *Sampler) void { - self.sampler.msgSend(void, objc.sel("release"), .{}); - } -}; diff --git a/src/renderer/metal/shaders.zig b/src/renderer/metal/shaders.zig index ff5f1e6bd..68994882e 100644 --- a/src/renderer/metal/shaders.zig +++ b/src/renderer/metal/shaders.zig @@ -6,6 +6,7 @@ const objc = @import("objc"); const math = @import("../../math.zig"); const mtl = @import("api.zig"); +const Pipeline = @import("Pipeline.zig"); const log = std.log.scoped(.metal); @@ -14,20 +15,24 @@ pub const Shaders = struct { library: objc.Object, /// Renders cell foreground elements (text, decorations). - cell_text_pipeline: objc.Object, + cell_text_pipeline: Pipeline, /// The cell background shader is the shader used to render the /// background of terminal cells. - cell_bg_pipeline: objc.Object, + cell_bg_pipeline: Pipeline, /// The image shader is the shader used to render images for things /// like the Kitty image protocol. - image_pipeline: objc.Object, + image_pipeline: Pipeline, /// Custom shaders to run against the final drawable texture. This /// can be used to apply a lot of effects. Each shader is run in sequence /// against the output of the previous shader. - post_pipelines: []const objc.Object, + post_pipelines: []const Pipeline, + + /// Set to true when deinited, if you try to deinit a defunct set + /// of shaders it will just be ignored, to prevent double-free. + defunct: bool = false, /// Initialize our shader set. /// @@ -44,15 +49,15 @@ pub const Shaders = struct { errdefer library.msgSend(void, objc.sel("release"), .{}); const cell_text_pipeline = try initCellTextPipeline(device, library, pixel_format); - errdefer cell_text_pipeline.msgSend(void, objc.sel("release"), .{}); + errdefer cell_text_pipeline.deinit(); const cell_bg_pipeline = try initCellBgPipeline(device, library, pixel_format); - errdefer cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); + errdefer cell_bg_pipeline.deinit(); const image_pipeline = try initImagePipeline(device, library, pixel_format); - errdefer image_pipeline.msgSend(void, objc.sel("release"), .{}); + errdefer image_pipeline.deinit(); - const post_pipelines: []const objc.Object = initPostPipelines( + const post_pipelines: []const Pipeline = initPostPipelines( alloc, device, library, @@ -66,7 +71,7 @@ pub const Shaders = struct { break :err &.{}; }; errdefer if (post_pipelines.len > 0) { - for (post_pipelines) |pipeline| pipeline.msgSend(void, objc.sel("release"), .{}); + for (post_pipelines) |pipeline| pipeline.deinit(); alloc.free(post_pipelines); }; @@ -80,16 +85,19 @@ pub const Shaders = struct { } pub fn deinit(self: *Shaders, alloc: Allocator) void { + if (self.defunct) return; + self.defunct = true; + // Release our primary shaders - self.cell_text_pipeline.msgSend(void, objc.sel("release"), .{}); - self.cell_bg_pipeline.msgSend(void, objc.sel("release"), .{}); - self.image_pipeline.msgSend(void, objc.sel("release"), .{}); + self.cell_text_pipeline.deinit(); + self.cell_bg_pipeline.deinit(); + self.image_pipeline.deinit(); self.library.msgSend(void, objc.sel("release"), .{}); // Release our postprocess shaders if (self.post_pipelines.len > 0) { for (self.post_pipelines) |pipeline| { - pipeline.msgSend(void, objc.sel("release"), .{}); + pipeline.deinit(); } alloc.free(self.post_pipelines); } @@ -140,25 +148,30 @@ pub const Uniforms = extern struct { /// The background color for the whole surface. bg_color: [4]u8 align(4), - /// Whether the cursor is 2 cells wide. - cursor_wide: bool align(1), + /// Various booleans. + /// + /// TODO: Maybe put these in a packed struct, like for OpenGL. + bools: extern struct { + /// Whether the cursor is 2 cells wide. + cursor_wide: bool align(1), - /// Indicates that colors provided to the shader are already in - /// the P3 color space, so they don't need to be converted from - /// sRGB. - use_display_p3: bool align(1), + /// Indicates that colors provided to the shader are already in + /// the P3 color space, so they don't need to be converted from + /// sRGB. + use_display_p3: bool align(1), - /// Indicates that the color attachments for the shaders have - /// an `*_srgb` pixel format, which means the shaders need to - /// output linear RGB colors rather than gamma encoded colors, - /// since blending will be performed in linear space and then - /// Metal itself will re-encode the colors for storage. - use_linear_blending: bool align(1), + /// Indicates that the color attachments for the shaders have + /// an `*_srgb` pixel format, which means the shaders need to + /// output linear RGB colors rather than gamma encoded colors, + /// since blending will be performed in linear space and then + /// Metal itself will re-encode the colors for storage. + use_linear_blending: bool align(1), - /// Enables a weight correction step that makes text rendered - /// with linear alpha blending have a similar apparent weight - /// (thickness) to gamma-incorrect blending. - use_linear_correction: bool align(1) = false, + /// Enables a weight correction step that makes text rendered + /// with linear alpha blending have a similar apparent weight + /// (thickness) to gamma-incorrect blending. + use_linear_correction: bool align(1) = false, + }, const PaddingExtend = packed struct(u8) { left: bool = false, @@ -214,15 +227,16 @@ fn initLibrary(device: objc.Object) !objc.Object { return library; } -/// Initialize our custom shader pipelines. The shaders argument is a -/// set of shader source code, not file paths. +/// Initialize our custom shader pipelines. +/// +/// The shaders argument is a set of shader source code, not file paths. fn initPostPipelines( alloc: Allocator, device: objc.Object, library: objc.Object, shaders: []const [:0]const u8, pixel_format: mtl.MTLPixelFormat, -) ![]const objc.Object { +) ![]const Pipeline { // If we have no shaders, do nothing. if (shaders.len == 0) return &.{}; @@ -230,10 +244,10 @@ fn initPostPipelines( var i: usize = 0; // Initialize our result set. If any error happens, we undo everything. - var pipelines = try alloc.alloc(objc.Object, shaders.len); + var pipelines = try alloc.alloc(Pipeline, shaders.len); errdefer { for (pipelines[0..i]) |pipeline| { - pipeline.msgSend(void, objc.sel("release"), .{}); + pipeline.deinit(); } alloc.free(pipelines); } @@ -259,7 +273,7 @@ fn initPostPipeline( library: objc.Object, data: [:0]const u8, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { +) !Pipeline { // Create our library which has the shader source const post_library = library: { const source = try macos.foundation.String.createWithBytes( @@ -282,16 +296,19 @@ fn initPostPipeline( }; defer post_library.msgSend(void, objc.sel("release"), .{}); - return (Pipeline{ + return try Pipeline.init(null, .{ + .device = device, .vertex_fn = "full_screen_vertex", .fragment_fn = "main0", - .blending_enabled = false, - }).init( - device, - library, - post_library, - pixel_format, - ); + .vertex_library = library, + .fragment_library = post_library, + .attachments = &.{ + .{ + .pixel_format = pixel_format, + .blending_enabled = false, + }, + }, + }); } /// This is a single parameter for the terminal cell shader. @@ -324,19 +341,21 @@ fn initCellTextPipeline( device: objc.Object, library: objc.Object, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { - return (Pipeline{ +) !Pipeline { + return try Pipeline.init(CellText, .{ + .device = device, .vertex_fn = "cell_text_vertex", .fragment_fn = "cell_text_fragment", - .Vertex = CellText, + .vertex_library = library, + .fragment_library = library, .step_fn = .per_instance, - .blending_enabled = true, - }).init( - device, - library, - library, - pixel_format, - ); + .attachments = &.{ + .{ + .pixel_format = pixel_format, + .blending_enabled = true, + }, + }, + }); } /// This is a single parameter for the cell bg shader. @@ -347,17 +366,20 @@ fn initCellBgPipeline( device: objc.Object, library: objc.Object, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { - return (Pipeline{ +) !Pipeline { + return try Pipeline.init(null, .{ + .device = device, .vertex_fn = "cell_bg_vertex", .fragment_fn = "cell_bg_fragment", - .blending_enabled = false, - }).init( - device, - library, - library, - pixel_format, - ); + .vertex_library = library, + .fragment_library = library, + .attachments = &.{ + .{ + .pixel_format = pixel_format, + .blending_enabled = false, + }, + }, + }); } /// Initialize the image render pipeline for our shader library. @@ -365,182 +387,21 @@ fn initImagePipeline( device: objc.Object, library: objc.Object, pixel_format: mtl.MTLPixelFormat, -) !objc.Object { - return (Pipeline{ +) !Pipeline { + return try Pipeline.init(Image, .{ + .device = device, .vertex_fn = "image_vertex", .fragment_fn = "image_fragment", - .Vertex = Image, + .vertex_library = library, + .fragment_library = library, .step_fn = .per_instance, - .blending_enabled = true, - }).init( - device, - library, - library, - pixel_format, - ); -} - -/// A struct with all the necessary info to initialize a pipeline. -const Pipeline = struct { - /// Name of the vertex function - vertex_fn: []const u8, - /// Name of the fragment function - fragment_fn: []const u8, - - /// Vertex attribute struct - Vertex: ?type = null, - /// Vertex step function - step_fn: mtl.MTLVertexStepFunction = .per_vertex, - - /// Whether blending is enabled for the color attachment - blending_enabled: bool = true, - - fn init( - self: *const Pipeline, - device: objc.Object, - vertex_library: objc.Object, - fragment_library: objc.Object, - pixel_format: mtl.MTLPixelFormat, - ) !objc.Object { - // Create our descriptor - const desc = init: { - const Class = objc.getClass("MTLRenderPipelineDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer desc.msgSend(void, objc.sel("release"), .{}); - - // Get our vertex and fragment functions and add them to the descriptor. - { - const str = try macos.foundation.String.createWithBytes( - self.vertex_fn, - .utf8, - false, - ); - defer str.release(); - - const ptr = vertex_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - const func_vert = objc.Object.fromId(ptr.?); - defer func_vert.msgSend(void, objc.sel("release"), .{}); - - desc.setProperty("vertexFunction", func_vert); - } - { - const str = try macos.foundation.String.createWithBytes( - self.fragment_fn, - .utf8, - false, - ); - defer str.release(); - - const ptr = fragment_library.msgSend(?*anyopaque, objc.sel("newFunctionWithName:"), .{str}); - const func_frag = objc.Object.fromId(ptr.?); - defer func_frag.msgSend(void, objc.sel("release"), .{}); - - desc.setProperty("fragmentFunction", func_frag); - } - - // If we have vertex attributes, create and add a vertex descriptor. - if (self.Vertex) |V| { - const vertex_desc = init: { - const Class = objc.getClass("MTLVertexDescriptor").?; - const id_alloc = Class.msgSend(objc.Object, objc.sel("alloc"), .{}); - const id_init = id_alloc.msgSend(objc.Object, objc.sel("init"), .{}); - break :init id_init; - }; - defer vertex_desc.msgSend(void, objc.sel("release"), .{}); - - // Our attributes are the fields of the input - const attrs = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "attributes")); - autoAttribute(V, attrs); - - // The layout describes how and when we fetch the next vertex input. - const layouts = objc.Object.fromId(vertex_desc.getProperty(?*anyopaque, "layouts")); - { - const layout = layouts.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - layout.setProperty("stepFunction", @intFromEnum(self.step_fn)); - layout.setProperty("stride", @as(c_ulong, @sizeOf(V))); - } - - desc.setProperty("vertexDescriptor", vertex_desc); - } - - // Set our color attachment - const attachments = objc.Object.fromId(desc.getProperty(?*anyopaque, "colorAttachments")); - { - const attachment = attachments.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, 0)}, - ); - - attachment.setProperty("pixelFormat", @intFromEnum(pixel_format)); - - attachment.setProperty("blendingEnabled", self.blending_enabled); - // We always use premultiplied alpha blending for now. - if (self.blending_enabled) { - attachment.setProperty("rgbBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("alphaBlendOperation", @intFromEnum(mtl.MTLBlendOperation.add)); - attachment.setProperty("sourceRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("sourceAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one)); - attachment.setProperty("destinationRGBBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - attachment.setProperty("destinationAlphaBlendFactor", @intFromEnum(mtl.MTLBlendFactor.one_minus_source_alpha)); - } - } - - // Make our state - var err: ?*anyopaque = null; - const pipeline_state = device.msgSend( - objc.Object, - objc.sel("newRenderPipelineStateWithDescriptor:error:"), - .{ desc, &err }, - ); - try checkError(err); - errdefer pipeline_state.msgSend(void, objc.sel("release"), .{}); - - return pipeline_state; - } -}; - -fn autoAttribute(T: type, attrs: objc.Object) void { - inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| { - const offset = @offsetOf(T, field.name); - - const FT = switch (@typeInfo(field.type)) { - .@"enum" => |e| e.tag_type, - else => field.type, - }; - - const format = switch (FT) { - [4]u8 => mtl.MTLVertexFormat.uchar4, - [2]u16 => mtl.MTLVertexFormat.ushort2, - [2]i16 => mtl.MTLVertexFormat.short2, - [2]f32 => mtl.MTLVertexFormat.float2, - [4]f32 => mtl.MTLVertexFormat.float4, - [2]i32 => mtl.MTLVertexFormat.int2, - u32 => mtl.MTLVertexFormat.uint, - [2]u32 => mtl.MTLVertexFormat.uint2, - [4]u32 => mtl.MTLVertexFormat.uint4, - u8 => mtl.MTLVertexFormat.uchar, - else => comptime unreachable, - }; - - const attr = attrs.msgSend( - objc.Object, - objc.sel("objectAtIndexedSubscript:"), - .{@as(c_ulong, i)}, - ); - - attr.setProperty("format", @intFromEnum(format)); - attr.setProperty("offset", @as(c_ulong, offset)); - attr.setProperty("bufferIndex", @as(c_ulong, 0)); - } + .attachments = &.{ + .{ + .pixel_format = pixel_format, + .blending_enabled = true, + }, + }, + }); } fn checkError(err_: ?*anyopaque) !void { diff --git a/src/renderer/opengl/CellProgram.zig b/src/renderer/opengl/CellProgram.zig deleted file mode 100644 index c4da8e233..000000000 --- a/src/renderer/opengl/CellProgram.zig +++ /dev/null @@ -1,196 +0,0 @@ -/// The OpenGL program for rendering terminal cells. -const CellProgram = @This(); - -const std = @import("std"); -const gl = @import("opengl"); - -program: gl.Program, -vao: gl.VertexArray, -ebo: gl.Buffer, -vbo: gl.Buffer, - -/// The raw structure that maps directly to the buffer sent to the vertex shader. -/// This must be "extern" so that the field order is not reordered by the -/// Zig compiler. -pub const Cell = extern struct { - /// vec2 grid_coord - grid_col: u16, - grid_row: u16, - - /// vec2 glyph_pos - glyph_x: u32 = 0, - glyph_y: u32 = 0, - - /// vec2 glyph_size - glyph_width: u32 = 0, - glyph_height: u32 = 0, - - /// vec2 glyph_offset - glyph_offset_x: i32 = 0, - glyph_offset_y: i32 = 0, - - /// vec4 color_in - r: u8, - g: u8, - b: u8, - a: u8, - - /// vec4 bg_color_in - bg_r: u8, - bg_g: u8, - bg_b: u8, - bg_a: u8, - - /// uint mode - mode: CellMode, - - /// The width in grid cells that a rendering takes. - grid_width: u8, -}; - -pub const CellMode = enum(u8) { - bg = 1, - fg = 2, - fg_constrained = 3, - fg_color = 7, - fg_powerline = 15, - - // Non-exhaustive because masks change it - _, - - /// Apply a mask to the mode. - pub fn mask(self: CellMode, m: CellMode) CellMode { - return @enumFromInt(@intFromEnum(self) | @intFromEnum(m)); - } - - pub fn isFg(self: CellMode) bool { - // Since we use bit tricks below, we want to ensure the enum - // doesn't change without us looking at this logic again. - comptime { - const info = @typeInfo(CellMode).@"enum"; - std.debug.assert(info.fields.len == 5); - } - - return @intFromEnum(self) & @intFromEnum(@as(CellMode, .fg)) != 0; - } -}; - -pub fn init() !CellProgram { - // Load and compile our shaders. - const program = try gl.Program.createVF( - @embedFile("../shaders/cell.v.glsl"), - @embedFile("../shaders/cell.f.glsl"), - ); - errdefer program.destroy(); - - // Set our cell dimensions - const pbind = try program.use(); - defer pbind.unbind(); - - // Set all of our texture indexes - try program.setUniform("text", 0); - try program.setUniform("text_color", 1); - - // Setup our VAO - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - const vaobind = try vao.bind(); - defer vaobind.unbind(); - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.element_array); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .static_draw); - - // Vertex buffer (VBO) - const vbo = try gl.Buffer.create(); - errdefer vbo.destroy(); - var vbobind = try vbo.bind(.array); - defer vbobind.unbind(); - var offset: usize = 0; - try vbobind.attributeAdvanced(0, 2, gl.c.GL_UNSIGNED_SHORT, false, @sizeOf(Cell), offset); - offset += 2 * @sizeOf(u16); - try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(2, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Cell), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(3, 2, gl.c.GL_INT, false, @sizeOf(Cell), offset); - offset += 2 * @sizeOf(i32); - try vbobind.attributeAdvanced(4, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeAdvanced(5, 4, gl.c.GL_UNSIGNED_BYTE, false, @sizeOf(Cell), offset); - offset += 4 * @sizeOf(u8); - try vbobind.attributeIAdvanced(6, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); - offset += 1 * @sizeOf(u8); - try vbobind.attributeIAdvanced(7, 1, gl.c.GL_UNSIGNED_BYTE, @sizeOf(Cell), offset); - try vbobind.enableAttribArray(0); - try vbobind.enableAttribArray(1); - try vbobind.enableAttribArray(2); - try vbobind.enableAttribArray(3); - try vbobind.enableAttribArray(4); - try vbobind.enableAttribArray(5); - try vbobind.enableAttribArray(6); - try vbobind.enableAttribArray(7); - try vbobind.attributeDivisor(0, 1); - try vbobind.attributeDivisor(1, 1); - try vbobind.attributeDivisor(2, 1); - try vbobind.attributeDivisor(3, 1); - try vbobind.attributeDivisor(4, 1); - try vbobind.attributeDivisor(5, 1); - try vbobind.attributeDivisor(6, 1); - try vbobind.attributeDivisor(7, 1); - - return .{ - .program = program, - .vao = vao, - .ebo = ebo, - .vbo = vbo, - }; -} - -pub fn bind(self: CellProgram) !Binding { - const program = try self.program.use(); - errdefer program.unbind(); - - const vao = try self.vao.bind(); - errdefer vao.unbind(); - - const ebo = try self.ebo.bind(.element_array); - errdefer ebo.unbind(); - - const vbo = try self.vbo.bind(.array); - errdefer vbo.unbind(); - - return .{ - .program = program, - .vao = vao, - .ebo = ebo, - .vbo = vbo, - }; -} - -pub fn deinit(self: CellProgram) void { - self.vbo.destroy(); - self.ebo.destroy(); - self.vao.destroy(); - self.program.destroy(); -} - -pub const Binding = struct { - program: gl.Program.Binding, - vao: gl.VertexArray.Binding, - ebo: gl.Buffer.Binding, - vbo: gl.Buffer.Binding, - - pub fn unbind(self: Binding) void { - self.vbo.unbind(); - self.ebo.unbind(); - self.vao.unbind(); - self.program.unbind(); - } -}; diff --git a/src/renderer/opengl/Frame.zig b/src/renderer/opengl/Frame.zig new file mode 100644 index 000000000..4c23fe106 --- /dev/null +++ b/src/renderer/opengl/Frame.zig @@ -0,0 +1,75 @@ +//! Wrapper for handling render passes. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const gl = @import("opengl"); + +const Renderer = @import("../generic.zig").Renderer(OpenGL); +const OpenGL = @import("../OpenGL.zig"); +const Target = @import("Target.zig"); +const Pipeline = @import("Pipeline.zig"); +const RenderPass = @import("RenderPass.zig"); +const Buffer = @import("buffer.zig").Buffer; + +const Health = @import("../../renderer.zig").Health; + +const log = std.log.scoped(.opengl); + +/// Options for beginning a frame. +pub const Options = struct {}; + +renderer: *Renderer, +target: *Target, + +/// Begin encoding a frame. +pub fn begin( + opts: Options, + /// Once the frame has been completed, the `frameCompleted` method + /// on the renderer is called with the health status of the frame. + renderer: *Renderer, + /// The target is presented via the provided renderer's API when completed. + target: *Target, +) !Self { + _ = opts; + + return .{ + .renderer = renderer, + .target = target, + }; +} + +/// Add a render pass to this frame with the provided attachments. +/// Returns a RenderPass which allows render steps to be added. +pub inline fn renderPass( + self: *const Self, + attachments: []const RenderPass.Options.Attachment, +) RenderPass { + _ = self; + return RenderPass.begin(.{ .attachments = attachments }); +} + +/// Complete this frame and present the target. +/// +/// If `sync` is true, this will block until the frame is presented. +/// +/// NOTE: For OpenGL, `sync` is ignored and we always block. +pub fn complete(self: *const Self, sync: bool) void { + _ = sync; + gl.finish(); + + // If there are any GL errors, consider the frame unhealthy. + const health: Health = if (gl.errors.getError()) .healthy else |_| .unhealthy; + + // If the frame is healthy, present it. + if (health == .healthy) { + self.renderer.api.present(self.target.*) catch |err| { + log.err("Failed to present render target: err={}", .{err}); + }; + } + + // Report the health to the renderer. + self.renderer.frameCompleted(health); +} diff --git a/src/renderer/opengl/ImageProgram.zig b/src/renderer/opengl/ImageProgram.zig deleted file mode 100644 index ff6794085..000000000 --- a/src/renderer/opengl/ImageProgram.zig +++ /dev/null @@ -1,134 +0,0 @@ -/// The OpenGL program for rendering terminal cells. -const ImageProgram = @This(); - -const std = @import("std"); -const gl = @import("opengl"); - -program: gl.Program, -vao: gl.VertexArray, -ebo: gl.Buffer, -vbo: gl.Buffer, - -pub const Input = extern struct { - /// vec2 grid_coord - grid_col: i32, - grid_row: i32, - - /// vec2 cell_offset - cell_offset_x: u32 = 0, - cell_offset_y: u32 = 0, - - /// vec4 source_rect - source_x: u32 = 0, - source_y: u32 = 0, - source_width: u32 = 0, - source_height: u32 = 0, - - /// vec2 dest_size - dest_width: u32 = 0, - dest_height: u32 = 0, -}; - -pub fn init() !ImageProgram { - // Load and compile our shaders. - const program = try gl.Program.createVF( - @embedFile("../shaders/image.v.glsl"), - @embedFile("../shaders/image.f.glsl"), - ); - errdefer program.destroy(); - - // Set our program uniforms - const pbind = try program.use(); - defer pbind.unbind(); - - // Set all of our texture indexes - try program.setUniform("image", 0); - - // Setup our VAO - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - const vaobind = try vao.bind(); - defer vaobind.unbind(); - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.element_array); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .static_draw); - - // Vertex buffer (VBO) - const vbo = try gl.Buffer.create(); - errdefer vbo.destroy(); - var vbobind = try vbo.bind(.array); - defer vbobind.unbind(); - var offset: usize = 0; - try vbobind.attributeAdvanced(0, 2, gl.c.GL_INT, false, @sizeOf(Input), offset); - offset += 2 * @sizeOf(i32); - try vbobind.attributeAdvanced(1, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); - offset += 2 * @sizeOf(u32); - try vbobind.attributeAdvanced(2, 4, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); - offset += 4 * @sizeOf(u32); - try vbobind.attributeAdvanced(3, 2, gl.c.GL_UNSIGNED_INT, false, @sizeOf(Input), offset); - offset += 2 * @sizeOf(u32); - try vbobind.enableAttribArray(0); - try vbobind.enableAttribArray(1); - try vbobind.enableAttribArray(2); - try vbobind.enableAttribArray(3); - try vbobind.attributeDivisor(0, 1); - try vbobind.attributeDivisor(1, 1); - try vbobind.attributeDivisor(2, 1); - try vbobind.attributeDivisor(3, 1); - - return .{ - .program = program, - .vao = vao, - .ebo = ebo, - .vbo = vbo, - }; -} - -pub fn bind(self: ImageProgram) !Binding { - const program = try self.program.use(); - errdefer program.unbind(); - - const vao = try self.vao.bind(); - errdefer vao.unbind(); - - const ebo = try self.ebo.bind(.element_array); - errdefer ebo.unbind(); - - const vbo = try self.vbo.bind(.array); - errdefer vbo.unbind(); - - return .{ - .program = program, - .vao = vao, - .ebo = ebo, - .vbo = vbo, - }; -} - -pub fn deinit(self: ImageProgram) void { - self.vbo.destroy(); - self.ebo.destroy(); - self.vao.destroy(); - self.program.destroy(); -} - -pub const Binding = struct { - program: gl.Program.Binding, - vao: gl.VertexArray.Binding, - ebo: gl.Buffer.Binding, - vbo: gl.Buffer.Binding, - - pub fn unbind(self: Binding) void { - self.vbo.unbind(); - self.ebo.unbind(); - self.vao.unbind(); - self.program.unbind(); - } -}; diff --git a/src/renderer/opengl/Pipeline.zig b/src/renderer/opengl/Pipeline.zig new file mode 100644 index 000000000..127d689f5 --- /dev/null +++ b/src/renderer/opengl/Pipeline.zig @@ -0,0 +1,170 @@ +//! Wrapper for handling render pipelines. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const gl = @import("opengl"); + +const OpenGL = @import("../OpenGL.zig"); +const Texture = @import("Texture.zig"); +const Buffer = @import("buffer.zig").Buffer; + +const log = std.log.scoped(.opengl); + +/// Options for initializing a render pipeline. +pub const Options = struct { + /// GLSL source of the vertex function + vertex_fn: [:0]const u8, + /// GLSL source of the fragment function + fragment_fn: [:0]const u8, + + /// Vertex step function + step_fn: StepFunction = .per_vertex, + + /// Whether to enable blending. + blending_enabled: bool = true, + + pub const StepFunction = enum { + constant, + per_vertex, + per_instance, + }; + +}; + +program: gl.Program, + +fbo: gl.Framebuffer, + +vao: gl.VertexArray, + +stride: usize, + +blending_enabled: bool, + +pub fn init(comptime VertexAttributes: ?type, opts: Options) !Self { + // Load and compile our shaders. + const program = try gl.Program.createVF( + opts.vertex_fn, + opts.fragment_fn, + ); + errdefer program.destroy(); + + const pbind = try program.use(); + defer pbind.unbind(); + + const fbo = try gl.Framebuffer.create(); + errdefer fbo.destroy(); + const fbobind = try fbo.bind(.framebuffer); + defer fbobind.unbind(); + + const vao = try gl.VertexArray.create(); + errdefer vao.destroy(); + const vaobind = try vao.bind(); + defer vaobind.unbind(); + + if (VertexAttributes) |VA| try autoAttribute(VA, vaobind, opts.step_fn); + + return .{ + .program = program, + .fbo = fbo, + .vao = vao, + .stride = if (VertexAttributes) |VA| @sizeOf(VA) else 0, + .blending_enabled = opts.blending_enabled, + }; +} + +pub fn deinit(self: *const Self) void { + self.program.destroy(); +} + +fn autoAttribute( + T: type, + vaobind: gl.VertexArray.Binding, + step_fn: Options.StepFunction, +) !void { + const divisor: gl.c.GLuint = switch (step_fn) { + .per_vertex => 0, + .per_instance => 1, + .constant => std.math.maxInt(gl.c.GLuint), + }; + + inline for (@typeInfo(T).@"struct".fields, 0..) |field, i| { + try vaobind.enableAttribArray(i); + try vaobind.attributeBinding(i, 0); + try vaobind.bindingDivisor(i, divisor); + + const offset = @offsetOf(T, field.name); + + const FT = switch (@typeInfo(field.type)) { + .@"enum" => |e| e.tag_type, + else => field.type, + }; + + const size, const IT = switch (@typeInfo(FT)) { + .array => |a| .{ a.len, a.child }, + else => .{ 1, FT }, + }; + + try switch (IT) { + u8 => vaobind.attributeIFormat( + i, + size, + gl.c.GL_UNSIGNED_BYTE, + offset, + ), + u16 => vaobind.attributeIFormat( + i, + size, + gl.c.GL_UNSIGNED_SHORT, + offset, + ), + u32 => vaobind.attributeIFormat( + i, + size, + gl.c.GL_UNSIGNED_INT, + offset, + ), + i8 => vaobind.attributeIFormat( + i, + size, + gl.c.GL_BYTE, + offset, + ), + i16 => vaobind.attributeIFormat( + i, + size, + gl.c.GL_SHORT, + offset, + ), + i32 => vaobind.attributeIFormat( + i, + size, + gl.c.GL_INT, + offset, + ), + f16 => vaobind.attributeFormat( + i, + size, + gl.c.GL_HALF_FLOAT, + false, + offset, + ), + f32 => vaobind.attributeFormat( + i, + size, + gl.c.GL_FLOAT, + false, + offset, + ), + f64 => vaobind.attributeLFormat( + i, + size, + offset, + ), + else => unreachable, + }; + } +} diff --git a/src/renderer/opengl/RenderPass.zig b/src/renderer/opengl/RenderPass.zig new file mode 100644 index 000000000..0f5bd89e7 --- /dev/null +++ b/src/renderer/opengl/RenderPass.zig @@ -0,0 +1,141 @@ +//! Wrapper for handling render passes. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const gl = @import("opengl"); + +const OpenGL = @import("../OpenGL.zig"); +const Target = @import("Target.zig"); +const Texture = @import("Texture.zig"); +const Pipeline = @import("Pipeline.zig"); +const RenderPass = @import("RenderPass.zig"); +const Buffer = @import("buffer.zig").Buffer; + +/// Options for beginning a render pass. +pub const Options = struct { + /// Color attachments for this render pass. + attachments: []const Attachment, + + /// Describes a color attachment. + pub const Attachment = struct { + target: union(enum) { + texture: Texture, + target: Target, + }, + clear_color: ?[4]f32 = null, + }; +}; + +/// Describes a step in a render pass. +pub const Step = struct { + pipeline: Pipeline, + uniforms: ?gl.Buffer = null, + buffers: []const ?gl.Buffer = &.{}, + textures: []const ?Texture = &.{}, + draw: Draw, + + /// Describes the draw call for this step. + pub const Draw = struct { + type: gl.Primitive, + vertex_count: usize, + instance_count: usize = 1, + }; +}; + +attachments: []const Options.Attachment, + +step_number: usize = 0, + +/// Begin a render pass. +pub fn begin( + opts: Options, +) Self { + return .{ + .attachments = opts.attachments, + }; +} + +/// Add a step to this render pass. +/// +/// TODO: Errors are silently ignored in this function, maybe they shouldn't be? +pub fn step(self: *Self, s: Step) void { + if (s.draw.instance_count == 0) return; + + const pbind = s.pipeline.program.use() catch return; + defer pbind.unbind(); + + const vaobind = s.pipeline.vao.bind() catch return; + defer vaobind.unbind(); + + const fbobind = switch (self.attachments[0].target) { + .target => |t| t.framebuffer.bind(.framebuffer) catch return, + .texture => |t| bind: { + const fbobind = s.pipeline.fbo.bind(.framebuffer) catch return; + fbobind.texture2D(.color0, t.target, t.texture, 0) catch { + fbobind.unbind(); + return; + }; + break :bind fbobind; + }, + }; + defer fbobind.unbind(); + + defer self.step_number += 1; + + // If we have a clear color and this is the + // first step in the pass, go ahead and clear. + if (self.step_number == 0) if (self.attachments[0].clear_color) |c| { + gl.clearColor(c[0], c[1], c[2], c[3]); + gl.clear(gl.c.GL_COLOR_BUFFER_BIT); + }; + + // Bind the uniform buffer we bind at index 1 to align with Metal. + if (s.uniforms) |ubo| { + _ = ubo.bindBase(.uniform, 1) catch return; + } + + // Bind relevant texture units. + for (s.textures, 0..) |t, i| if (t) |tex| { + gl.Texture.active(@intCast(i)) catch return; + _ = tex.texture.bind(tex.target) catch return; + }; + + // Bind 0th buffer as the vertex buffer, + // and bind the rest as storage buffers. + if (s.buffers.len > 0) { + if (s.buffers[0]) |vbo| vaobind.bindVertexBuffer( + 0, + vbo.id, + 0, + @intCast(s.pipeline.stride), + ) catch return; + + for (s.buffers[1..], 1..) |b, i| if (b) |buf| { + _ = buf.bindBase(.storage, @intCast(i)) catch return; + }; + } + + if (s.pipeline.blending_enabled) { + gl.enable(gl.c.GL_BLEND) catch return; + gl.blendFunc(gl.c.GL_ONE, gl.c.GL_ONE_MINUS_SRC_ALPHA) catch return; + } else { + gl.disable(gl.c.GL_BLEND) catch return; + } + + gl.drawArraysInstanced( + s.draw.type, + 0, + @intCast(s.draw.vertex_count), + @intCast(s.draw.instance_count), + ) catch return; +} + +/// Complete this render pass. +/// This struct can no longer be used after calling this. +pub fn complete(self: *const Self) void { + _ = self; + gl.flush(); +} diff --git a/src/renderer/opengl/Target.zig b/src/renderer/opengl/Target.zig new file mode 100644 index 000000000..1b3a13ed0 --- /dev/null +++ b/src/renderer/opengl/Target.zig @@ -0,0 +1,62 @@ +//! Represents a render target. +//! +//! In this case, an OpenGL renderbuffer-backed framebuffer. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const gl = @import("opengl"); + +const log = std.log.scoped(.opengl); + +/// Options for initializing a Target +pub const Options = struct { + /// Desired width + width: usize, + /// Desired height + height: usize, + + /// Internal format for the renderbuffer. + internal_format: gl.Texture.InternalFormat, +}; + +/// The underlying `gl.Framebuffer` instance. +framebuffer: gl.Framebuffer, + +/// The underlying `gl.Renderbuffer` instance. +renderbuffer: gl.Renderbuffer, + +/// Current width of this target. +width: usize, +/// Current height of this target. +height: usize, + +pub fn init(opts: Options) !Self { + const rbo = try gl.Renderbuffer.create(); + const bound_rbo = try rbo.bind(); + defer bound_rbo.unbind(); + try bound_rbo.storage( + opts.internal_format, + @intCast(opts.width), + @intCast(opts.height), + ); + + const fbo = try gl.Framebuffer.create(); + const bound_fbo = try fbo.bind(.framebuffer); + defer bound_fbo.unbind(); + try bound_fbo.renderbuffer(.color0, rbo); + + return .{ + .framebuffer = fbo, + .renderbuffer = rbo, + .width = opts.width, + .height = opts.height, + }; +} + +pub fn deinit(self: *Self) void { + self.framebuffer.destroy(); + self.renderbuffer.destroy(); +} diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig new file mode 100644 index 000000000..84a1ae9bc --- /dev/null +++ b/src/renderer/opengl/Texture.zig @@ -0,0 +1,99 @@ +//! Wrapper for handling textures. +const Self = @This(); + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const builtin = @import("builtin"); +const gl = @import("opengl"); + +const OpenGL = @import("../OpenGL.zig"); + +const log = std.log.scoped(.opengl); + +/// Options for initializing a texture. +pub const Options = struct { + format: gl.Texture.Format, + internal_format: gl.Texture.InternalFormat, + target: gl.Texture.Target, +}; + +texture: gl.Texture, + +/// The width of this texture. +width: usize, +/// The height of this texture. +height: usize, + +/// Format for this texture. +format: gl.Texture.Format, + +/// Target for this texture. +target: gl.Texture.Target, + +/// Initialize a texture +pub fn init( + opts: Options, + width: usize, + height: usize, + data: ?[]const u8, +) !Self { + const tex = try gl.Texture.create(); + errdefer tex.destroy(); + { + const texbind = try tex.bind(opts.target); + defer texbind.unbind(); + try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); + try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); + try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); + try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); + try texbind.image2D( + 0, + opts.internal_format, + @intCast(width), + @intCast(height), + 0, + opts.format, + .UnsignedByte, + if (data) |d| @ptrCast(d.ptr) else null, + ); + } + + return .{ + .texture = tex, + .width = width, + .height = height, + .format = opts.format, + .target = opts.target, + }; +} + +pub fn deinit(self: Self) void { + self.texture.destroy(); +} + +/// Replace a region of the texture with the provided data. +/// +/// Does NOT check the dimensions of the data to ensure correctness. +pub fn replaceRegion( + self: Self, + x: usize, + y: usize, + width: usize, + height: usize, + data: []const u8, +) !void { + const texbind = try self.texture.bind(self.target); + defer texbind.unbind(); + try texbind.subImage2D( + 0, + @intCast(x), + @intCast(y), + @intCast(width), + @intCast(height), + self.format, + .UnsignedByte, + data.ptr, + ); +} + diff --git a/src/renderer/opengl/buffer.zig b/src/renderer/opengl/buffer.zig new file mode 100644 index 000000000..48b6f410e --- /dev/null +++ b/src/renderer/opengl/buffer.zig @@ -0,0 +1,127 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const gl = @import("opengl"); + +const OpenGL = @import("../OpenGL.zig"); + +const log = std.log.scoped(.opengl); + +/// Options for initializing a buffer. +pub const Options = struct { + target: gl.Buffer.Target = .array, + usage: gl.Buffer.Usage = .dynamic_draw, +}; + +/// OpenGL data storage for a certain set of equal types. This is usually +/// used for vertex buffers, etc. This helpful wrapper makes it easy to +/// prealloc, shrink, grow, sync, buffers with OpenGL. +pub fn Buffer(comptime T: type) type { + return struct { + const Self = @This(); + + /// Underlying `gl.Buffer` instance. + buffer: gl.Buffer, + + /// Options this buffer was allocated with. + opts: Options, + + /// Current allocated length of the data store. + /// Note this is the number of `T`s, not the size in bytes. + len: usize, + + /// Initialize a buffer with the given length pre-allocated. + pub fn init(opts: Options, len: usize) !Self { + const buffer = try gl.Buffer.create(); + errdefer buffer.destroy(); + + const binding = try buffer.bind(opts.target); + defer binding.unbind(); + + try binding.setDataNullManual(len * @sizeOf(T), opts.usage); + + return .{ + .buffer = buffer, + .opts = opts, + .len = len, + }; + } + + /// Init the buffer filled with the given data. + pub fn initFill(opts: Options, data: []const T) !Self { + const buffer = try gl.Buffer.create(); + errdefer buffer.destroy(); + + const binding = try buffer.bind(opts.target); + defer binding.unbind(); + + try binding.setData(data, opts.usage); + + return .{ + .buffer = buffer, + .opts = opts, + .len = data.len * @sizeOf(T), + }; + } + + pub fn deinit(self: Self) void { + self.buffer.destroy(); + } + + /// Sync new contents to the buffer. The data is expected to be the + /// complete contents of the buffer. If the amount of data is larger + /// than the buffer length, the buffer will be reallocated. + /// + /// If the amount of data is smaller than the buffer length, the + /// remaining data in the buffer is left untouched. + pub fn sync(self: *Self, data: []const T) !void { + const binding = try self.buffer.bind(self.opts.target); + defer binding.unbind(); + + // If we need more space than our buffer has, we need to reallocate. + if (data.len > self.len) { + // Reallocate the buffer to hold double what we require. + self.len = data.len * 2; + try binding.setDataNullManual( + self.len * @sizeOf(T), + self.opts.usage, + ); + } + + // We can fit within the buffer so we can just replace bytes. + try binding.setSubData(0, data); + } + + /// Like Buffer.sync but takes data from an array of ArrayLists, + /// rather than a single array. Returns the number of items synced. + pub fn syncFromArrayLists(self: *Self, lists: []const std.ArrayListUnmanaged(T)) !usize { + const binding = try self.buffer.bind(self.opts.target); + defer binding.unbind(); + + var total_len: usize = 0; + for (lists) |list| { + total_len += list.items.len; + } + + // If we need more space than our buffer has, we need to reallocate. + if (total_len > self.len) { + // Reallocate the buffer to hold double what we require. + self.len = total_len * 2; + try binding.setDataNullManual( + self.len * @sizeOf(T), + self.opts.usage, + ); + } + + // We can fit within the buffer so we can just replace bytes. + var i: usize = 0; + + for (lists) |list| { + try binding.setSubData(i, list.items); + i += list.items.len * @sizeOf(T); + } + + return total_len; + } + }; +} diff --git a/src/renderer/opengl/cell.zig b/src/renderer/opengl/cell.zig new file mode 100644 index 000000000..abdbaa0e8 --- /dev/null +++ b/src/renderer/opengl/cell.zig @@ -0,0 +1,220 @@ +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; + +const renderer = @import("../../renderer.zig"); +const terminal = @import("../../terminal/main.zig"); +const shaderpkg = @import("shaders.zig"); + +/// The possible cell content keys that exist. +pub const Key = enum { + bg, + text, + underline, + strikethrough, + overline, + + /// Returns the GPU vertex type for this key. + pub fn CellType(self: Key) type { + return switch (self) { + .bg => shaderpkg.CellBg, + + .text, + .underline, + .strikethrough, + .overline, + => shaderpkg.CellText, + }; + } +}; + +/// A pool of ArrayLists with methods for bulk operations. +fn ArrayListPool(comptime T: type) type { + return struct { + const Self = ArrayListPool(T); + const ArrayListT = std.ArrayListUnmanaged(T); + + // An array containing the lists that belong to this pool. + lists: []ArrayListT = &[_]ArrayListT{}, + + // The pool will be initialized with empty ArrayLists. + pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self { + const self: Self = .{ + .lists = try alloc.alloc(ArrayListT, list_count), + }; + + for (self.lists) |*list| { + list.* = try ArrayListT.initCapacity(alloc, initial_capacity); + } + + return self; + } + + pub fn deinit(self: *Self, alloc: Allocator) void { + for (self.lists) |*list| { + list.deinit(alloc); + } + alloc.free(self.lists); + } + + /// Clear all lists in the pool. + pub fn reset(self: *Self) void { + for (self.lists) |*list| { + list.clearRetainingCapacity(); + } + } + }; +} + +/// The contents of all the cells in the terminal. +/// +/// The goal of this data structure is to allow for efficient row-wise +/// clearing of data from the GPU buffers, to allow for row-wise dirty +/// tracking to eliminate the overhead of rebuilding the GPU buffers +/// each frame. +/// +/// Must be initialized by resizing before calling any operations. +pub const Contents = struct { + size: renderer.GridSize = .{ .rows = 0, .columns = 0 }, + + /// Flat array containing cell background colors for the terminal grid. + /// + /// Indexed as `bg_cells[row * size.columns + col]`. + /// + /// Prefer accessing with `Contents.bgCell(row, col).*` instead + /// of directly indexing in order to avoid integer size bugs. + bg_cells: []shaderpkg.CellBg = undefined, + + /// The ArrayListPool which holds all of the foreground cells. When sized + /// with Contents.resize the individual ArrayLists are given enough room + /// that they can hold a single row with #cols glyphs, underlines, and + /// strikethroughs; however, appendAssumeCapacity MUST NOT be used since + /// it is possible to exceed this with combining glyphs that add a glyph + /// but take up no column since they combine with the previous one, as + /// well as with fonts that perform multi-substitutions for glyphs, which + /// can result in a similar situation where multiple glyphs reside in the + /// same column. + /// + /// Allocations should nevertheless be exceedingly rare since hitting the + /// initial capacity of a list would require a row filled with underlined + /// struck through characters, at least one of which is a multi-glyph + /// composite. + /// + /// Rows are indexed as Contents.fg_rows[y + 1], because the first list in + /// the pool is reserved for the cursor, which must be the first item in + /// the buffer. + /// + /// Must be initialized by calling resize on the Contents struct before + /// calling any operations. + fg_rows: ArrayListPool(shaderpkg.CellText) = .{}, + + pub fn deinit(self: *Contents, alloc: Allocator) void { + alloc.free(self.bg_cells); + self.fg_rows.deinit(alloc); + } + + /// Resize the cell contents for the given grid size. This will + /// always invalidate the entire cell contents. + pub fn resize( + self: *Contents, + alloc: Allocator, + size: renderer.GridSize, + ) !void { + self.size = size; + + const cell_count = @as(usize, size.columns) * @as(usize, size.rows); + + const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count); + errdefer alloc.free(bg_cells); + + @memset(bg_cells, .{ 0, 0, 0, 0 }); + + // The foreground lists can hold 3 types of items: + // - Glyphs + // - Underlines + // - Strikethroughs + // So we give them an initial capacity of size.columns * 3, which will + // avoid any further allocations in the vast majority of cases. Sadly + // we can not assume capacity though, since with combining glyphs that + // form a single grapheme, and multi-substitutions in fonts, the number + // of glyphs in a row is theoretically unlimited. + // + // We have size.rows + 1 lists because index 0 is used for a special + // list containing the cursor cell which needs to be first in the buffer. + var fg_rows = try ArrayListPool(shaderpkg.CellText).init(alloc, size.rows + 1, size.columns * 3); + errdefer fg_rows.deinit(alloc); + + alloc.free(self.bg_cells); + self.fg_rows.deinit(alloc); + + self.bg_cells = bg_cells; + self.fg_rows = fg_rows; + + // We don't need 3*cols worth of cells for the cursor list, so we can + // replace it with a smaller list. This is technically a tiny bit of + // extra work but resize is not a hot function so it's worth it to not + // waste the memory. + self.fg_rows.lists[0].deinit(alloc); + self.fg_rows.lists[0] = try std.ArrayListUnmanaged(shaderpkg.CellText).initCapacity(alloc, 1); + } + + /// Reset the cell contents to an empty state without resizing. + pub fn reset(self: *Contents) void { + @memset(self.bg_cells, .{ 0, 0, 0, 0 }); + self.fg_rows.reset(); + } + + /// Set the cursor value. If the value is null then the cursor is hidden. + pub fn setCursor(self: *Contents, v: ?shaderpkg.CellText) void { + self.fg_rows.lists[0].clearRetainingCapacity(); + + if (v) |cell| { + self.fg_rows.lists[0].appendAssumeCapacity(cell); + } + } + + /// Access a background cell. Prefer this function over direct indexing + /// of `bg_cells` in order to avoid integer size bugs causing overflows. + pub inline fn bgCell(self: *Contents, row: usize, col: usize) *shaderpkg.CellBg { + return &self.bg_cells[row * self.size.columns + col]; + } + + /// Add a cell to the appropriate list. Adding the same cell twice will + /// result in duplication in the vertex buffer. The caller should clear + /// the corresponding row with Contents.clear to remove old cells first. + pub fn add( + self: *Contents, + alloc: Allocator, + comptime key: Key, + cell: key.CellType(), + ) !void { + const y = cell.grid_pos[1]; + + assert(y < self.size.rows); + + switch (key) { + .bg => comptime unreachable, + + .text, + .underline, + .strikethrough, + .overline, + // We have a special list containing the cursor cell at the start + // of our fg row pool, so we need to add 1 to the y to get the + // correct index. + => try self.fg_rows.lists[y + 1].append(alloc, cell), + } + } + + /// Clear all of the cell contents for a given row. + pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void { + assert(y < self.size.rows); + + @memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 }); + + // We have a special list containing the cursor cell at the start + // of our fg row pool, so we need to add 1 to the y to get the + // correct index. + self.fg_rows.lists[y + 1].clearRetainingCapacity(); + } +}; diff --git a/src/renderer/opengl/custom.zig b/src/renderer/opengl/custom.zig deleted file mode 100644 index 859277ce5..000000000 --- a/src/renderer/opengl/custom.zig +++ /dev/null @@ -1,310 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const gl = @import("opengl"); -const Size = @import("../size.zig").Size; - -const log = std.log.scoped(.opengl_custom); - -/// The "INDEX" is the index into the global GL state and the -/// "BINDING" is the binding location in the shader. -const UNIFORM_INDEX: gl.c.GLuint = 0; -const UNIFORM_BINDING: gl.c.GLuint = 0; - -/// Global uniforms for custom shaders. -pub const Uniforms = extern struct { - resolution: [3]f32 align(16) = .{ 0, 0, 0 }, - time: f32 align(4) = 1, - time_delta: f32 align(4) = 1, - frame_rate: f32 align(4) = 1, - frame: i32 align(4) = 1, - channel_time: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - channel_resolution: [4][4]f32 align(16) = [1][4]f32{.{ 0, 0, 0, 0 }} ** 4, - mouse: [4]f32 align(16) = .{ 0, 0, 0, 0 }, - date: [4]f32 align(16) = .{ 0, 0, 0, 0 }, - sample_rate: f32 align(4) = 1, -}; - -/// The state associated with custom shaders. This should only be initialized -/// if there is at least one custom shader. -/// -/// To use this, the main terminal shader should render to the framebuffer -/// specified by "fbo". The resulting "fb_texture" will contain the color -/// attachment. This is then used as the iChannel0 input to the custom -/// shader. -pub const State = struct { - /// The uniform data - uniforms: Uniforms, - - /// The OpenGL buffers - fbo: gl.Framebuffer, - ubo: gl.Buffer, - vao: gl.VertexArray, - ebo: gl.Buffer, - fb_texture: gl.Texture, - - /// The set of programs for the custom shaders. - programs: []const Program, - - /// The first time a frame was drawn. This is used to update - /// the time uniform. - first_frame_time: std.time.Instant, - - /// The last time a frame was drawn. This is used to update - /// the time uniform. - last_frame_time: std.time.Instant, - - pub fn init( - alloc: Allocator, - srcs: []const [:0]const u8, - ) !State { - if (srcs.len == 0) return error.OneCustomShaderRequired; - - // Create our programs - var programs = std.ArrayList(Program).init(alloc); - defer programs.deinit(); - errdefer for (programs.items) |p| p.deinit(); - for (srcs) |src| { - try programs.append(try Program.init(src)); - } - - // Create the texture for the framebuffer - const fb_tex = try gl.Texture.create(); - errdefer fb_tex.destroy(); - { - const texbind = try fb_tex.bind(.@"2D"); - try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); - try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); - try texbind.image2D( - 0, - .rgb, - 1, - 1, - 0, - .rgb, - .UnsignedByte, - null, - ); - } - - // Create our framebuffer for rendering off screen. - // The shader prior to custom shaders should use this - // framebuffer. - const fbo = try gl.Framebuffer.create(); - errdefer fbo.destroy(); - const fbbind = try fbo.bind(.framebuffer); - defer fbbind.unbind(); - try fbbind.texture2D(.color0, .@"2D", fb_tex, 0); - const fbstatus = fbbind.checkStatus(); - if (fbstatus != .complete) { - log.warn( - "framebuffer is not complete state={}", - .{fbstatus}, - ); - return error.InvalidFramebuffer; - } - - // Create our uniform buffer that is shared across all - // custom shaders - const ubo = try gl.Buffer.create(); - errdefer ubo.destroy(); - { - var ubobind = try ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setDataNull(Uniforms, .static_draw); - } - - // Setup our VAO for the custom shader. - const vao = try gl.VertexArray.create(); - errdefer vao.destroy(); - const vaobind = try vao.bind(); - defer vaobind.unbind(); - - // Element buffer (EBO) - const ebo = try gl.Buffer.create(); - errdefer ebo.destroy(); - var ebobind = try ebo.bind(.element_array); - defer ebobind.unbind(); - try ebobind.setData([6]u8{ - 0, 1, 3, // Top-left triangle - 1, 2, 3, // Bottom-right triangle - }, .static_draw); - - return .{ - .programs = try programs.toOwnedSlice(), - .uniforms = .{}, - .fbo = fbo, - .ubo = ubo, - .vao = vao, - .ebo = ebo, - .fb_texture = fb_tex, - .first_frame_time = try std.time.Instant.now(), - .last_frame_time = try std.time.Instant.now(), - }; - } - - pub fn deinit(self: *const State, alloc: Allocator) void { - for (self.programs) |p| p.deinit(); - alloc.free(self.programs); - self.ubo.destroy(); - self.ebo.destroy(); - self.vao.destroy(); - self.fb_texture.destroy(); - self.fbo.destroy(); - } - - pub fn setScreenSize(self: *State, size: Size) !void { - // Update our uniforms - self.uniforms.resolution = .{ - @floatFromInt(size.screen.width), - @floatFromInt(size.screen.height), - 1, - }; - try self.syncUniforms(); - - // Update our texture - const texbind = try self.fb_texture.bind(.@"2D"); - try texbind.image2D( - 0, - .rgb, - @intCast(size.screen.width), - @intCast(size.screen.height), - 0, - .rgb, - .UnsignedByte, - null, - ); - } - - /// Call this prior to drawing a frame to update the time - /// and synchronize the uniforms. This synchronizes uniforms - /// so you should make changes to uniforms prior to calling - /// this. - pub fn newFrame(self: *State) !void { - // Update our frame time - const now = std.time.Instant.now() catch self.first_frame_time; - const since_ns: f32 = @floatFromInt(now.since(self.first_frame_time)); - const delta_ns: f32 = @floatFromInt(now.since(self.last_frame_time)); - self.uniforms.time = since_ns / std.time.ns_per_s; - self.uniforms.time_delta = delta_ns / std.time.ns_per_s; - self.last_frame_time = now; - - // Sync our uniform changes - try self.syncUniforms(); - } - - fn syncUniforms(self: *State) !void { - var ubobind = try self.ubo.bind(.uniform); - defer ubobind.unbind(); - try ubobind.setData(self.uniforms, .static_draw); - } - - /// Call this to bind all the necessary OpenGL resources for - /// all custom shaders. Each individual shader needs to be bound - /// one at a time too. - pub fn bind(self: *const State) !Binding { - // Move our uniform buffer into proper global index. Note that - // in theory we can do this globally once and never worry about - // it again. I don't think we're high-performance enough at all - // to worry about that and this makes it so you can just move - // around CustomProgram usage without worrying about clobbering - // the global state. - try self.ubo.bindBase(.uniform, UNIFORM_INDEX); - - // Bind our texture that is shared amongst all - try gl.Texture.active(gl.c.GL_TEXTURE0); - var texbind = try self.fb_texture.bind(.@"2D"); - errdefer texbind.unbind(); - - const vao = try self.vao.bind(); - errdefer vao.unbind(); - - const ebo = try self.ebo.bind(.element_array); - errdefer ebo.unbind(); - - return .{ - .vao = vao, - .ebo = ebo, - .fb_texture = texbind, - }; - } - - /// Copy the fbo's attached texture to the backbuffer. - pub fn copyFramebuffer(self: *State) !void { - const texbind = try self.fb_texture.bind(.@"2D"); - errdefer texbind.unbind(); - try texbind.copySubImage2D( - 0, - 0, - 0, - 0, - 0, - @intFromFloat(self.uniforms.resolution[0]), - @intFromFloat(self.uniforms.resolution[1]), - ); - } - - pub const Binding = struct { - vao: gl.VertexArray.Binding, - ebo: gl.Buffer.Binding, - fb_texture: gl.Texture.Binding, - - pub fn unbind(self: Binding) void { - self.ebo.unbind(); - self.vao.unbind(); - self.fb_texture.unbind(); - } - }; -}; - -/// A single OpenGL program (combined shaders) for custom shaders. -pub const Program = struct { - program: gl.Program, - - pub fn init(src: [:0]const u8) !Program { - const program = try gl.Program.createVF( - @embedFile("../shaders/custom.v.glsl"), - src, - ); - errdefer program.destroy(); - - // Map our uniform buffer to the global GL state - try program.uniformBlockBinding(UNIFORM_INDEX, UNIFORM_BINDING); - - return .{ .program = program }; - } - - pub fn deinit(self: *const Program) void { - self.program.destroy(); - } - - /// Bind the program for use. This should be called so that draw can - /// be called. - pub fn bind(self: *const Program) !Binding { - const program = try self.program.use(); - errdefer program.unbind(); - - return .{ - .program = program, - }; - } - - pub const Binding = struct { - program: gl.Program.Binding, - - pub fn unbind(self: Binding) void { - self.program.unbind(); - } - - pub fn draw(self: Binding) !void { - _ = self; - try gl.drawElementsInstanced( - gl.c.GL_TRIANGLES, - 6, - gl.c.GL_UNSIGNED_BYTE, - 1, - ); - } - }; -}; diff --git a/src/renderer/opengl/image.zig b/src/renderer/opengl/image.zig index 26cd90736..77779fb8a 100644 --- a/src/renderer/opengl/image.zig +++ b/src/renderer/opengl/image.zig @@ -3,6 +3,8 @@ const Allocator = std.mem.Allocator; const assert = std.debug.assert; const gl = @import("opengl"); const wuffs = @import("wuffs"); +const OpenGL = @import("../OpenGL.zig"); +const Texture = OpenGL.Texture; /// Represents a single image placement on the grid. A placement is a /// request to render an instance of an image. @@ -59,15 +61,15 @@ pub const Image = union(enum) { replace_rgba: Replace, /// The image is uploaded and ready to be used. - ready: gl.Texture, + ready: Texture, /// The image is uploaded but is scheduled to be unloaded. unload_pending: []u8, - unload_ready: gl.Texture, - unload_replace: struct { []u8, gl.Texture }, + unload_ready: Texture, + unload_replace: struct { []u8, Texture }, pub const Replace = struct { - texture: gl.Texture, + texture: Texture, pending: Pending, }; @@ -99,32 +101,32 @@ pub const Image = union(enum) { .replace_gray => |r| { alloc.free(r.pending.dataSlice(1)); - r.texture.destroy(); + r.texture.deinit(); }, .replace_gray_alpha => |r| { alloc.free(r.pending.dataSlice(2)); - r.texture.destroy(); + r.texture.deinit(); }, .replace_rgb => |r| { alloc.free(r.pending.dataSlice(3)); - r.texture.destroy(); + r.texture.deinit(); }, .replace_rgba => |r| { alloc.free(r.pending.dataSlice(4)); - r.texture.destroy(); + r.texture.deinit(); }, .unload_replace => |r| { alloc.free(r[0]); - r[1].destroy(); + r[1].deinit(); }, .ready, .unload_ready, - => |tex| tex.destroy(), + => |tex| tex.deinit(), } } @@ -168,7 +170,7 @@ pub const Image = union(enum) { // Get our existing texture. This switch statement will also handle // scenarios where there is no existing texture and we can modify // the self pointer directly. - const existing: gl.Texture = switch (self.*) { + const existing: Texture = switch (self.*) { // For pending, we can free the old data and become pending ourselves. .pending_gray => |p| { alloc.free(p.dataSlice(1)); @@ -356,7 +358,10 @@ pub const Image = union(enum) { pub fn upload( self: *Image, alloc: Allocator, + opengl: *const OpenGL, ) !void { + _ = opengl; + // Convert our data if we have to try self.convert(alloc); @@ -374,23 +379,15 @@ pub const Image = union(enum) { }; // Create our texture - const tex = try gl.Texture.create(); - errdefer tex.destroy(); - - const texbind = try tex.bind(.@"2D"); - try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); - try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); - try texbind.image2D( - 0, - formats.internal, + const tex = try Texture.init( + .{ + .format = formats.format, + .internal_format = formats.internal, + .target = .Rectangle, + }, @intCast(p.width), @intCast(p.height), - 0, - formats.format, - .UnsignedByte, - p.data, + p.data[0 .. p.width * p.height * self.depth()], ); // Uploaded. We can now clear our data and change our state. diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig new file mode 100644 index 000000000..253ae8719 --- /dev/null +++ b/src/renderer/opengl/shaders.zig @@ -0,0 +1,310 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const math = @import("../../math.zig"); + +const Pipeline = @import("Pipeline.zig"); + +const log = std.log.scoped(.opengl); + +/// This contains the state for the shaders used by the Metal renderer. +pub const Shaders = struct { + /// Renders cell foreground elements (text, decorations). + cell_text_pipeline: Pipeline, + + /// The cell background shader is the shader used to render the + /// background of terminal cells. + cell_bg_pipeline: Pipeline, + + /// The image shader is the shader used to render images for things + /// like the Kitty image protocol. + image_pipeline: Pipeline, + + /// Custom shaders to run against the final drawable texture. This + /// can be used to apply a lot of effects. Each shader is run in sequence + /// against the output of the previous shader. + post_pipelines: []const Pipeline, + + /// Set to true when deinited, if you try to deinit a defunct set + /// of shaders it will just be ignored, to prevent double-free. + defunct: bool = false, + + /// Initialize our shader set. + /// + /// "post_shaders" is an optional list of postprocess shaders to run + /// against the final drawable texture. This is an array of shader source + /// code, not file paths. + pub fn init( + alloc: Allocator, + post_shaders: []const [:0]const u8, + ) !Shaders { + const cell_text_pipeline = try initCellTextPipeline(); + errdefer cell_text_pipeline.deinit(); + + const cell_bg_pipeline = try initCellBgPipeline(); + errdefer cell_bg_pipeline.deinit(); + + const image_pipeline = try initImagePipeline(); + errdefer image_pipeline.deinit(); + + const post_pipelines: []const Pipeline = initPostPipelines( + alloc, + post_shaders, + ) catch |err| err: { + // If an error happens while building postprocess shaders we + // want to just not use any postprocess shaders since we don't + // want to block Ghostty from working. + log.warn("error initializing postprocess shaders err={}", .{err}); + break :err &.{}; + }; + errdefer if (post_pipelines.len > 0) { + for (post_pipelines) |pipeline| pipeline.deinit(); + alloc.free(post_pipelines); + }; + + return .{ + .cell_text_pipeline = cell_text_pipeline, + .cell_bg_pipeline = cell_bg_pipeline, + .image_pipeline = image_pipeline, + .post_pipelines = post_pipelines, + }; + } + + pub fn deinit(self: *Shaders, alloc: Allocator) void { + if (self.defunct) return; + self.defunct = true; + + // Release our primary shaders + self.cell_text_pipeline.deinit(); + self.cell_bg_pipeline.deinit(); + self.image_pipeline.deinit(); + + // Release our postprocess shaders + if (self.post_pipelines.len > 0) { + for (self.post_pipelines) |pipeline| { + pipeline.deinit(); + } + alloc.free(self.post_pipelines); + } + } +}; + +/// Single parameter for the image shader. See shader for field details. +pub const Image = extern struct { + grid_pos: [2]f32 align(8), + cell_offset: [2]f32 align(8), + source_rect: [4]f32 align(16), + dest_size: [2]f32 align(8), +}; + +/// The uniforms that are passed to the terminal cell shader. +pub const Uniforms = extern struct { + /// The projection matrix for turning world coordinates to normalized. + /// This is calculated based on the size of the screen. + projection_matrix: math.Mat align(16), + + /// Size of a single cell in pixels, unscaled. + cell_size: [2]f32 align(8), + + /// Size of the grid in columns and rows. + grid_size: [2]u16 align(4), + + /// The padding around the terminal grid in pixels. In order: + /// top, right, bottom, left. + grid_padding: [4]f32 align(16), + + /// Bit mask defining which directions to + /// extend cell colors in to the padding. + /// Order, LSB first: left, right, up, down + padding_extend: PaddingExtend align(4), + + /// The minimum contrast ratio for text. The contrast ratio is calculated + /// according to the WCAG 2.0 spec. + min_contrast: f32 align(4), + + /// The cursor position and color. + cursor_pos: [2]u16 align(4), + cursor_color: [4]u8 align(4), + + /// The background color for the whole surface. + bg_color: [4]u8 align(4), + + /// Various booleans, in a packed struct for space efficiency. + bools: Bools align(4), + + const Bools = packed struct(u32) { + /// Whether the cursor is 2 cells wide. + cursor_wide: bool, + + /// Indicates that colors provided to the shader are already in + /// the P3 color space, so they don't need to be converted from + /// sRGB. + use_display_p3: bool, + + /// Indicates that the color attachments for the shaders have + /// an `*_srgb` pixel format, which means the shaders need to + /// output linear RGB colors rather than gamma encoded colors, + /// since blending will be performed in linear space and then + /// Metal itself will re-encode the colors for storage. + use_linear_blending: bool, + + /// Enables a weight correction step that makes text rendered + /// with linear alpha blending have a similar apparent weight + /// (thickness) to gamma-incorrect blending. + use_linear_correction: bool = false, + + _padding: u28 = 0, + }; + + const PaddingExtend = packed struct(u32) { + left: bool = false, + right: bool = false, + up: bool = false, + down: bool = false, + _padding: u28 = 0, + }; +}; + +/// The uniforms used for custom postprocess shaders. +pub const PostUniforms = extern struct { + resolution: [3]f32 align(16), + time: f32 align(4), + time_delta: f32 align(4), + frame_rate: f32 align(4), + frame: i32 align(4), + channel_time: [4][4]f32 align(16), + channel_resolution: [4][4]f32 align(16), + mouse: [4]f32 align(16), + date: [4]f32 align(16), + sample_rate: f32 align(4), +}; + +/// Initialize our custom shader pipelines. The shaders argument is a +/// set of shader source code, not file paths. +fn initPostPipelines( + alloc: Allocator, + shaders: []const [:0]const u8, +) ![]const Pipeline { + // If we have no shaders, do nothing. + if (shaders.len == 0) return &.{}; + + // Keeps track of how many shaders we successfully wrote. + var i: usize = 0; + + // Initialize our result set. If any error happens, we undo everything. + var pipelines = try alloc.alloc(Pipeline, shaders.len); + errdefer { + for (pipelines[0..i]) |pipeline| { + pipeline.deinit(); + } + alloc.free(pipelines); + } + + // Build each shader. Note we don't use "0.." to build our index + // because we need to keep track of our length to clean up above. + for (shaders) |source| { + pipelines[i] = try initPostPipeline(source); + i += 1; + } + + return pipelines; +} + +/// Initialize a single custom shader pipeline from shader source. +fn initPostPipeline(data: [:0]const u8) !Pipeline { + return try Pipeline.init(null, .{ + .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"), + .fragment_fn = data, + }); +} + +/// This is a single parameter for the terminal cell shader. +pub const CellText = extern struct { + glyph_pos: [2]u32 align(8) = .{ 0, 0 }, + glyph_size: [2]u32 align(8) = .{ 0, 0 }, + bearings: [2]i16 align(4) = .{ 0, 0 }, + grid_pos: [2]u16 align(4), + color: [4]u8 align(4), + mode: Mode align(4), + constraint_width: u32 align(4) = 0, + + pub const Mode = enum(u32) { + fg = 1, + fg_constrained = 2, + fg_color = 3, + cursor = 4, + fg_powerline = 5, + }; + + // test { + // // Minimizing the size of this struct is important, + // // so we test it in order to be aware of any changes. + // try std.testing.expectEqual(32, @sizeOf(CellText)); + // } +}; + +/// Initialize the cell render pipeline. +fn initCellTextPipeline() !Pipeline { + return try Pipeline.init(CellText, .{ + .vertex_fn = loadShaderCode("../shaders/glsl/cell_text.v.glsl"), + .fragment_fn = loadShaderCode("../shaders/glsl/cell_text.f.glsl"), + .step_fn = .per_instance, + }); +} + +/// This is a single parameter for the cell bg shader. +pub const CellBg = [4]u8; + +/// Initialize the cell background render pipeline. +fn initCellBgPipeline() !Pipeline { + return try Pipeline.init(null, .{ + .vertex_fn = loadShaderCode("../shaders/glsl/full_screen.v.glsl"), + .fragment_fn = loadShaderCode("../shaders/glsl/cell_bg.f.glsl"), + }); +} + +/// Initialize the image render pipeline. +fn initImagePipeline() !Pipeline { + return try Pipeline.init(Image, .{ + .vertex_fn = loadShaderCode("../shaders/glsl/image.v.glsl"), + .fragment_fn = loadShaderCode("../shaders/glsl/image.f.glsl"), + .step_fn = .per_instance, + }); +} + +/// Load shader code from the target path, processing `#include` directives. +/// +/// Comptime only for now, this code is really sloppy and makes a bunch of +/// assumptions about things being well formed and file names not containing +/// quote marks. If we ever want to process `#include`s for custom shaders +/// then we need to write something better than this for it. +fn loadShaderCode(comptime path: []const u8) [:0]const u8 { + return comptime processIncludes(@embedFile(path), std.fs.path.dirname(path).?); +} + +/// Used by loadShaderCode +fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 { + @setEvalBranchQuota(100_000); + var i: usize = 0; + while (i < contents.len) { + if (std.mem.startsWith(u8, contents[i..], "#include")) { + assert(std.mem.startsWith(u8, contents[i..], "#include \"")); + const start = i + "#include \"".len; + const end = std.mem.indexOfScalarPos(u8, contents, start, '"').?; + return std.fmt.comptimePrint( + "{s}{s}{s}", + .{ + contents[0..i], + @embedFile(basedir ++ .{std.fs.path.sep} ++ contents[start..end]), + processIncludes(contents[end + 1 ..], basedir), + }, + ); + } + if (std.mem.indexOfPos(u8, contents, i, "\n#")) |j| { + i = (j + 1); + } else { + break; + } + } + return contents; +} diff --git a/src/renderer/shaders/cell.f.glsl b/src/renderer/shaders/cell.f.glsl deleted file mode 100644 index f9c1ce2b1..000000000 --- a/src/renderer/shaders/cell.f.glsl +++ /dev/null @@ -1,53 +0,0 @@ -#version 330 core - -in vec2 glyph_tex_coords; -flat in uint mode; - -// The color for this cell. If this is a background pass this is the -// background color. Otherwise, this is the foreground color. -flat in vec4 color; - -// The position of the cells top-left corner. -flat in vec2 screen_cell_pos; - -// Position the fragment coordinate to the upper left -layout(origin_upper_left) in vec4 gl_FragCoord; - -// Must declare this output for some versions of OpenGL. -layout(location = 0) out vec4 out_FragColor; - -// Font texture -uniform sampler2D text; -uniform sampler2D text_color; - -// Dimensions of the cell -uniform vec2 cell_size; - -// See vertex shader -const uint MODE_BG = 1u; -const uint MODE_FG = 2u; -const uint MODE_FG_CONSTRAINED = 3u; -const uint MODE_FG_COLOR = 7u; -const uint MODE_FG_POWERLINE = 15u; - -void main() { - float a; - - switch (mode) { - case MODE_BG: - out_FragColor = color; - break; - - case MODE_FG: - case MODE_FG_CONSTRAINED: - case MODE_FG_POWERLINE: - a = texture(text, glyph_tex_coords).r; - vec3 premult = color.rgb * color.a; - out_FragColor = vec4(premult.rgb*a, a); - break; - - case MODE_FG_COLOR: - out_FragColor = texture(text_color, glyph_tex_coords); - break; - } -} diff --git a/src/renderer/shaders/cell.metal b/src/renderer/shaders/cell.metal index 5b3875221..039c600ed 100644 --- a/src/renderer/shaders/cell.metal +++ b/src/renderer/shaders/cell.metal @@ -249,20 +249,12 @@ vertex CellBgVertexOut cell_bg_vertex( fragment float4 cell_bg_fragment( CellBgVertexOut in [[stage_in]], - constant uchar4 *cells [[buffer(0)]], - constant Uniforms& uniforms [[buffer(1)]] + constant Uniforms& uniforms [[buffer(1)]], + constant uchar4 *cells [[buffer(2)]] ) { int2 grid_pos = int2(floor((in.position.xy - uniforms.grid_padding.wx) / uniforms.cell_size)); - float4 bg = float4(0.0); - // If we have any background transparency then we render bg-colored cells as - // fully transparent, since the background is handled by the layer bg color - // and we don't want to double up our bg color, but if our bg color is fully - // opaque then our layer is opaque and can't handle transparency, so we need - // to return the bg color directly instead. - if (uniforms.bg_color.a == 255) { - bg = in.bg_color; - } + float4 bg = in.bg_color; // Clamp x position, extends edge bg colors in to padding on sides. if (grid_pos.x < 0) { @@ -374,19 +366,23 @@ vertex CellTextVertexOut cell_text_vertex( // Convert the grid x, y into world space x, y by accounting for cell size float2 cell_pos = uniforms.cell_size * float2(in.grid_pos); - // Turn the cell position into a vertex point depending on the - // vertex ID. Since we use instanced drawing, we have 4 vertices - // for each corner of the cell. We can use vertex ID to determine - // which one we're looking at. Using this, we can use 1 or 0 to keep - // or discard the value for the vertex. + // We use a triangle strip with 4 vertices to render quads, + // so we determine which corner of the cell this vertex is in + // based on the vertex ID. // - // 0 = top-right - // 1 = bot-right - // 2 = bot-left - // 3 = top-left + // 0 --> 1 + // | .'| + // | / | + // | L | + // 2 --> 3 + // + // 0 = top-left (0, 0) + // 1 = top-right (1, 0) + // 2 = bot-left (0, 1) + // 3 = bot-right (1, 1) float2 corner; - corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; - corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; + corner.x = float(vid == 1 || vid == 3); + corner.y = float(vid == 2 || vid == 3); CellTextVertexOut out; out.mode = in.mode; @@ -502,7 +498,7 @@ fragment float4 cell_text_fragment( CellTextVertexOut in [[stage_in]], texture2d textureGrayscale [[texture(0)]], texture2d textureColor [[texture(1)]], - constant Uniforms& uniforms [[buffer(2)]] + constant Uniforms& uniforms [[buffer(1)]] ) { constexpr sampler textureSampler( coord::pixel, @@ -621,19 +617,23 @@ vertex ImageVertexOut image_vertex( texture2d image [[texture(0)]], constant Uniforms& uniforms [[buffer(1)]] ) { - // Turn the image position into a vertex point depending on the - // vertex ID. Since we use instanced drawing, we have 4 vertices - // for each corner of the cell. We can use vertex ID to determine - // which one we're looking at. Using this, we can use 1 or 0 to keep - // or discard the value for the vertex. + // We use a triangle strip with 4 vertices to render quads, + // so we determine which corner of the cell this vertex is in + // based on the vertex ID. // - // 0 = top-right - // 1 = bot-right - // 2 = bot-left - // 3 = top-left + // 0 --> 1 + // | .'| + // | / | + // | L | + // 2 --> 3 + // + // 0 = top-left (0, 0) + // 1 = top-right (1, 0) + // 2 = bot-left (0, 1) + // 3 = bot-right (1, 1) float2 corner; - corner.x = (vid == 0 || vid == 1) ? 1.0f : 0.0f; - corner.y = (vid == 0 || vid == 3) ? 0.0f : 1.0f; + corner.x = float(vid == 1 || vid == 3); + corner.y = float(vid == 2 || vid == 3); // The texture coordinates start at our source x/y // and add the width/height depending on the corner. diff --git a/src/renderer/shaders/cell.v.glsl b/src/renderer/shaders/cell.v.glsl deleted file mode 100644 index f37e69adc..000000000 --- a/src/renderer/shaders/cell.v.glsl +++ /dev/null @@ -1,258 +0,0 @@ -#version 330 core - -// These are the possible modes that "mode" can be set to. This is -// used to multiplex multiple render modes into a single shader. -// -// NOTE: this must be kept in sync with the fragment shader -const uint MODE_BG = 1u; -const uint MODE_FG = 2u; -const uint MODE_FG_CONSTRAINED = 3u; -const uint MODE_FG_COLOR = 7u; -const uint MODE_FG_POWERLINE = 15u; - -// The grid coordinates (x, y) where x < columns and y < rows -layout (location = 0) in vec2 grid_coord; - -// Position of the glyph in the texture. -layout (location = 1) in vec2 glyph_pos; - -// Width/height of the glyph -layout (location = 2) in vec2 glyph_size; - -// Offset of the top-left corner of the glyph when rendered in a rect. -layout (location = 3) in vec2 glyph_offset; - -// The color for this cell in RGBA (0 to 1.0). Background or foreground -// depends on mode. -layout (location = 4) in vec4 color_in; - -// Only set for MODE_FG, this is the background color of the FG text. -// This is used to detect minimal contrast for the text. -layout (location = 5) in vec4 bg_color_in; - -// The mode of this shader. The mode determines what fields are used, -// what the output will be, etc. This shader is capable of executing in -// multiple "modes" so that we can share some logic and so that we can draw -// the entire terminal grid in a single GPU pass. -layout (location = 6) in uint mode_in; - -// The width in cells of this item. -layout (location = 7) in uint grid_width; - -// The background or foreground color for the fragment, depending on -// whether this is a background or foreground pass. -flat out vec4 color; - -// The x/y coordinate for the glyph representing the font. -out vec2 glyph_tex_coords; - -// The position of the cell top-left corner in screen cords. z and w -// are width and height. -flat out vec2 screen_cell_pos; - -// Pass the mode forward to the fragment shader. -flat out uint mode; - -uniform sampler2D text; -uniform sampler2D text_color; -uniform vec2 cell_size; -uniform vec2 grid_size; -uniform vec4 grid_padding; -uniform bool padding_vertical_top; -uniform bool padding_vertical_bottom; -uniform mat4 projection; -uniform float min_contrast; - -/******************************************************************** - * Modes - * - *------------------------------------------------------------------- - * MODE_BG - * - * In MODE_BG, this shader renders only the background color for the - * cell. This is a simple mode where we generate a simple rectangle - * made up of 4 vertices and then it is filled. In this mode, the output - * "color" is the fill color for the bg. - * - *------------------------------------------------------------------- - * MODE_FG - * - * In MODE_FG, the shader renders the glyph onto this cell and utilizes - * the glyph texture "text". In this mode, the output "color" is the - * fg color to use for the glyph. - * - */ - -//------------------------------------------------------------------- -// Color Functions -//------------------------------------------------------------------- - -// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef -float luminance_component(float c) { - if (c <= 0.03928) { - return c / 12.92; - } else { - return pow((c + 0.055) / 1.055, 2.4); - } -} - -float relative_luminance(vec3 color) { - vec3 color_adjusted = vec3( - luminance_component(color.r), - luminance_component(color.g), - luminance_component(color.b) - ); - - vec3 weights = vec3(0.2126, 0.7152, 0.0722); - return dot(color_adjusted, weights); -} - -// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef -float contrast_ratio(vec3 color1, vec3 color2) { - float luminance1 = relative_luminance(color1) + 0.05; - float luminance2 = relative_luminance(color2) + 0.05; - return max(luminance1, luminance2) / min(luminance1, luminance2); -} - -// Return the fg if the contrast ratio is greater than min, otherwise -// return a color that satisfies the contrast ratio. Currently, the color -// is always white or black, whichever has the highest contrast ratio. -vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) { - vec3 fg_premult = fg.rgb * fg.a; - vec3 bg_premult = bg.rgb * bg.a; - float ratio = contrast_ratio(fg_premult, bg_premult); - if (ratio < min_ratio) { - float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg_premult); - float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg_premult); - if (white_ratio > black_ratio) { - return vec4(1.0, 1.0, 1.0, fg.a); - } else { - return vec4(0.0, 0.0, 0.0, fg.a); - } - } - - return fg; -} - -//------------------------------------------------------------------- -// Main -//------------------------------------------------------------------- - -void main() { - // We always forward our mode unmasked because the fragment - // shader doesn't use any of the masks. - mode = mode_in; - - // Top-left cell coordinates converted to world space - // Example: (1,0) with a 30 wide cell is converted to (30,0) - vec2 cell_pos = cell_size * grid_coord; - - // Our Z value. For now we just use grid_z directly but we pull it - // out here so the variable name is more uniform to our cell_pos and - // in case we want to do any other math later. - float cell_z = 0.0; - - // Turn the cell position into a vertex point depending on the - // gl_VertexID. Since we use instanced drawing, we have 4 vertices - // for each corner of the cell. We can use gl_VertexID to determine - // which one we're looking at. Using this, we can use 1 or 0 to keep - // or discard the value for the vertex. - // - // 0 = top-right - // 1 = bot-right - // 2 = bot-left - // 3 = top-left - vec2 position; - position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; - position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; - - // Scaled for wide chars - vec2 cell_size_scaled = cell_size; - cell_size_scaled.x = cell_size_scaled.x * grid_width; - - switch (mode) { - case MODE_BG: - // If we're at the edge of the grid, we add our padding to the background - // to extend it. Note: grid_padding is top/right/bottom/left. - if (grid_coord.y == 0 && padding_vertical_top) { - cell_pos.y -= grid_padding.r; - cell_size_scaled.y += grid_padding.r; - } else if (grid_coord.y == grid_size.y - 1 && padding_vertical_bottom) { - cell_size_scaled.y += grid_padding.b; - } - if (grid_coord.x == 0) { - cell_pos.x -= grid_padding.a; - cell_size_scaled.x += grid_padding.a; - } else if (grid_coord.x == grid_size.x - 1) { - cell_size_scaled.x += grid_padding.g; - } - - // Calculate the final position of our cell in world space. - // We have to add our cell size since our vertices are offset - // one cell up and to the left. (Do the math to verify yourself) - cell_pos = cell_pos + cell_size_scaled * position; - - gl_Position = projection * vec4(cell_pos, cell_z, 1.0); - color = color_in / 255.0; - break; - - case MODE_FG: - case MODE_FG_CONSTRAINED: - case MODE_FG_COLOR: - case MODE_FG_POWERLINE: - vec2 glyph_offset_calc = glyph_offset; - - // The glyph_offset.y is the y bearing, a y value that when added - // to the baseline is the offset (+y is up). Our grid goes down. - // So we flip it with `cell_size.y - glyph_offset.y`. - glyph_offset_calc.y = cell_size_scaled.y - glyph_offset_calc.y; - - // If this is a constrained mode, we need to constrain it! - vec2 glyph_size_calc = glyph_size; - if (mode == MODE_FG_CONSTRAINED) { - if (glyph_size.x > cell_size_scaled.x) { - float new_y = glyph_size.y * (cell_size_scaled.x / glyph_size.x); - glyph_offset_calc.y = glyph_offset_calc.y + ((glyph_size.y - new_y) / 2); - glyph_size_calc.y = new_y; - glyph_size_calc.x = cell_size_scaled.x; - } - } - - // Calculate the final position of the cell. - cell_pos = cell_pos + (glyph_size_calc * position) + glyph_offset_calc; - gl_Position = projection * vec4(cell_pos, cell_z, 1.0); - - // We need to convert our texture position and size to normalized - // device coordinates (0 to 1.0) by dividing by the size of the texture. - ivec2 text_size; - switch(mode) { - case MODE_FG_CONSTRAINED: - case MODE_FG_POWERLINE: - case MODE_FG: - text_size = textureSize(text, 0); - break; - - case MODE_FG_COLOR: - text_size = textureSize(text_color, 0); - break; - } - vec2 glyph_tex_pos = glyph_pos / text_size; - vec2 glyph_tex_size = glyph_size / text_size; - glyph_tex_coords = glyph_tex_pos + glyph_tex_size * position; - - // If we have a minimum contrast, we need to check if we need to - // change the color of the text to ensure it has enough contrast - // with the background. - // We only apply this adjustment to "normal" text with MODE_FG, - // since we want color glyphs to appear in their original color - // and Powerline glyphs to be unaffected (else parts of the line would - // have different colors as some parts are displayed via background colors). - vec4 color_final = color_in / 255.0; - if (min_contrast > 1.0 && mode == MODE_FG) { - vec4 bg_color = bg_color_in / 255.0; - color_final = contrasted_color(min_contrast, color_final, bg_color); - } - color = color_final; - break; - } -} diff --git a/src/renderer/shaders/custom.v.glsl b/src/renderer/shaders/custom.v.glsl deleted file mode 100644 index 653e1800e..000000000 --- a/src/renderer/shaders/custom.v.glsl +++ /dev/null @@ -1,8 +0,0 @@ -#version 330 core - -void main(){ - vec2 position; - position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? -1. : 1.; - position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 1. : -1.; - gl_Position = vec4(position.xy, 0.0f, 1.0f); -} diff --git a/src/renderer/shaders/glsl/cell_bg.f.glsl b/src/renderer/shaders/glsl/cell_bg.f.glsl new file mode 100644 index 000000000..cfd598f95 --- /dev/null +++ b/src/renderer/shaders/glsl/cell_bg.f.glsl @@ -0,0 +1,61 @@ +#include "common.glsl" + +// Position the origin to the upper left +layout(origin_upper_left, pixel_center_integer) in vec4 gl_FragCoord; + +// Must declare this output for some versions of OpenGL. +layout(location = 0) out vec4 out_FragColor; + +layout(binding = 1, std430) readonly buffer bg_cells { + uint cells[]; +}; + +vec4 cell_bg() { + uvec2 grid_size = unpack2u16(grid_size_packed_2u16); + ivec2 grid_pos = ivec2(floor((gl_FragCoord.xy - grid_padding.wx) / cell_size)); + bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0; + + vec4 bg = load_color(unpack4u8(bg_color_packed_4u8), use_linear_blending); + + // Clamp x position, extends edge bg colors in to padding on sides. + if (grid_pos.x < 0) { + if ((padding_extend & EXTEND_LEFT) != 0) { + grid_pos.x = 0; + } else { + return bg; + } + } else if (grid_pos.x > grid_size.x - 1) { + if ((padding_extend & EXTEND_RIGHT) != 0) { + grid_pos.x = int(grid_size.x) - 1; + } else { + return bg; + } + } + + // Clamp y position if we should extend, otherwise discard if out of bounds. + if (grid_pos.y < 0) { + if ((padding_extend & EXTEND_UP) != 0) { + grid_pos.y = 0; + } else { + return bg; + } + } else if (grid_pos.y > grid_size.y - 1) { + if ((padding_extend & EXTEND_DOWN) != 0) { + grid_pos.y = int(grid_size.y) - 1; + } else { + return bg; + } + } + + // Load the color for the cell. + vec4 cell_color = load_color( + unpack4u8(cells[grid_pos.y * grid_size.x + grid_pos.x]), + use_linear_blending + ); + + return cell_color; +} + +void main() { + out_FragColor = cell_bg(); +} diff --git a/src/renderer/shaders/glsl/cell_text.f.glsl b/src/renderer/shaders/glsl/cell_text.f.glsl new file mode 100644 index 000000000..fda552424 --- /dev/null +++ b/src/renderer/shaders/glsl/cell_text.f.glsl @@ -0,0 +1,109 @@ +#include "common.glsl" + +layout(binding = 0) uniform sampler2DRect atlas_grayscale; +layout(binding = 1) uniform sampler2DRect atlas_color; + +in CellTextVertexOut { + flat uint mode; + flat vec4 color; + flat vec4 bg_color; + vec2 tex_coord; +} in_data; + +// These are the possible modes that "mode" can be set to. This is +// used to multiplex multiple render modes into a single shader. +// +// NOTE: this must be kept in sync with the fragment shader +const uint MODE_TEXT = 1u; +const uint MODE_TEXT_CONSTRAINED = 2u; +const uint MODE_TEXT_COLOR = 3u; +const uint MODE_TEXT_CURSOR = 4u; +const uint MODE_TEXT_POWERLINE = 5u; + +// Must declare this output for some versions of OpenGL. +layout(location = 0) out vec4 out_FragColor; + +void main() { + bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0; + bool use_linear_correction = (bools & USE_LINEAR_CORRECTION) != 0; + + switch (in_data.mode) { + default: + case MODE_TEXT_CURSOR: + case MODE_TEXT_CONSTRAINED: + case MODE_TEXT_POWERLINE: + case MODE_TEXT: + { + // Our input color is always linear. + vec4 color = in_data.color; + + // If we're not doing linear blending, then we need to + // re-apply the gamma encoding to our color manually. + // + // Since the alpha is premultiplied, we need to divide + // it out before unlinearizing and re-multiply it after. + if (!use_linear_blending) { + color.rgb /= vec3(color.a); + color = unlinearize(color); + color.rgb *= vec3(color.a); + } + + // Fetch our alpha mask for this pixel. + float a = texture(atlas_grayscale, in_data.tex_coord).r; + + // Linear blending weight correction corrects the alpha value to + // produce blending results which match gamma-incorrect blending. + if (use_linear_correction) { + // Short explanation of how this works: + // + // We get the luminances of the foreground and background colors, + // and then unlinearize them and perform blending on them. This + // gives us our desired luminance, which we derive our new alpha + // value from by mapping the range [bg_l, fg_l] to [0, 1], since + // our final blend will be a linear interpolation from bg to fg. + // + // This yields virtually identical results for grayscale blending, + // and very similar but non-identical results for color blending. + vec4 bg = in_data.bg_color; + float fg_l = luminance(color.rgb); + float bg_l = luminance(bg.rgb); + // To avoid numbers going haywire, we don't apply correction + // when the bg and fg luminances are within 0.001 of each other. + if (abs(fg_l - bg_l) > 0.001) { + float blend_l = linearize(unlinearize(fg_l) * a + unlinearize(bg_l) * (1.0 - a)); + a = clamp((blend_l - bg_l) / (fg_l - bg_l), 0.0, 1.0); + } + } + + // Multiply our whole color by the alpha mask. + // Since we use premultiplied alpha, this is + // the correct way to apply the mask. + color *= a; + + out_FragColor = color; + return; + } + + case MODE_TEXT_COLOR: + { + // For now, we assume that color glyphs + // are already premultiplied sRGB colors. + vec4 color = texture(atlas_color, in_data.tex_coord); + + // If we aren't doing linear blending, we can return this right away. + if (!use_linear_blending) { + out_FragColor = color; + return; + } + + // Otherwise we need to linearize the color. Since the alpha is + // premultiplied, we need to divide it out before linearizing. + color.rgb /= vec3(color.a); + color = linearize(color); + color.rgb *= vec3(color.a); + + out_FragColor = color; + return; + } + } +} diff --git a/src/renderer/shaders/glsl/cell_text.v.glsl b/src/renderer/shaders/glsl/cell_text.v.glsl new file mode 100644 index 000000000..76ede1082 --- /dev/null +++ b/src/renderer/shaders/glsl/cell_text.v.glsl @@ -0,0 +1,162 @@ +#include "common.glsl" + +// The position of the glyph in the texture (x, y) +layout(location = 0) in uvec2 glyph_pos; + +// The size of the glyph in the texture (w, h) +layout(location = 1) in uvec2 glyph_size; + +// The left and top bearings for the glyph (x, y) +layout(location = 2) in ivec2 bearings; + +// The grid coordinates (x, y) where x < columns and y < rows +layout(location = 3) in uvec2 grid_pos; + +// The color of the rendered text glyph. +layout(location = 4) in uvec4 color; + +// The mode for this cell. +layout(location = 5) in uint mode; + +// The width to constrain the glyph to, in cells, or 0 for no constraint. +layout(location = 6) in uint constraint_width; + +// These are the possible modes that "mode" can be set to. This is +// used to multiplex multiple render modes into a single shader. +const uint MODE_TEXT = 1u; +const uint MODE_TEXT_CONSTRAINED = 2u; +const uint MODE_TEXT_COLOR = 3u; +const uint MODE_TEXT_CURSOR = 4u; +const uint MODE_TEXT_POWERLINE = 5u; + +out CellTextVertexOut { + flat uint mode; + flat vec4 color; + flat vec4 bg_color; + vec2 tex_coord; +} out_data; + +layout(binding = 1, std430) readonly buffer bg_cells { + uint bg_colors[]; +}; + +void main() { + uvec2 grid_size = unpack2u16(grid_size_packed_2u16); + uvec2 cursor_pos = unpack2u16(cursor_pos_packed_2u16); + bool cursor_wide = (bools & CURSOR_WIDE) != 0; + bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0; + + // Convert the grid x, y into world space x, y by accounting for cell size + vec2 cell_pos = cell_size * vec2(grid_pos); + + int vid = gl_VertexID; + + // We use a triangle strip with 4 vertices to render quads, + // so we determine which corner of the cell this vertex is in + // based on the vertex ID. + // + // 0 --> 1 + // | .'| + // | / | + // | L | + // 2 --> 3 + // + // 0 = top-left (0, 0) + // 1 = top-right (1, 0) + // 2 = bot-left (0, 1) + // 3 = bot-right (1, 1) + vec2 corner; + corner.x = float(vid == 1 || vid == 3); + corner.y = float(vid == 2 || vid == 3); + + out_data.mode = mode; + + // === Grid Cell === + // +X + // 0,0--...-> + // | + // . offset.x = bearings.x + // +Y. .|. + // . | | + // | cell_pos -> +-------+ _. + // v ._| |_. _|- offset.y = cell_size.y - bearings.y + // | | .###. | | + // | | #...# | | + // glyph_size.y -+ | ##### | | + // | | #.... | +- bearings.y + // |_| .#### | | + // | |_| + // +-------+ + // |_._| + // | + // glyph_size.x + // + // In order to get the top left of the glyph, we compute an offset based on + // the bearings. The Y bearing is the distance from the bottom of the cell + // to the top of the glyph, so we subtract it from the cell height to get + // the y offset. The X bearing is the distance from the left of the cell + // to the left of the glyph, so it works as the x offset directly. + + vec2 size = vec2(glyph_size); + vec2 offset = vec2(bearings); + + offset.y = cell_size.y - offset.y; + + // If we're constrained then we need to scale the glyph. + if (mode == MODE_TEXT_CONSTRAINED) { + float max_width = cell_size.x * constraint_width; + // If this glyph is wider than the constraint width, + // fit it to the width and remove its horizontal offset. + if (size.x > max_width) { + float new_y = size.y * (max_width / size.x); + offset.y += (size.y - new_y) / 2.0; + offset.x = 0.0; + size.y = new_y; + size.x = max_width; + } else if (max_width - size.x > offset.x) { + // However, if it does fit in the constraint width, make + // sure the offset is small enough to not push it over the + // right edge of the constraint width. + offset.x = max_width - size.x; + } + } + + // Calculate the final position of the cell which uses our glyph size + // and glyph offset to create the correct bounding box for the glyph. + cell_pos = cell_pos + size * corner + offset; + gl_Position = projection_matrix * vec4(cell_pos.x, cell_pos.y, 0.0f, 1.0f); + + // Calculate the texture coordinate in pixels. This is NOT normalized + // (between 0.0 and 1.0), and does not need to be, since the texture will + // be sampled with pixel coordinate mode. + out_data.tex_coord = vec2(glyph_pos) + vec2(glyph_size) * corner; + + // Get our color. We always fetch a linearized version to + // make it easier to handle minimum contrast calculations. + out_data.color = load_color(color, true); + // Get the BG color + out_data.bg_color = load_color( + unpack4u8(bg_colors[grid_pos.y * grid_size.x + grid_pos.x]), + true + ); + + // If we have a minimum contrast, we need to check if we need to + // change the color of the text to ensure it has enough contrast + // with the background. + // We only apply this adjustment to "normal" text with MODE_TEXT, + // since we want color glyphs to appear in their original color + // and Powerline glyphs to be unaffected (else parts of the line would + // have different colors as some parts are displayed via background colors). + if (min_contrast > 1.0f && mode == MODE_TEXT) { + // Ensure our minimum contrast + out_data.color = contrasted_color(min_contrast, out_data.color, out_data.bg_color); + } + + // Check if current position is under cursor (including wide cursor) + bool is_cursor_pos = ((grid_pos.x == cursor_pos.x) || (cursor_wide && (grid_pos.x == (cursor_pos.x + 1)))) && (grid_pos.y == cursor_pos.y); + + // If this cell is the cursor cell, then we need to change the color. + if (mode != MODE_TEXT_CURSOR && is_cursor_pos) { + out_data.color = load_color(unpack4u8(cursor_color_packed_4u8), use_linear_blending); + } +} diff --git a/src/renderer/shaders/glsl/common.glsl b/src/renderer/shaders/glsl/common.glsl new file mode 100644 index 000000000..0450d0c06 --- /dev/null +++ b/src/renderer/shaders/glsl/common.glsl @@ -0,0 +1,155 @@ +#version 430 core + +// These are common definitions to be shared across shaders, the first +// line of any shader that needs these should be `#include "common.glsl"`. +// +// Included in this file are: +// - The interface block for the global uniforms. +// - Functions for unpacking values. +// - Functions for working with colors. + +//----------------------------------------------------------------------------// +// Global Uniforms +//----------------------------------------------------------------------------// +layout(binding = 1, std140) uniform Globals { + uniform mat4 projection_matrix; + uniform vec2 cell_size; + uniform uint grid_size_packed_2u16; + uniform vec4 grid_padding; + uniform uint padding_extend; + uniform float min_contrast; + uniform uint cursor_pos_packed_2u16; + uniform uint cursor_color_packed_4u8; + uniform uint bg_color_packed_4u8; + uniform uint bools; +}; + +// Bools +const uint CURSOR_WIDE = 1u; +const uint USE_DISPLAY_P3 = 2u; +const uint USE_LINEAR_BLENDING = 4u; +const uint USE_LINEAR_CORRECTION = 8u; + +// Padding extend enum +const uint EXTEND_LEFT = 1u; +const uint EXTEND_RIGHT = 2u; +const uint EXTEND_UP = 4u; +const uint EXTEND_DOWN = 8u; + +//----------------------------------------------------------------------------// +// Functions for Unpacking Values +//----------------------------------------------------------------------------// +// NOTE: These unpack functions assume little-endian. +// If this ever becomes a problem... oh dear! + +uvec4 unpack4u8(uint packed_value) { + return uvec4( + uint(packed_value >> 0) & uint(0xFF), + uint(packed_value >> 8) & uint(0xFF), + uint(packed_value >> 16) & uint(0xFF), + uint(packed_value >> 24) & uint(0xFF) + ); +} + +uvec2 unpack2u16(uint packed_value) { + return uvec2( + uint(packed_value >> 0) & uint(0xFFFF), + uint(packed_value >> 16) & uint(0xFFFF) + ); +} + +ivec2 unpack2i16(int packed_value) { + return ivec2( + (packed_value << 16) >> 16, + (packed_value << 0) >> 16 + ); +} + +//----------------------------------------------------------------------------// +// Color Functions +//----------------------------------------------------------------------------// + +// Compute the luminance of the provided color. +// +// Takes colors in linear RGB space. If your colors are gamma +// encoded, linearize them before using them with this function. +float luminance(vec3 color) { + return dot(color, vec3(0.2126f, 0.7152f, 0.0722f)); +} + +// https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef +// +// Takes colors in linear RGB space. If your colors are gamma +// encoded, linearize them before using them with this function. +float contrast_ratio(vec3 color1, vec3 color2) { + float luminance1 = luminance(color1) + 0.05; + float luminance2 = luminance(color2) + 0.05; + return max(luminance1, luminance2) / min(luminance1, luminance2); +} + +// Return the fg if the contrast ratio is greater than min, otherwise +// return a color that satisfies the contrast ratio. Currently, the color +// is always white or black, whichever has the highest contrast ratio. +// +// Takes colors in linear RGB space. If your colors are gamma +// encoded, linearize them before using them with this function. +vec4 contrasted_color(float min_ratio, vec4 fg, vec4 bg) { + float ratio = contrast_ratio(fg.rgb, bg.rgb); + if (ratio < min_ratio) { + float white_ratio = contrast_ratio(vec3(1.0, 1.0, 1.0), bg.rgb); + float black_ratio = contrast_ratio(vec3(0.0, 0.0, 0.0), bg.rgb); + if (white_ratio > black_ratio) { + return vec4(1.0); + } else { + return vec4(0.0); + } + } + + return fg; +} + +// Converts a color from sRGB gamma encoding to linear. +vec4 linearize(vec4 srgb) { + bvec3 cutoff = lessThanEqual(srgb.rgb, vec3(0.04045)); + vec3 higher = pow((srgb.rgb + vec3(0.055)) / vec3(1.055), vec3(2.4)); + vec3 lower = srgb.rgb / vec3(12.92); + + return vec4(mix(higher, lower, cutoff), srgb.a); +} +float linearize(float v) { + return v <= 0.04045 ? v / 12.92 : pow((v + 0.055) / 1.055, 2.4); +} + +// Converts a color from linear to sRGB gamma encoding. +vec4 unlinearize(vec4 linear) { + bvec3 cutoff = lessThanEqual(linear.rgb, vec3(0.0031308)); + vec3 higher = pow(linear.rgb, vec3(1.0 / 2.4)) * vec3(1.055) - vec3(0.055); + vec3 lower = linear.rgb * vec3(12.92); + + return vec4(mix(higher, lower, cutoff), linear.a); +} +float unlinearize(float v) { + return v <= 0.0031308 ? v * 12.92 : pow(v, 1.0 / 2.4) * 1.055 - 0.055; +} + +// Load a 4 byte RGBA non-premultiplied color and linearize +// and convert it as necessary depending on the provided info. +// +// `linear` controls whether the returned color is linear or gamma encoded. +vec4 load_color( + uvec4 in_color, + bool linear +) { + // 0 .. 255 -> 0.0 .. 1.0 + vec4 color = vec4(in_color) / vec4(255.0f); + + // Linearize if necessary. + if (linear) color = linearize(color); + + // Premultiply our color by its alpha. + color.rgb *= color.a; + + return color; +} + +//----------------------------------------------------------------------------// diff --git a/src/renderer/shaders/glsl/full_screen.v.glsl b/src/renderer/shaders/glsl/full_screen.v.glsl new file mode 100644 index 000000000..b89cedfa5 --- /dev/null +++ b/src/renderer/shaders/glsl/full_screen.v.glsl @@ -0,0 +1,24 @@ +#version 330 core + +void main() { + vec4 position; + position.x = (gl_VertexID == 2) ? 3.0 : -1.0; + position.y = (gl_VertexID == 0) ? -3.0 : 1.0; + position.z = 1.0; + position.w = 1.0; + + // Single triangle is clipped to viewport. + // + // X <- vid == 0: (-1, -3) + // |\ + // | \ + // | \ + // |###\ + // |#+# \ `+` is (0, 0). `#`s are viewport area. + // |### \ + // X------X <- vid == 2: (3, 1) + // ^ + // vid == 1: (-1, 1) + + gl_Position = position; +} diff --git a/src/renderer/shaders/glsl/image.f.glsl b/src/renderer/shaders/glsl/image.f.glsl new file mode 100644 index 000000000..cd93cf666 --- /dev/null +++ b/src/renderer/shaders/glsl/image.f.glsl @@ -0,0 +1,21 @@ +#include "common.glsl" + +layout(binding = 0) uniform sampler2DRect image; + +in vec2 tex_coord; + +layout(location = 0) out vec4 out_FragColor; + +void main() { + bool use_linear_blending = (bools & USE_LINEAR_BLENDING) != 0; + + vec4 rgba = texture(image, tex_coord); + + if (!use_linear_blending) { + rgba = unlinearize(rgba); + } + + rgba.rgb *= vec3(rgba.a); + + out_FragColor = rgba; +} diff --git a/src/renderer/shaders/glsl/image.v.glsl b/src/renderer/shaders/glsl/image.v.glsl new file mode 100644 index 000000000..55b12ed68 --- /dev/null +++ b/src/renderer/shaders/glsl/image.v.glsl @@ -0,0 +1,46 @@ +#include "common.glsl" + +layout(binding = 0) uniform sampler2DRect image; + +layout(location = 0) in vec2 grid_pos; +layout(location = 1) in vec2 cell_offset; +layout(location = 2) in vec4 source_rect; +layout(location = 3) in vec2 dest_size; + +out vec2 tex_coord; + +void main() { + int vid = gl_VertexID; + + // We use a triangle strip with 4 vertices to render quads, + // so we determine which corner of the cell this vertex is in + // based on the vertex ID. + // + // 0 --> 1 + // | .'| + // | / | + // | L | + // 2 --> 3 + // + // 0 = top-left (0, 0) + // 1 = top-right (1, 0) + // 2 = bot-left (0, 1) + // 3 = bot-right (1, 1) + vec2 corner; + corner.x = float(vid == 1 || vid == 3); + corner.y = float(vid == 2 || vid == 3); + + // The texture coordinates start at our source x/y + // and add the width/height depending on the corner. + // + // We don't need to normalize because we use pixel addressing for our sampler. + tex_coord = source_rect.xy; + tex_coord += source_rect.zw * corner; + + // The position of our image starts at the top-left of the grid cell and + // adds the source rect width/height components. + vec2 image_pos = (cell_size * grid_pos) + cell_offset; + image_pos += dest_size * corner; + + gl_Position = projection_matrix * vec4(image_pos.xy, 1.0, 1.0); +} diff --git a/src/renderer/shaders/image.f.glsl b/src/renderer/shaders/image.f.glsl deleted file mode 100644 index e4aa9ef8e..000000000 --- a/src/renderer/shaders/image.f.glsl +++ /dev/null @@ -1,29 +0,0 @@ -#version 330 core - -in vec2 tex_coord; - -layout(location = 0) out vec4 out_FragColor; - -uniform sampler2D image; - -// Converts a color from linear to sRGB gamma encoding. -vec4 unlinearize(vec4 linear) { - bvec3 cutoff = lessThan(linear.rgb, vec3(0.0031308)); - vec3 higher = pow(linear.rgb, vec3(1.0/2.4)) * vec3(1.055) - vec3(0.055); - vec3 lower = linear.rgb * vec3(12.92); - - return vec4(mix(higher, lower, cutoff), linear.a); -} - -void main() { - vec4 color = texture(image, tex_coord); - - // Our texture is stored with an sRGB internal format, - // which means that the values are linearized when we - // sample the texture, but for now we actually want to - // output the color with gamma compression, so we do - // that. - color = unlinearize(color); - - out_FragColor = vec4(color.rgb * color.a, color.a); -} diff --git a/src/renderer/shaders/image.v.glsl b/src/renderer/shaders/image.v.glsl deleted file mode 100644 index e3d07ca9e..000000000 --- a/src/renderer/shaders/image.v.glsl +++ /dev/null @@ -1,44 +0,0 @@ -#version 330 core - -layout (location = 0) in vec2 grid_pos; -layout (location = 1) in vec2 cell_offset; -layout (location = 2) in vec4 source_rect; -layout (location = 3) in vec2 dest_size; - -out vec2 tex_coord; - -uniform sampler2D image; -uniform vec2 cell_size; -uniform mat4 projection; - -void main() { - // The size of the image in pixels - vec2 image_size = textureSize(image, 0); - - // Turn the cell position into a vertex point depending on the - // gl_VertexID. Since we use instanced drawing, we have 4 vertices - // for each corner of the cell. We can use gl_VertexID to determine - // which one we're looking at. Using this, we can use 1 or 0 to keep - // or discard the value for the vertex. - // - // 0 = top-right - // 1 = bot-right - // 2 = bot-left - // 3 = top-left - vec2 position; - position.x = (gl_VertexID == 0 || gl_VertexID == 1) ? 1. : 0.; - position.y = (gl_VertexID == 0 || gl_VertexID == 3) ? 0. : 1.; - - // The texture coordinates start at our source x/y, then add the width/height - // as enabled by our instance id, then normalize to [0, 1] - tex_coord = source_rect.xy; - tex_coord += source_rect.zw * position; - tex_coord /= image_size; - - // The position of our image starts at the top-left of the grid cell and - // adds the source rect width/height components. - vec2 image_pos = (cell_size * grid_pos) + cell_offset; - image_pos += dest_size * position; - - gl_Position = projection * vec4(image_pos.xy, 0, 1.0); -} diff --git a/src/renderer/shaders/shadertoy_prefix.glsl b/src/renderer/shaders/shadertoy_prefix.glsl index a1a220bd4..5bc25bc03 100644 --- a/src/renderer/shaders/shadertoy_prefix.glsl +++ b/src/renderer/shaders/shadertoy_prefix.glsl @@ -1,24 +1,24 @@ #version 430 core -layout(binding = 0) uniform Globals { - uniform vec3 iResolution; - uniform float iTime; - uniform float iTimeDelta; - uniform float iFrameRate; - uniform int iFrame; - uniform float iChannelTime[4]; - uniform vec3 iChannelResolution[4]; - uniform vec4 iMouse; - uniform vec4 iDate; - uniform float iSampleRate; +layout(binding = 1, std140) uniform Globals { + uniform vec3 iResolution; + uniform float iTime; + uniform float iTimeDelta; + uniform float iFrameRate; + uniform int iFrame; + uniform float iChannelTime[4]; + uniform vec3 iChannelResolution[4]; + uniform vec4 iMouse; + uniform vec4 iDate; + uniform float iSampleRate; }; -layout(binding = 0) uniform sampler2D iChannel0; +layout(binding = 0) uniform sampler2D iChannel0; // These are unused currently by Ghostty: -// layout(binding = 1) uniform sampler2D iChannel1; -// layout(binding = 2) uniform sampler2D iChannel2; -// layout(binding = 3) uniform sampler2D iChannel3; +// layout(binding = 1) uniform sampler2D iChannel1; +// layout(binding = 2) uniform sampler2D iChannel2; +// layout(binding = 3) uniform sampler2D iChannel3; layout(location = 0) in vec4 gl_FragCoord; layout(location = 0) out vec4 _fragColor; diff --git a/src/renderer/shadertoy.zig b/src/renderer/shadertoy.zig index 45d86cbfe..68171a23e 100644 --- a/src/renderer/shadertoy.zig +++ b/src/renderer/shadertoy.zig @@ -205,18 +205,25 @@ pub const SpirvLog = struct { /// Convert SPIR-V binary to MSL. pub fn mslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { - return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, null); + const c = spvcross.c; + return try spvCross(alloc, spvcross.c.SPVC_BACKEND_MSL, spv, (struct { + fn setOptions(options: c.spvc_compiler_options) error{SpvcFailed}!void { + // We enable decoration binding, because we need this + // to properly locate the uniform block to index 1. + if (c.spvc_compiler_options_set_bool( + options, + c.SPVC_COMPILER_OPTION_MSL_ENABLE_DECORATION_BINDING, + c.SPVC_TRUE, + ) != c.SPVC_SUCCESS) { + return error.SpvcFailed; + } + } + }).setOptions); } -/// Convert SPIR-V binary to GLSL.. +/// Convert SPIR-V binary to GLSL. pub fn glslFromSpv(alloc: Allocator, spv: []const u8) ![:0]const u8 { - // Our minimum version for shadertoy shaders is OpenGL 4.2 because - // Spirv-Cross generates binding locations for uniforms which is - // only supported in OpenGL 4.2 and above. - // - // If we can figure out a way to NOT do this then we can lower this - // version. - const GLSL_VERSION = 420; + const GLSL_VERSION = 430; const c = spvcross.c; return try spvCross(alloc, c.SPVC_BACKEND_GLSL, spv, (struct { From ac2eef9aeb319b0a1532607c215925bfc198e8c0 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 17 Jun 2025 16:34:05 -0600 Subject: [PATCH 504/642] renderer: disable multi-buffering for OpenGL Frames are sequential for OpenGL since the completion handler always calls `glFinish`, so the extra buffers do nothing but waste memory. --- src/renderer/OpenGL.zig | 4 ++++ src/renderer/generic.zig | 11 +++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index c2f8bd652..59c2f41b6 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -29,6 +29,10 @@ pub const imagepkg = @import("opengl/image.zig"); pub const custom_shader_target: shadertoy.Target = .glsl; +/// Because OpenGL's frame completion is always +/// sync, we have no need for multi-buffering. +pub const swap_chain_count = 1; + const log = std.log.scoped(.opengl); /// We require at least OpenGL 4.3 diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 359f5f1b3..52f789c6c 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -204,8 +204,15 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If this is one then we don't do any double+ buffering at all. // This is comptime because there isn't a good reason to change // this at runtime and there is a lot of complexity to support it. - // For comptime, this is useful for debugging. - const buf_count = 3; + const buf_count = count: { + if (@hasDecl(GraphicsAPI, "swap_chain_count")) { + break :count GraphicsAPI.swap_chain_count; + } + + // Default to triple buffering if + // graphics API has no preference. + break :count 3; + }; /// `buf_count` structs that can hold the /// data needed by the GPU to draw a frame. From 6dc5ae7a00999d0366a70cd389421b5a349e0462 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 17 Jun 2025 17:31:22 -0600 Subject: [PATCH 505/642] format (remove empty lines) --- src/renderer/opengl/Pipeline.zig | 1 - src/renderer/opengl/Texture.zig | 1 - 2 files changed, 2 deletions(-) diff --git a/src/renderer/opengl/Pipeline.zig b/src/renderer/opengl/Pipeline.zig index 127d689f5..501e6124c 100644 --- a/src/renderer/opengl/Pipeline.zig +++ b/src/renderer/opengl/Pipeline.zig @@ -31,7 +31,6 @@ pub const Options = struct { per_vertex, per_instance, }; - }; program: gl.Program, diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index 84a1ae9bc..d5ec816a6 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -96,4 +96,3 @@ pub fn replaceRegion( data.ptr, ); } - From ea1e507af712b5e843bf43ac08026f4ff4dc0b64 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Tue, 17 Jun 2025 17:32:57 -0600 Subject: [PATCH 506/642] unwrap unnecessary @"" identifiers --- pkg/macos/video/pixel_format.zig | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pkg/macos/video/pixel_format.zig b/pkg/macos/video/pixel_format.zig index 30f11881e..78091daa3 100644 --- a/pkg/macos/video/pixel_format.zig +++ b/pkg/macos/video/pixel_format.zig @@ -96,33 +96,33 @@ pub const PixelFormat = enum(c_int) { /// Component Y'CbCr 8-bit 4:2:2, full range, ordered Y'0 Cb Y'1 Cr @"422YpCbCr8FullRange" = c.kCVPixelFormatType_422YpCbCr8FullRange, /// 8 bit one component, black is zero - @"OneComponent8" = c.kCVPixelFormatType_OneComponent8, + OneComponent8 = c.kCVPixelFormatType_OneComponent8, /// 8 bit two component, black is zero - @"TwoComponent8" = c.kCVPixelFormatType_TwoComponent8, + TwoComponent8 = c.kCVPixelFormatType_TwoComponent8, /// little-endian RGB101010, 2 MSB are ignored, wide-gamut (384-895) @"30RGBLEPackedWideGamut" = c.kCVPixelFormatType_30RGBLEPackedWideGamut, /// little-endian ARGB2101010 full-range ARGB - @"ARGB2101010LEPacked" = c.kCVPixelFormatType_ARGB2101010LEPacked, + ARGB2101010LEPacked = c.kCVPixelFormatType_ARGB2101010LEPacked, /// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha) @"40ARGBLEWideGamut" = c.kCVPixelFormatType_40ARGBLEWideGamut, /// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha). Alpha premultiplied @"40ARGBLEWideGamutPremultiplied" = c.kCVPixelFormatType_40ARGBLEWideGamutPremultiplied, /// 10 bit little-endian one component, stored as 10 MSBs of 16 bits, black is zero - @"OneComponent10" = c.kCVPixelFormatType_OneComponent10, + OneComponent10 = c.kCVPixelFormatType_OneComponent10, /// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero - @"OneComponent12" = c.kCVPixelFormatType_OneComponent12, + OneComponent12 = c.kCVPixelFormatType_OneComponent12, /// 16 bit little-endian one component, black is zero - @"OneComponent16" = c.kCVPixelFormatType_OneComponent16, + OneComponent16 = c.kCVPixelFormatType_OneComponent16, /// 16 bit little-endian two component, black is zero - @"TwoComponent16" = c.kCVPixelFormatType_TwoComponent16, + TwoComponent16 = c.kCVPixelFormatType_TwoComponent16, /// 16 bit one component IEEE half-precision float, 16-bit little-endian samples - @"OneComponent16Half" = c.kCVPixelFormatType_OneComponent16Half, + OneComponent16Half = c.kCVPixelFormatType_OneComponent16Half, /// 32 bit one component IEEE float, 32-bit little-endian samples - @"OneComponent32Float" = c.kCVPixelFormatType_OneComponent32Float, + OneComponent32Float = c.kCVPixelFormatType_OneComponent32Float, /// 16 bit two component IEEE half-precision float, 16-bit little-endian samples - @"TwoComponent16Half" = c.kCVPixelFormatType_TwoComponent16Half, + TwoComponent16Half = c.kCVPixelFormatType_TwoComponent16Half, /// 32 bit two component IEEE float, 32-bit little-endian samples - @"TwoComponent32Float" = c.kCVPixelFormatType_TwoComponent32Float, + TwoComponent32Float = c.kCVPixelFormatType_TwoComponent32Float, /// 64 bit RGBA IEEE half-precision float, 16-bit little-endian samples @"64RGBAHalf" = c.kCVPixelFormatType_64RGBAHalf, /// 128 bit RGBA IEEE float, 32-bit little-endian samples @@ -136,13 +136,13 @@ pub const PixelFormat = enum(c_int) { /// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G B G B... alternating with R G R G... @"14Bayer_GBRG" = c.kCVPixelFormatType_14Bayer_GBRG, /// IEEE754-2008 binary16 (half float), describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) ) - @"DisparityFloat16" = c.kCVPixelFormatType_DisparityFloat16, + DisparityFloat16 = c.kCVPixelFormatType_DisparityFloat16, /// IEEE754-2008 binary32 float, describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) ) - @"DisparityFloat32" = c.kCVPixelFormatType_DisparityFloat32, + DisparityFloat32 = c.kCVPixelFormatType_DisparityFloat32, /// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters - @"DepthFloat16" = c.kCVPixelFormatType_DepthFloat16, + DepthFloat16 = c.kCVPixelFormatType_DepthFloat16, /// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters - @"DepthFloat32" = c.kCVPixelFormatType_DepthFloat32, + DepthFloat32 = c.kCVPixelFormatType_DepthFloat32, /// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960]) @"420YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange, /// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960]) From 541bb0d4d9ee441b9655bd8edd697a3168587207 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 18 Jun 2025 16:54:50 -0600 Subject: [PATCH 507/642] fix window cross-compilation --- src/renderer/OpenGL.zig | 12 +++++++++++- src/renderer/opengl/shaders.zig | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 59c2f41b6..fe266d2ef 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -70,6 +70,16 @@ pub fn glfwWindowHints(config: *const configpkg.Config) glfw.Window.Hints { }; } +/// 32-bit windows cross-compilation breaks with `.c` for some reason, so... +const gl_debug_proc_callconv = + @typeInfo( + @typeInfo( + @typeInfo( + gl.c.GLDEBUGPROC, + ).optional.child, + ).pointer.child, + ).@"fn".calling_convention; + fn glDebugMessageCallback( src: gl.c.GLenum, typ: gl.c.GLenum, @@ -78,7 +88,7 @@ fn glDebugMessageCallback( len: gl.c.GLsizei, msg: [*c]const gl.c.GLchar, user_param: ?*const anyopaque, -) callconv(.c) void { +) callconv(gl_debug_proc_callconv) void { _ = user_param; const src_str: []const u8 = switch (src) { diff --git a/src/renderer/opengl/shaders.zig b/src/renderer/opengl/shaders.zig index 253ae8719..e509b723a 100644 --- a/src/renderer/opengl/shaders.zig +++ b/src/renderer/opengl/shaders.zig @@ -295,7 +295,7 @@ fn processIncludes(contents: [:0]const u8, basedir: []const u8) [:0]const u8 { "{s}{s}{s}", .{ contents[0..i], - @embedFile(basedir ++ .{std.fs.path.sep} ++ contents[start..end]), + @embedFile(basedir ++ "/" ++ contents[start..end]), processIncludes(contents[end + 1 ..], basedir), }, ); From e8460e80b206ebadd82c15562e46c86396da6564 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Wed, 18 Jun 2025 17:01:14 -0600 Subject: [PATCH 508/642] docs: update info about runtime change of `custom-shader` Also removes incorrect information about OpenGL requirement, since the minimum required OpenGL is now unconditionally 4.3 --- src/config/Config.zig | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/config/Config.zig b/src/config/Config.zig index f7a197184..bb094bf4d 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1967,9 +1967,6 @@ keybind: Keybinds = .{}, /// causing the window to be completely black. If this happens, you can /// unset this configuration to disable the shader. /// -/// On Linux, this requires OpenGL 4.2. Ghostty typically only requires -/// OpenGL 3.3, but custom shaders push that requirement up to 4.2. -/// /// The shader API is identical to the Shadertoy API: you specify a `mainImage` /// function and the available uniforms match Shadertoy. The iChannel0 uniform /// is a texture containing the rendered terminal screen. @@ -1983,8 +1980,7 @@ keybind: Keybinds = .{}, /// This can be repeated multiple times to load multiple shaders. The shaders /// will be run in the order they are specified. /// -/// Changing this value at runtime and reloading the configuration will only -/// affect new windows, tabs, and splits. +/// This can be changed at runtime and will affect all open terminals. @"custom-shader": RepeatablePath = .{}, /// If `true` (default), the focused terminal surface will run an animation @@ -2002,8 +1998,7 @@ keybind: Keybinds = .{}, /// will use more CPU per terminal surface and can become quite expensive /// depending on the shader and your terminal usage. /// -/// This value can be changed at runtime and will affect all currently -/// open terminals. +/// This can be changed at runtime and will affect all open terminals. @"custom-shader-animation": CustomShaderAnimation = .true, /// Bell features to enable if bell support is available in your runtime. Not From 8b23e73d203d975811e9a97f33092b9221c86ba5 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:28:31 -0600 Subject: [PATCH 509/642] metal: retain IOSurfaceLayer ourselves instead of relying on the view If this was Swift code, we'd be using a strong reference, which would retain the layer for us and release it when the object is deallocated, but this is Zig land so we have to do that manually. NOTE: We don't *have* to do this, but it fits much better with Zig idiom and hopefully avoids potential future footguns. We should do this to any autoreleased objects that we persist a reference to in a Zig struct. --- src/renderer/Metal.zig | 4 +--- src/renderer/metal/IOSurfaceLayer.zig | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 766cbefa5..94c087f6c 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -165,9 +165,7 @@ pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { pub fn deinit(self: *Metal) void { self.queue.release(); self.device.release(); - - // NOTE: We don't release the layer here because that should be taken - // care of automatically when the hosting view is destroyed. + self.layer.release(); } pub fn loopEnter(self: *Metal) void { diff --git a/src/renderer/metal/IOSurfaceLayer.zig b/src/renderer/metal/IOSurfaceLayer.zig index 4c51a55c2..9212bd5e1 100644 --- a/src/renderer/metal/IOSurfaceLayer.zig +++ b/src/renderer/metal/IOSurfaceLayer.zig @@ -21,11 +21,14 @@ var Subclass: ?objc.Class = null; layer: objc.Object, pub fn init() !IOSurfaceLayer { + // The layer returned by `[CALayer layer]` is autoreleased, which means + // that at the end of the current autorelease pool it will be deallocated + // if it isn't retained, so we retain it here manually an extra time. const layer = (try getSubclass()).msgSend( objc.Object, objc.sel("layer"), .{}, - ); + ).retain(); errdefer layer.release(); // The layer gravity is set to top-left so that the contents aren't From b9e35c59704e9be16d11595dba372d574d2fe709 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:48:30 -0600 Subject: [PATCH 510/642] renderer: uncomment resize message handling We need this to get info about the padding, even if we do derive the grid and screen size separately. In the future this should possibly be changed to a message that only sends the padding info and nothing else. --- src/renderer/Thread.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index 03ca7b5e1..c4036415b 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -452,7 +452,7 @@ fn drainMailbox(self: *Thread) !void { self.renderer.markDirty(); }, - .resize => {}, //|v| try self.renderer.setScreenSize(v), + .resize => |v| self.renderer.setScreenSize(v), .change_config => |config| { defer config.alloc.destroy(config.thread); From dccbec2283759e126adc387bffd8469cafd061fa Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:51:55 -0600 Subject: [PATCH 511/642] style(renderer): capture generic consts as decls in returned struct Out of an abundance of caution, since there have been issues in the past relating to consts outside of the returned struct. --- src/renderer/generic.zig | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 52f789c6c..0308b4c6d 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -68,24 +68,24 @@ const log = std.log.scoped(.generic_renderer); /// [ Texture ] - An abstraction over a GPU texture. /// pub fn Renderer(comptime GraphicsAPI: type) type { - const Target = GraphicsAPI.Target; - const Buffer = GraphicsAPI.Buffer; - const Texture = GraphicsAPI.Texture; - const RenderPass = GraphicsAPI.RenderPass; - const shaderpkg = GraphicsAPI.shaders; - - const cellpkg = GraphicsAPI.cellpkg; - const imagepkg = GraphicsAPI.imagepkg; - const Image = imagepkg.Image; - const ImageMap = imagepkg.ImageMap; - - const Shaders = shaderpkg.Shaders; - - const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); - return struct { const Self = @This(); + const Target = GraphicsAPI.Target; + const Buffer = GraphicsAPI.Buffer; + const Texture = GraphicsAPI.Texture; + const RenderPass = GraphicsAPI.RenderPass; + const shaderpkg = GraphicsAPI.shaders; + + const cellpkg = GraphicsAPI.cellpkg; + const imagepkg = GraphicsAPI.imagepkg; + const Image = imagepkg.Image; + const ImageMap = imagepkg.ImageMap; + + const Shaders = shaderpkg.Shaders; + + const ImagePlacementList = std.ArrayListUnmanaged(imagepkg.Placement); + /// Allocator that can be used alloc: std.mem.Allocator, From 6b7d751007291e9e082d8486171f97749b04b17c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:53:30 -0600 Subject: [PATCH 512/642] renderer: make GraphicsAPI.swap_chain_count required --- src/renderer/Metal.zig | 3 +++ src/renderer/generic.zig | 10 +--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 94c087f6c..a69d2e3d3 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -34,6 +34,9 @@ pub const imagepkg = @import("metal/image.zig"); pub const custom_shader_target: shadertoy.Target = .msl; +/// Triple buffering. +pub const swap_chain_count = 3; + const log = std.log.scoped(.metal); // Get native API access on certain platforms so we can do more customization. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 0308b4c6d..274f32585 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -204,15 +204,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // If this is one then we don't do any double+ buffering at all. // This is comptime because there isn't a good reason to change // this at runtime and there is a lot of complexity to support it. - const buf_count = count: { - if (@hasDecl(GraphicsAPI, "swap_chain_count")) { - break :count GraphicsAPI.swap_chain_count; - } - - // Default to triple buffering if - // graphics API has no preference. - break :count 3; - }; + const buf_count = GraphicsAPI.swap_chain_count; /// `buf_count` structs that can hold the /// data needed by the GPU to draw a frame. From 2f10caec8f694ccf135d024264969cfc1b3be823 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 14:56:18 -0600 Subject: [PATCH 513/642] renderer: clarify why SwapChain.defunct is required --- src/renderer/generic.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 274f32585..2467bfb2e 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -217,6 +217,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Set to true when deinited, if you try to deinit a defunct /// swap chain it will just be ignored, to prevent double-free. + /// + /// This is required because of `displayUnrealized`, since it + /// `deinits` the swapchain, which leads to a double-free if + /// the renderer is deinited after that. defunct: bool = false, pub fn init(api: GraphicsAPI, custom_shaders: bool) !SwapChain { From 9d00018f8b48c5623ed9798f23d3f01f62779ed6 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:02:58 -0600 Subject: [PATCH 514/642] renderer: minimize initial size of GPU resources These will all be resized anyway on the first frame, so there's no point in preallocating sizes that will be too small. --- src/renderer/generic.zig | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 2467bfb2e..fce69316e 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -300,23 +300,30 @@ pub fn Renderer(comptime GraphicsAPI: type) type { var uniforms = try UniformBuffer.init(api.uniformBufferOptions(), 1); errdefer uniforms.deinit(); - // Create the buffers for our vertex data. The preallocation size - // is likely too small but our first frame update will resize it. - var cells = try CellTextBuffer.init(api.fgBufferOptions(), 10 * 10); + // Create GPU buffers for our cells. + // + // We start them off with a size of 1, which will of course be + // too small, but they will be resized as needed. This is a bit + // wasteful but since it's a one-time thing it's not really a + // huge concern. + var cells = try CellTextBuffer.init(api.fgBufferOptions(), 1); errdefer cells.deinit(); - var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 10 * 10); + var cells_bg = try CellBgBuffer.init(api.bgBufferOptions(), 1); errdefer cells_bg.deinit(); // Initialize our textures for our font atlas. + // + // As with the buffers above, we start these off as small + // as possible since they'll inevitably be resized anyway. const grayscale = try api.initAtlasTexture(&.{ .data = undefined, - .size = 8, + .size = 1, .format = .grayscale, }); errdefer grayscale.deinit(); const color = try api.initAtlasTexture(&.{ .data = undefined, - .size = 8, + .size = 1, .format = .rgba, }); errdefer color.deinit(); @@ -328,8 +335,8 @@ pub fn Renderer(comptime GraphicsAPI: type) type { null; errdefer if (custom_shader_state) |*state| state.deinit(); - // Initialize the target at 1x1 px, this is slightly - // wasteful but it's only done once so whatever. + // Initialize the target. Just as with the other resources, + // start it off as small as we can since it'll be resized. const target = try api.initTarget(1, 1); return .{ From ea7a91e2ba77da472de2d95a8aec1f1fb8b24ed1 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:16:31 -0600 Subject: [PATCH 515/642] style(renderer): explicit error sets --- pkg/opengl/Texture.zig | 13 +++++++------ src/renderer/Metal.zig | 7 +++++-- src/renderer/OpenGL.zig | 7 +++++-- src/renderer/metal/Texture.zig | 9 +++++++-- src/renderer/opengl/Texture.zig | 31 ++++++++++++++++++------------- 5 files changed, 42 insertions(+), 25 deletions(-) diff --git a/pkg/opengl/Texture.zig b/pkg/opengl/Texture.zig index 833a9bb4d..4a1d61433 100644 --- a/pkg/opengl/Texture.zig +++ b/pkg/opengl/Texture.zig @@ -7,15 +7,16 @@ const glad = @import("glad.zig"); id: c.GLuint, -pub fn active(index: c_uint) !void { +pub fn active(index: c_uint) errors.Error!void { glad.context.ActiveTexture.?(index + c.GL_TEXTURE0); try errors.getError(); } /// Create a single texture. -pub fn create() !Texture { +pub fn create() errors.Error!Texture { var id: c.GLuint = undefined; glad.context.GenTextures.?(1, &id); + try errors.getError(); return .{ .id = id }; } @@ -107,7 +108,7 @@ pub const Binding = struct { glad.context.GenerateMipmap.?(@intFromEnum(b.target)); } - pub fn parameter(b: Binding, name: Parameter, value: anytype) !void { + pub fn parameter(b: Binding, name: Parameter, value: anytype) errors.Error!void { switch (@TypeOf(value)) { c.GLint => glad.context.TexParameteri.?( @intFromEnum(b.target), @@ -129,7 +130,7 @@ pub const Binding = struct { format: Format, typ: DataType, data: ?*const anyopaque, - ) !void { + ) errors.Error!void { glad.context.TexImage2D.?( @intFromEnum(b.target), level, @@ -154,7 +155,7 @@ pub const Binding = struct { format: Format, typ: DataType, data: ?*const anyopaque, - ) !void { + ) errors.Error!void { glad.context.TexSubImage2D.?( @intFromEnum(b.target), level, @@ -178,7 +179,7 @@ pub const Binding = struct { y: c.GLint, width: c.GLsizei, height: c.GLsizei, - ) !void { + ) errors.Error!void { glad.context.CopyTexSubImage2D.?( @intFromEnum(b.target), level, diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index a69d2e3d3..21a10d45f 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -284,14 +284,17 @@ pub inline fn textureOptions(self: Metal) Texture.Options { } /// Initializes a Texture suitable for the provided font atlas. -pub fn initAtlasTexture(self: *const Metal, atlas: *const font.Atlas) !Texture { +pub fn initAtlasTexture( + self: *const Metal, + atlas: *const font.Atlas, +) Texture.Error!Texture { const pixel_format: mtl.MTLPixelFormat = switch (atlas.format) { .grayscale => .r8unorm, .rgba => .bgra8unorm, else => @panic("unsupported atlas format for Metal texture"), }; - return Texture.init( + return try Texture.init( .{ .device = self.device, .pixel_format = pixel_format, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index fe266d2ef..1d1b41f0e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -384,7 +384,10 @@ pub inline fn textureOptions(self: OpenGL) Texture.Options { } /// Initializes a Texture suitable for the provided font atlas. -pub fn initAtlasTexture(self: *const OpenGL, atlas: *const font.Atlas) !Texture { +pub fn initAtlasTexture( + self: *const OpenGL, + atlas: *const font.Atlas, +) Texture.Error!Texture { _ = self; const format: gl.Texture.Format, const internal_format: gl.Texture.InternalFormat = switch (atlas.format) { @@ -393,7 +396,7 @@ pub fn initAtlasTexture(self: *const OpenGL, atlas: *const font.Atlas) !Texture else => @panic("unsupported atlas format for OpenGL texture"), }; - return Texture.init( + return try Texture.init( .{ .format = format, .internal_format = internal_format, diff --git a/src/renderer/metal/Texture.zig b/src/renderer/metal/Texture.zig index 6e3ae78c7..32820f8fc 100644 --- a/src/renderer/metal/Texture.zig +++ b/src/renderer/metal/Texture.zig @@ -31,13 +31,18 @@ height: usize, /// Bytes per pixel for this texture. bpp: usize, +pub const Error = error{ + /// A Metal API call failed. + MetalFailed, +}; + /// Initialize a texture pub fn init( opts: Options, width: usize, height: usize, data: ?[]const u8, -) !Self { +) Error!Self { // Create our descriptor const desc = init: { const Class = objc.getClass("MTLTextureDescriptor").?; @@ -90,7 +95,7 @@ pub fn replaceRegion( width: usize, height: usize, data: []const u8, -) !void { +) error{}!void { self.texture.msgSend( void, objc.sel("replaceRegion:mipmapLevel:withBytes:bytesPerRow:"), diff --git a/src/renderer/opengl/Texture.zig b/src/renderer/opengl/Texture.zig index d5ec816a6..07123922f 100644 --- a/src/renderer/opengl/Texture.zig +++ b/src/renderer/opengl/Texture.zig @@ -31,23 +31,28 @@ format: gl.Texture.Format, /// Target for this texture. target: gl.Texture.Target, +pub const Error = error{ + /// An OpenGL API call failed. + OpenGLFailed, +}; + /// Initialize a texture pub fn init( opts: Options, width: usize, height: usize, data: ?[]const u8, -) !Self { - const tex = try gl.Texture.create(); +) Error!Self { + const tex = gl.Texture.create() catch return error.OpenGLFailed; errdefer tex.destroy(); { - const texbind = try tex.bind(opts.target); + const texbind = tex.bind(opts.target) catch return error.OpenGLFailed; defer texbind.unbind(); - try texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE); - try texbind.parameter(.MinFilter, gl.c.GL_LINEAR); - try texbind.parameter(.MagFilter, gl.c.GL_LINEAR); - try texbind.image2D( + texbind.parameter(.WrapS, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; + texbind.parameter(.WrapT, gl.c.GL_CLAMP_TO_EDGE) catch return error.OpenGLFailed; + texbind.parameter(.MinFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; + texbind.parameter(.MagFilter, gl.c.GL_LINEAR) catch return error.OpenGLFailed; + texbind.image2D( 0, opts.internal_format, @intCast(width), @@ -56,7 +61,7 @@ pub fn init( opts.format, .UnsignedByte, if (data) |d| @ptrCast(d.ptr) else null, - ); + ) catch return error.OpenGLFailed; } return .{ @@ -82,10 +87,10 @@ pub fn replaceRegion( width: usize, height: usize, data: []const u8, -) !void { - const texbind = try self.texture.bind(self.target); +) Error!void { + const texbind = self.texture.bind(self.target) catch return error.OpenGLFailed; defer texbind.unbind(); - try texbind.subImage2D( + texbind.subImage2D( 0, @intCast(x), @intCast(y), @@ -94,5 +99,5 @@ pub fn replaceRegion( self.format, .UnsignedByte, data.ptr, - ); + ) catch return error.OpenGLFailed; } From 3e7d64b5ce965070ca361d1f8d94cfb157faddad Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:45:43 -0600 Subject: [PATCH 516/642] style(renderer): explicit empty error set for OpenGL init --- src/renderer/OpenGL.zig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 1d1b41f0e..584d3cf9d 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -47,7 +47,10 @@ blending: configpkg.Config.AlphaBlending, /// The most recently presented target, in case we need to present it again. last_target: ?Target = null, -pub fn init(alloc: Allocator, opts: rendererpkg.Options) !OpenGL { +/// NOTE: This is an error{}!OpenGL instead of just OpenGL for parity with +/// Metal, since it needs to be fallible so does this, even though it +/// can't actually fail. +pub fn init(alloc: Allocator, opts: rendererpkg.Options) error{}!OpenGL { return .{ .alloc = alloc, .blending = opts.config.blending, From 8b9e6641f22dfefe917a80cc99542990e149cbbc Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:48:44 -0600 Subject: [PATCH 517/642] style(renderer): explicit result type In case of future breaking changes to `options` --- src/renderer/generic.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index fce69316e..3462b6fa4 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -633,7 +633,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { const font_critical: struct { metrics: font.Metrics, } = font_critical: { - const grid = options.font_grid; + const grid: *font.SharedGrid = options.font_grid; grid.lock.lockShared(); defer grid.lock.unlockShared(); break :font_critical .{ From a8021085587f0dbe8ad9299bbab90d98ef3ff134 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:49:53 -0600 Subject: [PATCH 518/642] renderer: remove unused surface parameter from updateFrame --- src/renderer/Thread.zig | 1 - src/renderer/generic.zig | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/renderer/Thread.zig b/src/renderer/Thread.zig index c4036415b..b8884f2fb 100644 --- a/src/renderer/Thread.zig +++ b/src/renderer/Thread.zig @@ -605,7 +605,6 @@ fn renderCallback( // Update our frame data t.renderer.updateFrame( - t.surface, t.state, t.flags.cursor_blink_visible, ) catch |err| diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 3462b6fa4..d7b4f4226 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1027,12 +1027,9 @@ pub fn Renderer(comptime GraphicsAPI: type) type { /// Update the frame data. pub fn updateFrame( self: *Self, - surface: *apprt.Surface, state: *renderer.State, cursor_blink_visible: bool, ) !void { - _ = surface; - // Data we extract out of the critical area. const Critical = struct { bg: terminal.color.RGB, From ab926fc842da4765148295b7a12f42cb28b86d25 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 15:51:48 -0600 Subject: [PATCH 519/642] naming(GraphicsAPI): repeat -> presentLastTarget --- src/renderer/Metal.zig | 2 +- src/renderer/OpenGL.zig | 2 +- src/renderer/generic.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 21a10d45f..4ba477c40 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -242,7 +242,7 @@ pub inline fn present(self: *Metal, target: Target, sync: bool) !void { } /// Present the last presented target again. (noop for Metal) -pub inline fn repeat(self: *Metal) !void { +pub inline fn presentLastTarget(self: *Metal) !void { _ = self; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 584d3cf9d..81dbae66e 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -357,7 +357,7 @@ pub fn present(self: *OpenGL, target: Target) !void { } /// Present the last presented target again. -pub fn repeat(self: *OpenGL) !void { +pub fn presentLastTarget(self: *OpenGL) !void { if (self.last_target) |target| try self.present(target); } diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index d7b4f4226..b3e6c4e12 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1274,7 +1274,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // We still need to present the last target again, because the // apprt may be swapping buffers and display an outdated frame // if we don't draw something new. - try self.api.repeat(); + try self.api.presentLastTarget(); return; } self.cells_rebuilt = false; From ddf1a5b23d2447b1d55656f95c2faad12570a84e Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 20 Jun 2025 16:16:17 -0600 Subject: [PATCH 520/642] renderer: move drawFrame AutoreleasePool handling to GraphicsAPI Introduces `drawFrameStart`/`drawFrameEnd` for this purpose. --- src/renderer/Metal.zig | 20 ++++++++++++++++++++ src/renderer/OpenGL.zig | 14 ++++++++++++++ src/renderer/generic.zig | 13 ++++--------- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 4ba477c40..03a568d87 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -61,6 +61,9 @@ blending: configpkg.Config.AlphaBlending, /// the "shared" storage mode, instead we have to use the "managed" mode. default_storage_mode: mtl.MTLResourceOptions.StorageMode, +/// We start an AutoreleasePool before `drawFrame` and end it afterwards. +autorelease_pool: ?*objc.AutoreleasePool = null, + pub fn init(alloc: Allocator, opts: rendererpkg.Options) !Metal { comptime switch (builtin.os.tag) { .macos, .ios => {}, @@ -185,6 +188,23 @@ fn displayCallback(renderer: *Renderer) align(8) void { }; } +/// Actions taken before doing anything in `drawFrame`. +/// +/// Right now we use this to start an AutoreleasePool. +pub fn drawFrameStart(self: *Metal) void { + assert(self.autorelease_pool == null); + self.autorelease_pool = .init(); +} + +/// Actions taken after `drawFrame` is done. +/// +/// Right now we use this to end our AutoreleasePool. +pub fn drawFrameEnd(self: *Metal) void { + assert(self.autorelease_pool != null); + self.autorelease_pool.?.deinit(); + self.autorelease_pool = null; +} + pub fn initShaders( self: *const Metal, alloc: Allocator, diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 81dbae66e..23496c148 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -289,6 +289,20 @@ pub fn displayRealized(self: *const OpenGL) void { } } +/// Actions taken before doing anything in `drawFrame`. +/// +/// Right now there's nothing we need to do for OpenGL. +pub fn drawFrameStart(self: *OpenGL) void { + _ = self; +} + +/// Actions taken after `drawFrame` is done. +/// +/// Right now there's nothing we need to do for OpenGL. +pub fn drawFrameEnd(self: *OpenGL) void { + _ = self; +} + pub fn initShaders( self: *const OpenGL, alloc: Allocator, diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index b3e6c4e12..f178d7bef 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -1241,15 +1241,10 @@ pub fn Renderer(comptime GraphicsAPI: type) type { self.draw_mutex.lock(); defer self.draw_mutex.unlock(); - // There's probably a more elegant way to do this... - // - // This is effectively an @autoreleasepool{} block, which we need in - // order to ensure that autoreleased objects are properly released. - const pool = if (builtin.os.tag.isDarwin()) - @import("objc").AutoreleasePool.init() - else - void; - defer if (builtin.os.tag.isDarwin()) pool.deinit(); + // Let our graphics API do any bookkeeping, etc. + // that it needs to do before / after `drawFrame`. + self.api.drawFrameStart(); + defer self.api.drawFrameEnd(); // Retrieve the most up-to-date surface size from the Graphics API const surface_size = try self.api.surfaceSize(); From b249fe0b2c68de13bac80f0262832d469ddd85bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rio=20Victor=20Ribeiro=20Silva?= Date: Fri, 20 Jun 2025 21:15:03 -0300 Subject: [PATCH 521/642] fix: undo poedit formatting --- po/pt_BR.UTF-8.po | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index c7bdf4df7..ba13f4460 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -11,14 +11,13 @@ msgstr "" "POT-Creation-Date: 2025-04-22 08:57-0700\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: Brazilian Portuguese \n" "Language: pt_BR\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" -"X-Generator: Poedit 3.6\n" #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5 msgid "Change Terminal Title" @@ -175,8 +174,8 @@ msgid "" "An application is attempting to read from the clipboard. The current " "clipboard contents are shown below." msgstr "" -"Uma aplicação está tentando ler da área de transferência. O conteúdo atual da " -"área de transferência está sendo exibido abaixo." +"Uma aplicação está tentando ler da área de transferência. O conteúdo atual " +"da área de transferência está sendo exibido abaixo." #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 @@ -190,8 +189,8 @@ msgstr "Permitir" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 msgid "" -"An application is attempting to write to the clipboard. The current clipboard " -"contents are shown below." +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." msgstr "" "Uma aplicação está tentando escrever na área de transferência. O conteúdo " "atual da área de transferência está aparecendo abaixo." @@ -221,7 +220,8 @@ msgid "New Split" msgstr "Nova divisão" #: src/apprt/gtk/Window.zig:312 -msgid "⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." msgstr "" "⚠️ Você está rodando uma build de debug do Ghostty! O desempenho será afetado." From 7ae5018fe8dbee7db7ff9da64a7ae4a11d874444 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 17 Jun 2025 20:59:12 -0700 Subject: [PATCH 522/642] macos: new terminal intent --- macos/Ghostty.xcodeproj/project.pbxproj | 16 ++++ macos/Sources/App/macOS/AppDelegate.swift | 2 +- .../App Intents/GhosttyIntentError.swift | 9 +++ .../App Intents/NewTerminalIntent.swift | 81 +++++++++++++++++++ .../Terminal/TerminalController.swift | 2 +- 5 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 macos/Sources/Features/App Intents/GhosttyIntentError.swift create mode 100644 macos/Sources/Features/App Intents/NewTerminalIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 5c584709e..c1a7bbaef 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -120,6 +120,8 @@ A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; }; A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; }; A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */; }; + A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */; }; + A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -240,6 +242,8 @@ A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = ""; }; A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = ""; }; A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroupCloseCoordinator.swift; sourceTree = ""; }; + A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTerminalIntent.swift; sourceTree = ""; }; + A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -299,6 +303,7 @@ A56D58872ACDE6BE00508D2C /* Services */, A59630982AEE1C4400D64628 /* Terminal */, A5CBD05A2CA0C5910017A1AE /* QuickTerminal */, + A5E4082C2E0237270035FEAC /* App Intents */, A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */, A57D79252C9C8782001D522E /* Secure Input */, A58636622DEF955100E04A10 /* Splits */, @@ -598,6 +603,15 @@ path = ClipboardConfirmation; sourceTree = ""; }; + A5E4082C2E0237270035FEAC /* App Intents */ = { + isa = PBXGroup; + children = ( + A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, + A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, + ); + path = "App Intents"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -777,8 +791,10 @@ A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, + A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, + A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */, A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */, A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */, A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index c56d7c3ac..7336f18d6 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -167,7 +167,7 @@ class AppDelegate: NSObject, // This registers the Ghostty => Services menu to exist. NSApp.servicesMenu = menuServices - + // Setup a local event monitor for app-level keyboard shortcuts. See // localEventHandler for more info why. _ = NSEvent.addLocalMonitorForEvents( diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift new file mode 100644 index 000000000..a04db1e6f --- /dev/null +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -0,0 +1,9 @@ +enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { + case appUnavailable + + var localizedStringResource: LocalizedStringResource { + switch self { + case .appUnavailable: return "The Ghostty app isn't properly initialized." + } + } +} diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift new file mode 100644 index 000000000..c54d31c09 --- /dev/null +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -0,0 +1,81 @@ +import AppKit +import AppIntents + +/// App intent that allows creating a new terminal window or tab. +/// +/// This requires macOS 15 or greater because we use features of macOS 15 here. +@available(macOS 15.0, *) +struct NewTerminalIntent: AppIntent { + static var title: LocalizedStringResource = "New Terminal" + static var description = IntentDescription("Create a new terminal.") + + @Parameter( + title: "Location", + description: "The location that the terminal should be created.", + default: .window + ) + var location: NewTerminalLocation + + @Parameter( + title: "Working Directory", + description: "The working directory to open in the terminal.", + supportedContentTypes: [.folder] + ) + var workingDirectory: IntentFile? + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .foreground(.immediate) + + @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") + static var openAppWhenRun = true + + static var parameterSummary: some ParameterSummary { + Summary("New Terminal \(\.$location)") + } + + @MainActor + func perform() async throws -> some IntentResult { + guard let appDelegate = NSApp.delegate as? AppDelegate else { + throw GhosttyIntentError.appUnavailable + } + + var config = Ghostty.SurfaceConfiguration() + + // If we were given a working directory then open that directory + if let url = workingDirectory?.fileURL { + let dir = url.hasDirectoryPath ? url : url.deletingLastPathComponent() + config.workingDirectory = dir.path(percentEncoded: false) + } + + switch location { + case .window: + _ = TerminalController.newWindow( + appDelegate.ghostty, + withBaseConfig: config) + + case .tab: + _ = TerminalController.newTab( + appDelegate.ghostty, + from: TerminalController.preferredParent?.window, + withBaseConfig: config) + } + + return .result() + } +} + +// MARK: NewTerminalLocation + +enum NewTerminalLocation: String { + case tab + case window +} + +extension NewTerminalLocation: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Location") + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .tab: .init(title: "Tab"), + .window: .init(title: "Window"), + ] +} diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 273744237..a224c9248 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -169,7 +169,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr private static var lastCascadePoint = NSPoint(x: 0, y: 0) // The preferred parent terminal controller. - private static var preferredParent: TerminalController? { + static var preferredParent: TerminalController? { all.first { $0.window?.isMainWindow ?? false } ?? all.last From 2aa731a64e13856f02f0484dfce403a855abb723 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 08:30:41 -0700 Subject: [PATCH 523/642] macos: TerminalEntity --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ .../Features/App Intents/TerminalEntity.swift | 67 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 macos/Sources/Features/App Intents/TerminalEntity.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index c1a7bbaef..913ce1995 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -122,6 +122,7 @@ A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */; }; A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */; }; A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; }; + A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -244,6 +245,7 @@ A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroupCloseCoordinator.swift; sourceTree = ""; }; A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTerminalIntent.swift; sourceTree = ""; }; A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = ""; }; + A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEntity.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -606,6 +608,7 @@ A5E4082C2E0237270035FEAC /* App Intents */ = { isa = PBXGroup; children = ( + A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); @@ -753,6 +756,7 @@ A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */, A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, + A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */, A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */, A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */, A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/TerminalEntity.swift b/macos/Sources/Features/App Intents/TerminalEntity.swift new file mode 100644 index 000000000..dabfa25ad --- /dev/null +++ b/macos/Sources/Features/App Intents/TerminalEntity.swift @@ -0,0 +1,67 @@ +import AppKit +import AppIntents + +struct TerminalEntity: AppEntity { + let id: UUID + + @Property(title: "Title") + var title: String + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: "Terminal") + } + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(title)") + } + + static var defaultQuery = TerminalQuery() + + init(_ view: Ghostty.SurfaceView) { + self.id = view.uuid + self.title = view.title + } +} + +struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { + @MainActor + func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] { + return all.filter { + identifiers.contains($0.uuid) + }.map { + TerminalEntity($0) + } + } + + @MainActor + func entities(matching string: String) async throws -> [TerminalEntity] { + return all.filter { + $0.title.localizedCaseInsensitiveContains(string) + }.map { + TerminalEntity($0) + } + } + + @MainActor + func allEntities() async throws -> [TerminalEntity] { + return all.map { TerminalEntity($0) } + } + + @MainActor + func suggestedEntities() async throws -> [TerminalEntity] { + return try await allEntities() + } + + @MainActor + private var all: [Ghostty.SurfaceView] { + // Find all of our terminal windows (includes quick terminal) + let controllers = NSApp.windows.compactMap { + $0.windowController as? BaseTerminalController + } + + // Get all our surfaces + return controllers.reduce([]) { result, c in + result + (c.surfaceTree.root?.leaves() ?? []) + } + } +} From 93f0ee2089c4642813d9002faa100a592027643f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 10:39:15 -0700 Subject: [PATCH 524/642] macos: GetTerminalDetails intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++ .../GetTerminalDetailsIntent.swift | 65 +++++++++++++++++++ .../Features/App Intents/TerminalEntity.swift | 26 +++++++- .../Sources/Ghostty/SurfaceView_AppKit.swift | 26 +++++++- .../Helpers/Extensions/NSView+Extension.swift | 19 ++++++ 5 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 913ce1995..7bac50670 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -123,6 +123,7 @@ A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */; }; A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; }; A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */; }; + A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -246,6 +247,7 @@ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTerminalIntent.swift; sourceTree = ""; }; A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = ""; }; A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEntity.swift; sourceTree = ""; }; + A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTerminalDetailsIntent.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -610,6 +612,7 @@ children = ( A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, + A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); path = "App Intents"; @@ -754,6 +757,7 @@ C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */, A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */, + A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */, A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */, A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */, A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift new file mode 100644 index 000000000..a57ad3ac4 --- /dev/null +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -0,0 +1,65 @@ +import AppKit +import AppIntents + +/// App intent that retrieves details about a specific terminal. +struct GetTerminalDetailsIntent: AppIntent { + static var title: LocalizedStringResource = "Get Details of Terminal" + + @Parameter( + title: "Detail", + description: "The detail to extract about a terminal." + ) + var detail: TerminalDetail + + @Parameter( + title: "Terminal", + description: "The terminal to extract information about." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + static var parameterSummary: some ParameterSummary { + Summary("Get \(\.$detail) from \(\.$terminal)") + } + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue { + switch detail { + case .title: return .result(value: terminal.title) + case .workingDirectory: return .result(value: terminal.workingDirectory) + case .allContents: + guard let view = terminal.surfaceView else { return .result(value: nil) } + return .result(value: view.cachedScreenContents.get()) + case .selectedText: + guard let view = terminal.surfaceView else { return .result(value: nil) } + return .result(value: view.accessibilitySelectedText()) + case .visibleText: + guard let view = terminal.surfaceView else { return .result(value: nil) } + return .result(value: view.cachedVisibleContents.get()) + } + } +} + +// MARK: TerminalDetail + +enum TerminalDetail: String { + case title + case workingDirectory + case allContents + case selectedText + case visibleText +} + +extension TerminalDetail: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Detail") + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .title: .init(title: "Title"), + .workingDirectory: .init(title: "Working Directory"), + .allContents: .init(title: "Full Contents"), + .selectedText: .init(title: "Selected Text"), + .visibleText: .init(title: "Visible Text"), + ] +} diff --git a/macos/Sources/Features/App Intents/TerminalEntity.swift b/macos/Sources/Features/App Intents/TerminalEntity.swift index dabfa25ad..0d2832bf5 100644 --- a/macos/Sources/Features/App Intents/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/TerminalEntity.swift @@ -1,5 +1,6 @@ import AppKit import AppIntents +import SwiftUI struct TerminalEntity: AppEntity { let id: UUID @@ -7,12 +8,31 @@ struct TerminalEntity: AppEntity { @Property(title: "Title") var title: String + @Property(title: "Working Directory") + var workingDirectory: String? + + var screenshot: Image? + static var typeDisplayRepresentation: TypeDisplayRepresentation { TypeDisplayRepresentation(name: "Terminal") } + @MainActor var displayRepresentation: DisplayRepresentation { - DisplayRepresentation(title: "\(title)") + var rep = DisplayRepresentation(title: "\(title)") + if let screenshot, + let nsImage = ImageRenderer(content: screenshot).nsImage, + let data = nsImage.tiffRepresentation { + rep.image = .init(data: data) + } + + return rep + } + + /// Returns the view associated with this entity. This may no longer exist. + @MainActor + var surfaceView: Ghostty.SurfaceView? { + Self.defaultQuery.all.first { $0.uuid == self.id } } static var defaultQuery = TerminalQuery() @@ -20,6 +40,8 @@ struct TerminalEntity: AppEntity { init(_ view: Ghostty.SurfaceView) { self.id = view.uuid self.title = view.title + self.workingDirectory = view.pwd + self.screenshot = view.screenshot() } } @@ -53,7 +75,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { } @MainActor - private var all: [Ghostty.SurfaceView] { + var all: [Ghostty.SurfaceView] { // Find all of our terminal windows (includes quick terminal) let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index a47dbdaca..131e39ba7 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -139,7 +139,8 @@ extension Ghostty { private var titleFromTerminal: String? // The cached contents of the screen. - private var cachedScreenContents: CachedValue + private(set) var cachedScreenContents: CachedValue + private(set) var cachedVisibleContents: CachedValue /// Event monitor (see individual events for why) private var eventMonitor: Any? = nil @@ -166,6 +167,7 @@ extension Ghostty { // it back up later so we can reference `self`. This is a hack we should // fix at some point. self.cachedScreenContents = .init(duration: .milliseconds(500)) { "" } + self.cachedVisibleContents = self.cachedScreenContents // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer @@ -193,6 +195,26 @@ extension Ghostty { defer { ghostty_surface_free_text(surface, &text) } return String(cString: text.text) } + cachedVisibleContents = .init(duration: .milliseconds(500)) { [weak self] in + guard let self else { return "" } + guard let surface = self.surface else { return "" } + var text = ghostty_text_s() + let sel = ghostty_selection_s( + top_left: ghostty_point_s( + tag: GHOSTTY_POINT_VIEWPORT, + coord: GHOSTTY_POINT_COORD_TOP_LEFT, + x: 0, + y: 0), + bottom_right: ghostty_point_s( + tag: GHOSTTY_POINT_VIEWPORT, + coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT, + x: 0, + y: 0), + rectangle: false) + guard ghostty_surface_read_text(surface, sel, &text) else { return "" } + defer { ghostty_surface_free_text(surface, &text) } + return String(cString: text.text) + } // Set a timer to show the ghost emoji after 500ms if no title is set titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in @@ -1979,7 +2001,7 @@ extension Ghostty.SurfaceView { /// Caches a value for some period of time, evicting it automatically when that time expires. /// We use this to cache our surface content. This probably should be extracted some day /// to a more generic helper. -fileprivate class CachedValue { +class CachedValue { private var value: T? private let fetch: () -> T private let duration: Duration diff --git a/macos/Sources/Helpers/Extensions/NSView+Extension.swift b/macos/Sources/Helpers/Extensions/NSView+Extension.swift index b3628d406..fb209e4ac 100644 --- a/macos/Sources/Helpers/Extensions/NSView+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSView+Extension.swift @@ -1,4 +1,5 @@ import AppKit +import SwiftUI extension NSView { /// Returns true if this view is currently in the responder chain @@ -15,6 +16,24 @@ extension NSView { } } +// MARK: Screenshot + +extension NSView { + /// Take a screenshot of just this view. + func screenshot() -> NSImage? { + guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { return nil } + cacheDisplay(in: bounds, to: bitmapRep) + let image = NSImage(size: bounds.size) + image.addRepresentation(bitmapRep) + return image + } + + func screenshot() -> Image? { + guard let nsImage: NSImage = self.screenshot() else { return nil } + return Image(nsImage: nsImage) + } +} + // MARK: View Traversal and Search extension NSView { From e51a93ee7cb859cf3c312293282dde31a0cc55d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 11:14:47 -0700 Subject: [PATCH 525/642] macos: Terminal entity has screen contents deferred --- .../Features/App Intents/TerminalEntity.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/macos/Sources/Features/App Intents/TerminalEntity.swift b/macos/Sources/Features/App Intents/TerminalEntity.swift index 0d2832bf5..3aea691fe 100644 --- a/macos/Sources/Features/App Intents/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/TerminalEntity.swift @@ -11,6 +11,26 @@ struct TerminalEntity: AppEntity { @Property(title: "Working Directory") var workingDirectory: String? + @MainActor + @DeferredProperty(title: "Full Contents") + @available(macOS 26.0, *) + var screenContents: String? { + get async { + guard let surfaceView else { return nil } + return surfaceView.cachedScreenContents.get() + } + } + + @MainActor + @DeferredProperty(title: "Visible Contents") + @available(macOS 26.0, *) + var visibleContents: String? { + get async { + guard let surfaceView else { return nil } + return surfaceView.cachedVisibleContents.get() + } + } + var screenshot: Image? static var typeDisplayRepresentation: TypeDisplayRepresentation { From b8d44637547cf8041f490e9fac4931d33ff51900 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 11:37:11 -0700 Subject: [PATCH 526/642] macos: terminal not found should be an error --- .../Features/App Intents/GetTerminalDetailsIntent.swift | 6 +++--- macos/Sources/Features/App Intents/GhosttyIntentError.swift | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index a57ad3ac4..5c41908f4 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -30,13 +30,13 @@ struct GetTerminalDetailsIntent: AppIntent { case .title: return .result(value: terminal.title) case .workingDirectory: return .result(value: terminal.workingDirectory) case .allContents: - guard let view = terminal.surfaceView else { return .result(value: nil) } + guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } return .result(value: view.cachedScreenContents.get()) case .selectedText: - guard let view = terminal.surfaceView else { return .result(value: nil) } + guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } return .result(value: view.accessibilitySelectedText()) case .visibleText: - guard let view = terminal.surfaceView else { return .result(value: nil) } + guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } return .result(value: view.cachedVisibleContents.get()) } } diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift index a04db1e6f..34a0636d9 100644 --- a/macos/Sources/Features/App Intents/GhosttyIntentError.swift +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -1,9 +1,11 @@ enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { case appUnavailable + case surfaceNotFound var localizedStringResource: LocalizedStringResource { switch self { case .appUnavailable: return "The Ghostty app isn't properly initialized." + case .surfaceNotFound: return "The terminal no longer exists." } } } From 683b38f62ca4032bb3c0880c3857b70c6707fd91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 19:37:41 -0700 Subject: [PATCH 527/642] macos: can specify parent terminal for new terminal intent --- .../App Intents/NewTerminalIntent.swift | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index c54d31c09..51b037cca 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -23,16 +23,18 @@ struct NewTerminalIntent: AppIntent { ) var workingDirectory: IntentFile? + @Parameter( + title: "Parent Terminal", + description: "The terminal to inherit the base configuration from." + ) + var parent: TerminalEntity? + @available(macOS 26.0, *) static var supportedModes: IntentModes = .foreground(.immediate) @available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes") static var openAppWhenRun = true - static var parameterSummary: some ParameterSummary { - Summary("New Terminal \(\.$location)") - } - @MainActor func perform() async throws -> some IntentResult { guard let appDelegate = NSApp.delegate as? AppDelegate else { @@ -47,16 +49,31 @@ struct NewTerminalIntent: AppIntent { config.workingDirectory = dir.path(percentEncoded: false) } + // Determine if we have a parent and get it + let parent: Ghostty.SurfaceView? + if let parentParam = self.parent { + guard let view = parentParam.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + parent = view + } else if let preferred = TerminalController.preferredParent { + parent = preferred.focusedSurface ?? preferred.surfaceTree.root?.leftmostLeaf() + } else { + parent = nil + } + switch location { case .window: _ = TerminalController.newWindow( appDelegate.ghostty, - withBaseConfig: config) + withBaseConfig: config, + withParent: parent?.window) case .tab: _ = TerminalController.newTab( appDelegate.ghostty, - from: TerminalController.preferredParent?.window, + from: parent?.window, withBaseConfig: config) } From bbb69c8f27273247cc8e838aef5075cc258575d9 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 18 Jun 2025 19:50:05 -0700 Subject: [PATCH 528/642] macos: NewTerminalIntent returns Terminal, can split --- .../App Intents/NewTerminalIntent.swift | 58 +++++++++++++++-- .../Terminal/BaseTerminalController.swift | 65 ++++++++++++------- 2 files changed, 93 insertions(+), 30 deletions(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 51b037cca..55f33bd46 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -1,5 +1,6 @@ import AppKit import AppIntents +import GhosttyKit /// App intent that allows creating a new terminal window or tab. /// @@ -16,6 +17,12 @@ struct NewTerminalIntent: AppIntent { ) var location: NewTerminalLocation + @Parameter( + title: "Command", + description: "Command to execute instead of the default shell." + ) + var command: String? + @Parameter( title: "Working Directory", description: "The working directory to open in the terminal.", @@ -36,12 +43,14 @@ struct NewTerminalIntent: AppIntent { static var openAppWhenRun = true @MainActor - func perform() async throws -> some IntentResult { + func perform() async throws -> some IntentResult & ReturnsValue { guard let appDelegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } + let ghostty = appDelegate.ghostty var config = Ghostty.SurfaceConfiguration() + config.command = command // If we were given a working directory then open that directory if let url = workingDirectory?.fileURL { @@ -65,19 +74,38 @@ struct NewTerminalIntent: AppIntent { switch location { case .window: - _ = TerminalController.newWindow( - appDelegate.ghostty, + let newController = TerminalController.newWindow( + ghostty, withBaseConfig: config, withParent: parent?.window) + if let view = newController.surfaceTree.root?.leftmostLeaf() { + return .result(value: TerminalEntity(view)) + } case .tab: - _ = TerminalController.newTab( - appDelegate.ghostty, + let newController = TerminalController.newTab( + ghostty, from: parent?.window, withBaseConfig: config) + if let view = newController?.surfaceTree.root?.leftmostLeaf() { + return .result(value: TerminalEntity(view)) + } + + case .splitLeft, .splitRight, .splitUp, .splitDown: + guard let parent, + let controller = parent.window?.windowController as? BaseTerminalController else { + throw GhosttyIntentError.surfaceNotFound + } + + if let view = controller.newSplit( + at: parent, + direction: location.splitDirection! + ) { + return .result(value: TerminalEntity(view)) + } } - return .result() + return .result(value: .none) } } @@ -86,6 +114,20 @@ struct NewTerminalIntent: AppIntent { enum NewTerminalLocation: String { case tab case window + case splitLeft = "split:left" + case splitRight = "split:right" + case splitUp = "split:up" + case splitDown = "split:down" + + var splitDirection: SplitTree.NewDirection? { + switch self { + case .splitLeft: return .left + case .splitRight: return .right + case .splitUp: return .up + case .splitDown: return .down + default: return nil + } + } } extension NewTerminalLocation: AppEnum { @@ -94,5 +136,9 @@ extension NewTerminalLocation: AppEnum { static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ .tab: .init(title: "Tab"), .window: .init(title: "Window"), + .splitLeft: .init(title: "Split Left"), + .splitRight: .init(title: "Split Right"), + .splitUp: .init(title: "Split Up"), + .splitDown: .init(title: "Split Down"), ] } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index bc91b920e..81b7d32b6 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -193,6 +193,46 @@ class BaseTerminalController: NSWindowController, } } + // MARK: Methods + + /// Create a new split. + @discardableResult + func newSplit( + at oldView: Ghostty.SurfaceView, + direction: SplitTree.NewDirection, + baseConfig config: Ghostty.SurfaceConfiguration? = nil + ) -> Ghostty.SurfaceView? { + // We can only create new splits for surfaces in our tree. + guard surfaceTree.root?.node(view: oldView) != nil else { return nil } + + // Create a new surface view + guard let ghostty_app = ghostty.app else { return nil } + let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config) + + // Do the split + let newTree: SplitTree + do { + newTree = try surfaceTree.insert( + view: newView, + at: oldView, + direction: direction) + } catch { + // If splitting fails for any reason (it should not), then we just log + // and return. The new view we created will be deinitialized and its + // no big deal. + Ghostty.logger.warning("failed to insert split: \(error)") + return nil + } + + replaceSurfaceTree( + newTree, + moveFocusTo: newView, + moveFocusFrom: oldView, + undoAction: "New Split") + + return newView + } + /// Called when the surfaceTree variable changed. /// /// Subclasses should call super first. @@ -477,30 +517,7 @@ class BaseTerminalController: NSWindowController, default: return } - // Create a new surface view - guard let ghostty_app = ghostty.app else { return } - let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config) - - // Do the split - let newTree: SplitTree - do { - newTree = try surfaceTree.insert( - view: newView, - at: oldView, - direction: splitDirection) - } catch { - // If splitting fails for any reason (it should not), then we just log - // and return. The new view we created will be deinitialized and its - // no big deal. - Ghostty.logger.warning("failed to insert split: \(error)") - return - } - - replaceSurfaceTree( - newTree, - moveFocusTo: newView, - moveFocusFrom: oldView, - undoAction: "New Split") + newSplit(at: oldView, direction: splitDirection, baseConfig: config) } @objc private func ghosttyDidEqualizeSplits(_ notification: Notification) { From 5259d0fa55e8f60db8632cf47dab98744eb730ba Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 07:07:32 -0700 Subject: [PATCH 529/642] macos: starting to work on new libghostty data models --- macos/Ghostty.xcodeproj/project.pbxproj | 16 +++-- .../TerminalCommandPalette.swift | 38 ++++------- macos/Sources/Ghostty/AppError.swift | 3 - macos/Sources/Ghostty/Ghostty.Command.swift | 46 +++++++++++++ macos/Sources/Ghostty/Ghostty.Error.swift | 12 ++++ macos/Sources/Ghostty/Ghostty.Surface.swift | 64 +++++++++++++++++++ macos/Sources/Ghostty/Package.swift | 9 +++ macos/Sources/Ghostty/SurfaceView.swift | 4 +- .../Sources/Ghostty/SurfaceView_AppKit.swift | 21 +++--- src/apprt/embedded.zig | 4 +- 10 files changed, 171 insertions(+), 46 deletions(-) delete mode 100644 macos/Sources/Ghostty/AppError.swift create mode 100644 macos/Sources/Ghostty/Ghostty.Command.swift create mode 100644 macos/Sources/Ghostty/Ghostty.Error.swift create mode 100644 macos/Sources/Ghostty/Ghostty.Surface.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 7bac50670..db2c9d893 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -53,7 +53,6 @@ A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; }; A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; - A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; }; A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; }; A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; }; A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; }; @@ -124,6 +123,9 @@ A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; }; A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */; }; A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */; }; + A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; }; + A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; }; + A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -175,7 +177,6 @@ A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = ""; }; A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = ""; }; A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = ""; }; - A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = ""; }; A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = ""; }; A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = ""; }; A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = ""; }; @@ -248,6 +249,9 @@ A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = ""; }; A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEntity.swift; sourceTree = ""; }; A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTerminalDetailsIntent.swift; sourceTree = ""; }; + A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = ""; }; + A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = ""; }; + A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -440,12 +444,14 @@ A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */, A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */, A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */, + A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */, A514C8D52B54A16400493A16 /* Ghostty.Config.swift */, A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */, + A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */, A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */, A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */, A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */, - A55685DF29A03A9F004303CE /* AppError.swift */, + A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */, A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */, A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */, ); @@ -766,6 +772,7 @@ A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, + A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */, A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */, A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */, @@ -800,6 +807,7 @@ A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */, A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */, + A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */, @@ -809,13 +817,13 @@ A57D79272C9C879B001D522E /* SecureInput.swift in Sources */, A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */, A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */, + A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */, A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */, A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */, A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */, A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */, A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */, A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */, - A55685E029A03A9F004303CE /* AppError.swift in Sources */, A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, diff --git a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift index 47f2baf23..d02828494 100644 --- a/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift +++ b/macos/Sources/Features/Command Palette/TerminalCommandPalette.swift @@ -17,33 +17,19 @@ struct TerminalCommandPaletteView: View { // The commands available to the command palette. private var commandOptions: [CommandOption] { - guard let surface = surfaceView.surface else { return [] } - - var ptr: UnsafeMutablePointer? = nil - var count: Int = 0 - ghostty_surface_commands(surface, &ptr, &count) - guard let ptr else { return [] } - - let buffer = UnsafeBufferPointer(start: ptr, count: count) - return Array(buffer).filter { c in - let key = String(cString: c.action_key) - switch (key) { - case "toggle_tab_overview", - "toggle_window_decorations", - "show_gtk_inspector": - return false - default: - return true - } - }.map { c in - let action = String(cString: c.action) - return CommandOption( - title: String(cString: c.title), - description: String(cString: c.description), - symbols: ghosttyConfig.keyboardShortcut(for: action)?.keyList - ) { - onAction(action) + guard let surface = surfaceView.surfaceModel else { return [] } + do { + return try surface.commands().map { c in + return CommandOption( + title: c.title, + description: c.description, + symbols: ghosttyConfig.keyboardShortcut(for: c.action)?.keyList + ) { + onAction(c.action) + } } + } catch { + return [] } } diff --git a/macos/Sources/Ghostty/AppError.swift b/macos/Sources/Ghostty/AppError.swift deleted file mode 100644 index 55f191d3d..000000000 --- a/macos/Sources/Ghostty/AppError.swift +++ /dev/null @@ -1,3 +0,0 @@ -enum AppError: Error { - case surfaceCreateError -} diff --git a/macos/Sources/Ghostty/Ghostty.Command.swift b/macos/Sources/Ghostty/Ghostty.Command.swift new file mode 100644 index 000000000..1479ae92d --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.Command.swift @@ -0,0 +1,46 @@ +import GhosttyKit + +extension Ghostty { + /// `ghostty_command_s` + struct Command: Sendable { + private let cValue: ghostty_command_s + + /// The title of the command. + var title: String { + String(cString: cValue.title) + } + + /// Human-friendly description of what this command will do. + var description: String { + String(cString: cValue.description) + } + + /// The full action that must be performed to invoke this command. + var action: String { + String(cString: cValue.action) + } + + /// Only the key portion of the action so you can compare action types, e.g. `goto_split` + /// instead of `goto_split:left`. + var actionKey: String { + String(cString: cValue.action_key) + } + + /// True if this can be performed on this target. + var isSupported: Bool { + !Self.unsupportedActionKeys.contains(actionKey) + } + + /// Unsupported action keys, because they either don't make sense in the context of our + /// target platform or they just aren't implemented yet. + static let unsupportedActionKeys: [String] = [ + "toggle_tab_overview", + "toggle_window_decorations", + "show_gtk_inspector", + ] + + init(cValue: ghostty_command_s) { + self.cValue = cValue + } + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Error.swift b/macos/Sources/Ghostty/Ghostty.Error.swift new file mode 100644 index 000000000..66f6857bf --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.Error.swift @@ -0,0 +1,12 @@ +extension Ghostty { + /// Possible errors from internal Ghostty calls. + enum Error: Swift.Error, CustomLocalizedStringResourceConvertible { + case apiFailed + + var localizedStringResource: LocalizedStringResource { + switch self { + case .apiFailed: return "libghostty API call failed" + } + } + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift new file mode 100644 index 000000000..5560ff3a8 --- /dev/null +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -0,0 +1,64 @@ +import GhosttyKit + +extension Ghostty { + /// Represents a single surface within Ghostty. + /// + /// NOTE(mitchellh): This is a work-in-progress class as part of a general refactor + /// of our Ghostty data model. At the time of writing there's still a ton of surface + /// functionality that is not encapsulated in this class. It is planned to migrate that + /// all over. + /// + /// Wraps a `ghostty_surface_t` + final class Surface: Sendable { + private let surface: ghostty_surface_t + + /// Read the underlying C value for this surface. This is unsafe because the value will be + /// freed when the Surface class is deinitialized. + var unsafeCValue: ghostty_surface_t { + surface + } + + /// Initialize from the C structure. + init(cSurface: ghostty_surface_t) { + self.surface = cSurface + } + + deinit { + // deinit is not guaranteed to happen on the main actor and our API + // calls into libghostty must happen there so we capture the surface + // value so we don't capture `self` and then we detach it in a task. + // We can't wait for the task to succeed so this will happen sometime + // but that's okay. + let surface = self.surface + Task.detached { @MainActor in + ghostty_surface_free(surface) + } + } + + /// Perform a keybinding action. + /// + /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` + /// you can perform `goto_tab:4` with this. + /// + /// Returns true if the action was performed. Invalid actions return false. + @MainActor + func perform(action: String) -> Bool { + let len = action.utf8CString.count + if (len == 0) { return false } + return action.withCString { cString in + ghostty_surface_binding_action(surface, cString, UInt(len - 1)) + } + } + + /// Command options for this surface. + @MainActor + func commands() throws -> [Command] { + var ptr: UnsafeMutablePointer? = nil + var count: Int = 0 + ghostty_surface_commands(surface, &ptr, &count) + guard let ptr else { throw Error.apiFailed } + let buffer = UnsafeBufferPointer(start: ptr, count: count) + return Array(buffer).map { Command(cValue: $0) }.filter { $0.isSupported } + } + } +} diff --git a/macos/Sources/Ghostty/Package.swift b/macos/Sources/Ghostty/Package.swift index 82721c17e..125a09825 100644 --- a/macos/Sources/Ghostty/Package.swift +++ b/macos/Sources/Ghostty/Package.swift @@ -19,6 +19,15 @@ struct Ghostty { static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show" } +// MARK: C Extensions + +/// A command is fully self-contained so it is Sendable. +extension ghostty_command_s: @unchecked @retroactive Sendable {} + +/// A surface is sendable because it is just a reference type. Using the surface in parameters +/// may be unsafe but the value itself is safe to send across threads. +extension ghostty_surface_t: @unchecked @retroactive Sendable {} + // MARK: Build Info extension Ghostty { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index f830da4ef..371e4ff41 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -79,7 +79,7 @@ extension Ghostty { let pubResign = center.publisher(for: NSWindow.didResignKeyNotification) #endif - Surface(view: surfaceView, size: geo.size) + SurfaceRepresentable(view: surfaceView, size: geo.size) .focused($surfaceFocus) .focusedValue(\.ghosttySurfacePwd, surfaceView.pwd) .focusedValue(\.ghosttySurfaceView, surfaceView) @@ -381,7 +381,7 @@ extension Ghostty { /// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible /// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to /// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with. - struct Surface: OSViewRepresentable { + struct SurfaceRepresentable: OSViewRepresentable { /// The view to render for the terminal surface. let view: SurfaceView diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 131e39ba7..fe0851261 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -115,10 +115,20 @@ extension Ghostty { } } + /// Returns the data model for this surface. + /// + /// Note: eventually, all surface access will be through this, but presently its in a transition + /// state so we're mixing this with direct surface access. + private(set) var surfaceModel: Ghostty.Surface? + + /// Returns the underlying C value for the surface. See "note" on surfaceModel. + var surface: ghostty_surface_t? { + surfaceModel?.unsafeCValue + } + // Notification identifiers associated with this surface var notificationIdentifiers: Set = [] - private(set) var surface: ghostty_surface_t? private var markedText: NSMutableAttributedString private(set) var focused: Bool = true private var prevPressureStage: Int = 0 @@ -282,10 +292,10 @@ extension Ghostty { let surface_cfg = baseConfig ?? SurfaceConfiguration() var surface_cfg_c = surface_cfg.ghosttyConfig(view: self) guard let surface = ghostty_surface_new(app, &surface_cfg_c) else { - self.error = AppError.surfaceCreateError + self.error = Ghostty.Error.apiFailed return } - self.surface = surface; + self.surfaceModel = Ghostty.Surface(cSurface: surface) // Setup our tracking area so we get mouse moved events updateTrackingAreas() @@ -340,11 +350,6 @@ extension Ghostty { // Remove any notifications associated with this surface let identifiers = Array(self.notificationIdentifiers) UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers) - - // Free our core surface resources - if let surface = self.surface { - ghostty_surface_free(surface) - } } func focusDidChange(_ focused: Bool) { diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a61c75e96..01e287d16 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -1837,12 +1837,10 @@ pub const CAPI = struct { return false; }; - _ = ptr.core_surface.performBindingAction(action) catch |err| { + return ptr.core_surface.performBindingAction(action) catch |err| { log.err("error performing binding action action={} err={}", .{ action, err }); return false; }; - - return true; } /// Complete a clipboard read request started via the read callback. From 14e46d09791bed1332b93f050a58eb69d9c8350b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 09:43:25 -0700 Subject: [PATCH 530/642] macos: InvokeCommandPaletteIntent and CommandEntity --- macos/Ghostty.xcodeproj/project.pbxproj | 18 ++- .../App Intents/CommandPaletteIntent.swift | 34 +++++ .../App Intents/Entities/CommandEntity.swift | 128 ++++++++++++++++++ .../{ => Entities}/TerminalEntity.swift | 5 + 4 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 macos/Sources/Features/App Intents/CommandPaletteIntent.swift create mode 100644 macos/Sources/Features/App Intents/Entities/CommandEntity.swift rename macos/Sources/Features/App Intents/{ => Entities}/TerminalEntity.swift (96%) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index db2c9d893..990280397 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -126,6 +126,8 @@ A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; }; A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; }; A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; }; + A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; }; + A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -252,6 +254,8 @@ A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = ""; }; A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = ""; }; A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = ""; }; + A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = ""; }; + A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -616,14 +620,24 @@ A5E4082C2E0237270035FEAC /* App Intents */ = { isa = PBXGroup; children = ( - A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, + A5E408412E0453370035FEAC /* Entities */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, + A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); path = "App Intents"; sourceTree = ""; }; + A5E408412E0453370035FEAC /* Entities */ = { + isa = PBXGroup; + children = ( + A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */, + A5E4083F2E04532A0035FEAC /* CommandEntity.swift */, + ); + path = Entities; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -750,6 +764,7 @@ buildActionMask = 2147483647; files = ( A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */, + A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */, A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */, A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */, A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */, @@ -827,6 +842,7 @@ A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */, A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */, A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */, + A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */, A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */, A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */, A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift new file mode 100644 index 000000000..2c1ff3386 --- /dev/null +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -0,0 +1,34 @@ +import AppKit +import AppIntents + +/// App intent that invokes a command palette entry. +@available(macOS 14.0, *) +struct CommandPaletteIntent: AppIntent { + static var title: LocalizedStringResource = "Invoke Command Palette Action" + + @Parameter( + title: "Terminal", + description: "The terminal to base available commands from." + ) + var terminal: TerminalEntity + + @Parameter( + title: "Command", + description: "The command to invoke.", + optionsProvider: CommandQuery() + ) + var command: CommandEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + let performed = surface.perform(action: command.action) + return .result(value: performed) + } +} diff --git a/macos/Sources/Features/App Intents/Entities/CommandEntity.swift b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift new file mode 100644 index 000000000..f7abcc6de --- /dev/null +++ b/macos/Sources/Features/App Intents/Entities/CommandEntity.swift @@ -0,0 +1,128 @@ +import AppIntents + +// MARK: AppEntity + +@available(macOS 14.0, *) +struct CommandEntity: AppEntity { + let id: ID + + // Note: for macOS 26 we can move all the properties to @ComputedProperty. + + @Property(title: "Title") + var title: String + + @Property(title: "Description") + var description: String + + @Property(title: "Action") + var action: String + + /// The underlying data model + let command: Ghostty.Command + + /// A command identifier is a composite key based on the terminal and action. + struct ID: Hashable { + let terminalId: TerminalEntity.ID + let actionKey: String + + init(terminalId: TerminalEntity.ID, actionKey: String) { + self.terminalId = terminalId + self.actionKey = actionKey + } + } + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: "Command Palette Command") + } + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation( + title: LocalizedStringResource(stringLiteral: command.title), + subtitle: LocalizedStringResource(stringLiteral: command.description), + ) + } + + static var defaultQuery = CommandQuery() + + init(_ command: Ghostty.Command, for terminal: TerminalEntity) { + self.id = .init(terminalId: terminal.id, actionKey: command.actionKey) + self.command = command + self.title = command.title + self.description = command.description + self.action = command.action + } +} + +@available(macOS 14.0, *) +extension CommandEntity.ID: RawRepresentable { + var rawValue: String { + return "\(terminalId):\(actionKey)" + } + + init?(rawValue: String) { + let components = rawValue.split(separator: ":", maxSplits: 1) + guard components.count == 2 else { return nil } + + guard let terminalId = TerminalEntity.ID(uuidString: String(components[0])) else { + return nil + } + + self.terminalId = terminalId + self.actionKey = String(components[1]) + } +} + +// Required by AppEntity +@available(macOS 14.0, *) +extension CommandEntity.ID: EntityIdentifierConvertible { + static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? { + .init(rawValue: entityIdentifierString) + } + + var entityIdentifierString: String { + rawValue + } +} + +// MARK: EntityQuery + +@available(macOS 14.0, *) +struct CommandQuery: EntityQuery { + // Inject our terminal parameter from our command palette intent. + @IntentParameterDependency(\.$terminal) + var commandPaletteIntent + + @MainActor + func entities(for identifiers: [CommandEntity.ID]) async throws -> [CommandEntity] { + // Extract unique terminal IDs to avoid fetching duplicates + let terminalIds = Set(identifiers.map(\.terminalId)) + let terminals = try await TerminalEntity.defaultQuery.entities(for: Array(terminalIds)) + + // Build a cache of terminals and their available commands + // This avoids repeated command fetching for the same terminal + typealias Tuple = (terminal: TerminalEntity, commands: [Ghostty.Command]) + let commandMap: [TerminalEntity.ID: Tuple] = + terminals.reduce(into: [:]) { result, terminal in + guard let commands = try? terminal.surfaceModel?.commands() else { return } + result[terminal.id] = (terminal: terminal, commands: commands) + } + + // Map each identifier to its corresponding CommandEntity. If a command doesn't + // exist it maps to nil and is removed via compactMap. + return identifiers.compactMap { id in + guard let (terminal, commands) = commandMap[id.terminalId], + let command = commands.first(where: { $0.actionKey == id.actionKey }) else { + return nil + } + + return CommandEntity(command, for: terminal) + } + } + + @MainActor + func suggestedEntities() async throws -> [CommandEntity] { + guard let terminal = commandPaletteIntent?.terminal, + let surface = terminal.surfaceModel else { return [] } + return try surface.commands().map { CommandEntity($0, for: terminal) } + } +} diff --git a/macos/Sources/Features/App Intents/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift similarity index 96% rename from macos/Sources/Features/App Intents/TerminalEntity.swift rename to macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 3aea691fe..750512d02 100644 --- a/macos/Sources/Features/App Intents/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -55,6 +55,11 @@ struct TerminalEntity: AppEntity { Self.defaultQuery.all.first { $0.uuid == self.id } } + @MainActor + var surfaceModel: Ghostty.Surface? { + surfaceView?.surfaceModel + } + static var defaultQuery = TerminalQuery() init(_ view: Ghostty.SurfaceView) { From c904e86883a8567d96ab6655d28365557d77c57f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 10:47:56 -0700 Subject: [PATCH 531/642] macos: invoke keybind intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 +++ .../Features/App Intents/KeybindIntent.swift | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 macos/Sources/Features/App Intents/KeybindIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 990280397..a691ce55f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -128,6 +128,7 @@ A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; }; A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; }; A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; }; + A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408442E0483F80035FEAC /* KeybindIntent.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -256,6 +257,7 @@ A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = ""; }; A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = ""; }; A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = ""; }; + A5E408442E0483F80035FEAC /* KeybindIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindIntent.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -624,6 +626,7 @@ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, + A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); path = "App Intents"; @@ -823,6 +826,7 @@ A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */, A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */, A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */, + A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */, A5FEB3002ABB69450068369E /* main.swift in Sources */, A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */, A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift new file mode 100644 index 000000000..ddb9c489c --- /dev/null +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -0,0 +1,32 @@ +import AppKit +import AppIntents + +/// App intent that invokes a command palette entry. +struct KeybindIntent: AppIntent { + static var title: LocalizedStringResource = "Invoke a Keybind Action" + + @Parameter( + title: "Terminal", + description: "The terminal to base available commands from." + ) + var terminal: TerminalEntity + + @Parameter( + title: "Action", + description: "The keybind action to invoke. This can be any valid keybind action you could put in a configuration file." + ) + var action: String + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + let performed = surface.perform(action: action) + return .result(value: performed) + } +} From a6074040e7f9268c84ce44bd5bbe0d072548b9ed Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 11:07:46 -0700 Subject: [PATCH 532/642] macos: input intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../Features/App Intents/InputIntent.swift | 92 +++++++++++++++++++ .../Features/App Intents/KeybindIntent.swift | 3 +- macos/Sources/Ghostty/Ghostty.Input.swift | 11 +++ macos/Sources/Ghostty/Ghostty.Surface.swift | 13 +++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 10 +- 6 files changed, 123 insertions(+), 10 deletions(-) create mode 100644 macos/Sources/Features/App Intents/InputIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a691ce55f..bbb34820f 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; }; A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; }; A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408442E0483F80035FEAC /* KeybindIntent.swift */; }; + A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408462E0485270035FEAC /* InputIntent.swift */; }; A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; }; AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; }; C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; }; @@ -258,6 +259,7 @@ A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = ""; }; A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = ""; }; A5E408442E0483F80035FEAC /* KeybindIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindIntent.swift; sourceTree = ""; }; + A5E408462E0485270035FEAC /* InputIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputIntent.swift; sourceTree = ""; }; A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = ""; }; C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = ""; }; @@ -626,6 +628,7 @@ A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, + A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, ); @@ -819,6 +822,7 @@ A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */, A5874D992DAD751B00E83852 /* CGS.swift in Sources */, A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */, + A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */, A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift in Sources */, A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */, A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift new file mode 100644 index 000000000..46c849c99 --- /dev/null +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -0,0 +1,92 @@ +import AppKit +import AppIntents + +/// App intent to input text in a terminal. +struct InputTextIntent: AppIntent { + static var title: LocalizedStringResource = "Input Text to Terminal" + + @Parameter( + title: "Text", + description: "The text to input to the terminal. The text will be inputted as if it was pasted.", + inputOptions: String.IntentInputOptions( + capitalizationType: .none, + multiline: true, + autocorrect: false, + smartQuotes: false, + smartDashes: false + ) + ) + var text: String + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + surface.sendText(text) + return .result() + } +} + +/// App intent to trigger a keyboard event. +struct KeyEventIntent: AppIntent { + static var title: LocalizedStringResource = "Send Keyboard Event to Terminal" + static var description = IntentDescription("Simulate a keyboard event. This will not handle text encoding; use the 'Input Text' action for that.") + + @Parameter( + title: "Text", + description: "The key to send to the terminal." + ) + var key: KeyIntentKey + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + surface.sendText(text) + return .result() + } +} + +// MARK: TerminalDetail + +enum KeyIntentKey: String { + case title + case workingDirectory + case allContents + case selectedText + case visibleText +} + +extension KeyIntentKey: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Detail") + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .title: .init(title: "Title"), + .workingDirectory: .init(title: "Working Directory"), + .allContents: .init(title: "Full Contents"), + .selectedText: .init(title: "Selected Text"), + .visibleText: .init(title: "Visible Text"), + ] +} diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index ddb9c489c..adeb64331 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -1,13 +1,12 @@ import AppKit import AppIntents -/// App intent that invokes a command palette entry. struct KeybindIntent: AppIntent { static var title: LocalizedStringResource = "Invoke a Keybind Action" @Parameter( title: "Terminal", - description: "The terminal to base available commands from." + description: "The terminal to invoke the action on." ) var terminal: TerminalEntity diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index 942ca5973..e18203f65 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -208,4 +208,15 @@ extension Ghostty { 0x43: GHOSTTY_KEY_NUMPAD_MULTIPLY, 0x4E: GHOSTTY_KEY_NUMPAD_SUBTRACT, ]; + + /// `ghostty_input_key_e` + enum Key: String { + case undentified + + var cKey: ghostty_input_key_e { + switch self { + case .undentified: GHOSTTY_KEY_UNIDENTIFIED + } + } + } } diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 5560ff3a8..10e699c1f 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -35,6 +35,19 @@ extension Ghostty { } } + /// Send text to the terminal as if it was typed. This doesn't send the key events so keyboard + /// shortcuts and other encodings do not take effect. + @MainActor + func sendText(_ text: String) { + let len = text.utf8CString.count + if (len == 0) { return } + + text.withCString { ptr in + // len includes the null terminator so we do len - 1 + ghostty_surface_text(surface, ptr, UInt(len - 1)) + } + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index fe0851261..2e7cf499b 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1700,7 +1700,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { func insertText(_ string: Any, replacementRange: NSRange) { // We must have an associated event guard NSApp.currentEvent != nil else { return } - guard let surface = self.surface else { return } + guard let surfaceModel else { return } // We want the string view of the any value var chars = "" @@ -1724,13 +1724,7 @@ extension Ghostty.SurfaceView: NSTextInputClient { return } - let len = chars.utf8CString.count - if (len == 0) { return } - - chars.withCString { ptr in - // len includes the null terminator so we do len - 1 - ghostty_surface_text(surface, ptr, UInt(len - 1)) - } + surfaceModel.sendText(chars) } /// This function needs to exist for two reasons: From 93619ad42045c42eaf3a1cc1cdb2cd3a32cc2d29 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 11:29:34 -0700 Subject: [PATCH 533/642] macos: Ghostty.Key --- .../Features/App Intents/InputIntent.swift | 25 +- macos/Sources/Ghostty/Ghostty.Input.swift | 905 +++++++++++++++--- macos/Sources/Ghostty/InspectorView.swift | 4 +- 3 files changed, 789 insertions(+), 145 deletions(-) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 46c849c99..1b9f88c9f 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -47,7 +47,7 @@ struct KeyEventIntent: AppIntent { title: "Text", description: "The key to send to the terminal." ) - var key: KeyIntentKey + var key: Ghostty.Key @Parameter( title: "Terminal", @@ -64,29 +64,6 @@ struct KeyEventIntent: AppIntent { throw GhosttyIntentError.surfaceNotFound } - surface.sendText(text) return .result() } } - -// MARK: TerminalDetail - -enum KeyIntentKey: String { - case title - case workingDirectory - case allContents - case selectedText - case visibleText -} - -extension KeyIntentKey: AppEnum { - static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Detail") - - static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ - .title: .init(title: "Title"), - .workingDirectory: .init(title: "Working Directory"), - .allContents: .init(title: "Full Contents"), - .selectedText: .init(title: "Selected Text"), - .visibleText: .init(title: "Visible Text"), - ] -} diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index e18203f65..b3060a44d 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -1,3 +1,4 @@ +import AppIntents import Cocoa import SwiftUI import GhosttyKit @@ -92,131 +93,797 @@ extension Ghostty { GHOSTTY_KEY_SPACE: .space, ] - // Mapping of event keyCode to ghostty input key values. This is cribbed from - // glfw mostly since we started as a glfw-based app way back in the day! - static let keycodeToKey: [UInt16 : ghostty_input_key_e] = [ - 0x1D: GHOSTTY_KEY_DIGIT_0, - 0x12: GHOSTTY_KEY_DIGIT_1, - 0x13: GHOSTTY_KEY_DIGIT_2, - 0x14: GHOSTTY_KEY_DIGIT_3, - 0x15: GHOSTTY_KEY_DIGIT_4, - 0x17: GHOSTTY_KEY_DIGIT_5, - 0x16: GHOSTTY_KEY_DIGIT_6, - 0x1A: GHOSTTY_KEY_DIGIT_7, - 0x1C: GHOSTTY_KEY_DIGIT_8, - 0x19: GHOSTTY_KEY_DIGIT_9, - 0x00: GHOSTTY_KEY_A, - 0x0B: GHOSTTY_KEY_B, - 0x08: GHOSTTY_KEY_C, - 0x02: GHOSTTY_KEY_D, - 0x0E: GHOSTTY_KEY_E, - 0x03: GHOSTTY_KEY_F, - 0x05: GHOSTTY_KEY_G, - 0x04: GHOSTTY_KEY_H, - 0x22: GHOSTTY_KEY_I, - 0x26: GHOSTTY_KEY_J, - 0x28: GHOSTTY_KEY_K, - 0x25: GHOSTTY_KEY_L, - 0x2E: GHOSTTY_KEY_M, - 0x2D: GHOSTTY_KEY_N, - 0x1F: GHOSTTY_KEY_O, - 0x23: GHOSTTY_KEY_P, - 0x0C: GHOSTTY_KEY_Q, - 0x0F: GHOSTTY_KEY_R, - 0x01: GHOSTTY_KEY_S, - 0x11: GHOSTTY_KEY_T, - 0x20: GHOSTTY_KEY_U, - 0x09: GHOSTTY_KEY_V, - 0x0D: GHOSTTY_KEY_W, - 0x07: GHOSTTY_KEY_X, - 0x10: GHOSTTY_KEY_Y, - 0x06: GHOSTTY_KEY_Z, - - 0x27: GHOSTTY_KEY_QUOTE, - 0x2A: GHOSTTY_KEY_BACKSLASH, - 0x2B: GHOSTTY_KEY_COMMA, - 0x18: GHOSTTY_KEY_EQUAL, - 0x32: GHOSTTY_KEY_BACKQUOTE, - 0x21: GHOSTTY_KEY_BRACKET_LEFT, - 0x1B: GHOSTTY_KEY_MINUS, - 0x2F: GHOSTTY_KEY_PERIOD, - 0x1E: GHOSTTY_KEY_BRACKET_RIGHT, - 0x29: GHOSTTY_KEY_SEMICOLON, - 0x2C: GHOSTTY_KEY_SLASH, - - 0x33: GHOSTTY_KEY_BACKSPACE, - 0x39: GHOSTTY_KEY_CAPS_LOCK, - 0x75: GHOSTTY_KEY_DELETE, - 0x7D: GHOSTTY_KEY_ARROW_DOWN, - 0x77: GHOSTTY_KEY_END, - 0x24: GHOSTTY_KEY_ENTER, - 0x35: GHOSTTY_KEY_ESCAPE, - 0x7A: GHOSTTY_KEY_F1, - 0x78: GHOSTTY_KEY_F2, - 0x63: GHOSTTY_KEY_F3, - 0x76: GHOSTTY_KEY_F4, - 0x60: GHOSTTY_KEY_F5, - 0x61: GHOSTTY_KEY_F6, - 0x62: GHOSTTY_KEY_F7, - 0x64: GHOSTTY_KEY_F8, - 0x65: GHOSTTY_KEY_F9, - 0x6D: GHOSTTY_KEY_F10, - 0x67: GHOSTTY_KEY_F11, - 0x6F: GHOSTTY_KEY_F12, - 0x69: GHOSTTY_KEY_PRINT_SCREEN, - 0x6B: GHOSTTY_KEY_F14, - 0x71: GHOSTTY_KEY_F15, - 0x6A: GHOSTTY_KEY_F16, - 0x40: GHOSTTY_KEY_F17, - 0x4F: GHOSTTY_KEY_F18, - 0x50: GHOSTTY_KEY_F19, - 0x5A: GHOSTTY_KEY_F20, - 0x73: GHOSTTY_KEY_HOME, - 0x72: GHOSTTY_KEY_INSERT, - 0x7B: GHOSTTY_KEY_ARROW_LEFT, - 0x3A: GHOSTTY_KEY_ALT_LEFT, - 0x3B: GHOSTTY_KEY_CONTROL_LEFT, - 0x38: GHOSTTY_KEY_SHIFT_LEFT, - 0x37: GHOSTTY_KEY_META_LEFT, - 0x47: GHOSTTY_KEY_NUM_LOCK, - 0x79: GHOSTTY_KEY_PAGE_DOWN, - 0x74: GHOSTTY_KEY_PAGE_UP, - 0x7C: GHOSTTY_KEY_ARROW_RIGHT, - 0x3D: GHOSTTY_KEY_ALT_RIGHT, - 0x3E: GHOSTTY_KEY_CONTROL_RIGHT, - 0x3C: GHOSTTY_KEY_SHIFT_RIGHT, - 0x36: GHOSTTY_KEY_META_RIGHT, - 0x31: GHOSTTY_KEY_SPACE, - 0x30: GHOSTTY_KEY_TAB, - 0x7E: GHOSTTY_KEY_ARROW_UP, - - 0x52: GHOSTTY_KEY_NUMPAD_0, - 0x53: GHOSTTY_KEY_NUMPAD_1, - 0x54: GHOSTTY_KEY_NUMPAD_2, - 0x55: GHOSTTY_KEY_NUMPAD_3, - 0x56: GHOSTTY_KEY_NUMPAD_4, - 0x57: GHOSTTY_KEY_NUMPAD_5, - 0x58: GHOSTTY_KEY_NUMPAD_6, - 0x59: GHOSTTY_KEY_NUMPAD_7, - 0x5B: GHOSTTY_KEY_NUMPAD_8, - 0x5C: GHOSTTY_KEY_NUMPAD_9, - 0x45: GHOSTTY_KEY_NUMPAD_ADD, - 0x41: GHOSTTY_KEY_NUMPAD_DECIMAL, - 0x4B: GHOSTTY_KEY_NUMPAD_DIVIDE, - 0x4C: GHOSTTY_KEY_NUMPAD_ENTER, - 0x51: GHOSTTY_KEY_NUMPAD_EQUAL, - 0x43: GHOSTTY_KEY_NUMPAD_MULTIPLY, - 0x4E: GHOSTTY_KEY_NUMPAD_SUBTRACT, - ]; - /// `ghostty_input_key_e` enum Key: String { - case undentified + // Writing System Keys + case backquote + case backslash + case bracketLeft + case bracketRight + case comma + case digit0 + case digit1 + case digit2 + case digit3 + case digit4 + case digit5 + case digit6 + case digit7 + case digit8 + case digit9 + case equal + case intlBackslash + case intlRo + case intlYen + case a + case b + case c + case d + case e + case f + case g + case h + case i + case j + case k + case l + case m + case n + case o + case p + case q + case r + case s + case t + case u + case v + case w + case x + case y + case z + case minus + case period + case quote + case semicolon + case slash + + // Functional Keys + case altLeft + case altRight + case backspace + case capsLock + case contextMenu + case controlLeft + case controlRight + case enter + case metaLeft + case metaRight + case shiftLeft + case shiftRight + case space + case tab + case convert + case kanaMode + case nonConvert + + // Control Pad Section + case delete + case end + case help + case home + case insert + case pageDown + case pageUp + + // Arrow Pad Section + case arrowDown + case arrowLeft + case arrowRight + case arrowUp + + // Numpad Section + case numLock + case numpad0 + case numpad1 + case numpad2 + case numpad3 + case numpad4 + case numpad5 + case numpad6 + case numpad7 + case numpad8 + case numpad9 + case numpadAdd + case numpadBackspace + case numpadClear + case numpadClearEntry + case numpadComma + case numpadDecimal + case numpadDivide + case numpadEnter + case numpadEqual + case numpadMemoryAdd + case numpadMemoryClear + case numpadMemoryRecall + case numpadMemoryStore + case numpadMemorySubtract + case numpadMultiply + case numpadParenLeft + case numpadParenRight + case numpadSubtract + case numpadSeparator + case numpadUp + case numpadDown + case numpadRight + case numpadLeft + case numpadBegin + case numpadHome + case numpadEnd + case numpadInsert + case numpadDelete + case numpadPageUp + case numpadPageDown + + // Function Section + case escape + case f1 + case f2 + case f3 + case f4 + case f5 + case f6 + case f7 + case f8 + case f9 + case f10 + case f11 + case f12 + case f13 + case f14 + case f15 + case f16 + case f17 + case f18 + case f19 + case f20 + case f21 + case f22 + case f23 + case f24 + case f25 + case fn + case fnLock + case printScreen + case scrollLock + case pause + + // Media Keys + case browserBack + case browserFavorites + case browserForward + case browserHome + case browserRefresh + case browserSearch + case browserStop + case eject + case launchApp1 + case launchApp2 + case launchMail + case mediaPlayPause + case mediaSelect + case mediaStop + case mediaTrackNext + case mediaTrackPrevious + case power + case sleep + case audioVolumeDown + case audioVolumeMute + case audioVolumeUp + case wakeUp + + // Legacy, Non-standard, and Special Keys + case copy + case cut + case paste + + /// Get a key from a keycode + init?(keyCode: UInt16) { + if let key = Key.allCases.first(where: { $0.keyCode == keyCode }) { + self = key + return + } + + return nil + } var cKey: ghostty_input_key_e { switch self { - case .undentified: GHOSTTY_KEY_UNIDENTIFIED + // Writing System Keys + case .backquote: GHOSTTY_KEY_BACKQUOTE + case .backslash: GHOSTTY_KEY_BACKSLASH + case .bracketLeft: GHOSTTY_KEY_BRACKET_LEFT + case .bracketRight: GHOSTTY_KEY_BRACKET_RIGHT + case .comma: GHOSTTY_KEY_COMMA + case .digit0: GHOSTTY_KEY_DIGIT_0 + case .digit1: GHOSTTY_KEY_DIGIT_1 + case .digit2: GHOSTTY_KEY_DIGIT_2 + case .digit3: GHOSTTY_KEY_DIGIT_3 + case .digit4: GHOSTTY_KEY_DIGIT_4 + case .digit5: GHOSTTY_KEY_DIGIT_5 + case .digit6: GHOSTTY_KEY_DIGIT_6 + case .digit7: GHOSTTY_KEY_DIGIT_7 + case .digit8: GHOSTTY_KEY_DIGIT_8 + case .digit9: GHOSTTY_KEY_DIGIT_9 + case .equal: GHOSTTY_KEY_EQUAL + case .intlBackslash: GHOSTTY_KEY_INTL_BACKSLASH + case .intlRo: GHOSTTY_KEY_INTL_RO + case .intlYen: GHOSTTY_KEY_INTL_YEN + case .a: GHOSTTY_KEY_A + case .b: GHOSTTY_KEY_B + case .c: GHOSTTY_KEY_C + case .d: GHOSTTY_KEY_D + case .e: GHOSTTY_KEY_E + case .f: GHOSTTY_KEY_F + case .g: GHOSTTY_KEY_G + case .h: GHOSTTY_KEY_H + case .i: GHOSTTY_KEY_I + case .j: GHOSTTY_KEY_J + case .k: GHOSTTY_KEY_K + case .l: GHOSTTY_KEY_L + case .m: GHOSTTY_KEY_M + case .n: GHOSTTY_KEY_N + case .o: GHOSTTY_KEY_O + case .p: GHOSTTY_KEY_P + case .q: GHOSTTY_KEY_Q + case .r: GHOSTTY_KEY_R + case .s: GHOSTTY_KEY_S + case .t: GHOSTTY_KEY_T + case .u: GHOSTTY_KEY_U + case .v: GHOSTTY_KEY_V + case .w: GHOSTTY_KEY_W + case .x: GHOSTTY_KEY_X + case .y: GHOSTTY_KEY_Y + case .z: GHOSTTY_KEY_Z + case .minus: GHOSTTY_KEY_MINUS + case .period: GHOSTTY_KEY_PERIOD + case .quote: GHOSTTY_KEY_QUOTE + case .semicolon: GHOSTTY_KEY_SEMICOLON + case .slash: GHOSTTY_KEY_SLASH + + // Functional Keys + case .altLeft: GHOSTTY_KEY_ALT_LEFT + case .altRight: GHOSTTY_KEY_ALT_RIGHT + case .backspace: GHOSTTY_KEY_BACKSPACE + case .capsLock: GHOSTTY_KEY_CAPS_LOCK + case .contextMenu: GHOSTTY_KEY_CONTEXT_MENU + case .controlLeft: GHOSTTY_KEY_CONTROL_LEFT + case .controlRight: GHOSTTY_KEY_CONTROL_RIGHT + case .enter: GHOSTTY_KEY_ENTER + case .metaLeft: GHOSTTY_KEY_META_LEFT + case .metaRight: GHOSTTY_KEY_META_RIGHT + case .shiftLeft: GHOSTTY_KEY_SHIFT_LEFT + case .shiftRight: GHOSTTY_KEY_SHIFT_RIGHT + case .space: GHOSTTY_KEY_SPACE + case .tab: GHOSTTY_KEY_TAB + case .convert: GHOSTTY_KEY_CONVERT + case .kanaMode: GHOSTTY_KEY_KANA_MODE + case .nonConvert: GHOSTTY_KEY_NON_CONVERT + + // Control Pad Section + case .delete: GHOSTTY_KEY_DELETE + case .end: GHOSTTY_KEY_END + case .help: GHOSTTY_KEY_HELP + case .home: GHOSTTY_KEY_HOME + case .insert: GHOSTTY_KEY_INSERT + case .pageDown: GHOSTTY_KEY_PAGE_DOWN + case .pageUp: GHOSTTY_KEY_PAGE_UP + + // Arrow Pad Section + case .arrowDown: GHOSTTY_KEY_ARROW_DOWN + case .arrowLeft: GHOSTTY_KEY_ARROW_LEFT + case .arrowRight: GHOSTTY_KEY_ARROW_RIGHT + case .arrowUp: GHOSTTY_KEY_ARROW_UP + + // Numpad Section + case .numLock: GHOSTTY_KEY_NUM_LOCK + case .numpad0: GHOSTTY_KEY_NUMPAD_0 + case .numpad1: GHOSTTY_KEY_NUMPAD_1 + case .numpad2: GHOSTTY_KEY_NUMPAD_2 + case .numpad3: GHOSTTY_KEY_NUMPAD_3 + case .numpad4: GHOSTTY_KEY_NUMPAD_4 + case .numpad5: GHOSTTY_KEY_NUMPAD_5 + case .numpad6: GHOSTTY_KEY_NUMPAD_6 + case .numpad7: GHOSTTY_KEY_NUMPAD_7 + case .numpad8: GHOSTTY_KEY_NUMPAD_8 + case .numpad9: GHOSTTY_KEY_NUMPAD_9 + case .numpadAdd: GHOSTTY_KEY_NUMPAD_ADD + case .numpadBackspace: GHOSTTY_KEY_NUMPAD_BACKSPACE + case .numpadClear: GHOSTTY_KEY_NUMPAD_CLEAR + case .numpadClearEntry: GHOSTTY_KEY_NUMPAD_CLEAR_ENTRY + case .numpadComma: GHOSTTY_KEY_NUMPAD_COMMA + case .numpadDecimal: GHOSTTY_KEY_NUMPAD_DECIMAL + case .numpadDivide: GHOSTTY_KEY_NUMPAD_DIVIDE + case .numpadEnter: GHOSTTY_KEY_NUMPAD_ENTER + case .numpadEqual: GHOSTTY_KEY_NUMPAD_EQUAL + case .numpadMemoryAdd: GHOSTTY_KEY_NUMPAD_MEMORY_ADD + case .numpadMemoryClear: GHOSTTY_KEY_NUMPAD_MEMORY_CLEAR + case .numpadMemoryRecall: GHOSTTY_KEY_NUMPAD_MEMORY_RECALL + case .numpadMemoryStore: GHOSTTY_KEY_NUMPAD_MEMORY_STORE + case .numpadMemorySubtract: GHOSTTY_KEY_NUMPAD_MEMORY_SUBTRACT + case .numpadMultiply: GHOSTTY_KEY_NUMPAD_MULTIPLY + case .numpadParenLeft: GHOSTTY_KEY_NUMPAD_PAREN_LEFT + case .numpadParenRight: GHOSTTY_KEY_NUMPAD_PAREN_RIGHT + case .numpadSubtract: GHOSTTY_KEY_NUMPAD_SUBTRACT + case .numpadSeparator: GHOSTTY_KEY_NUMPAD_SEPARATOR + case .numpadUp: GHOSTTY_KEY_NUMPAD_UP + case .numpadDown: GHOSTTY_KEY_NUMPAD_DOWN + case .numpadRight: GHOSTTY_KEY_NUMPAD_RIGHT + case .numpadLeft: GHOSTTY_KEY_NUMPAD_LEFT + case .numpadBegin: GHOSTTY_KEY_NUMPAD_BEGIN + case .numpadHome: GHOSTTY_KEY_NUMPAD_HOME + case .numpadEnd: GHOSTTY_KEY_NUMPAD_END + case .numpadInsert: GHOSTTY_KEY_NUMPAD_INSERT + case .numpadDelete: GHOSTTY_KEY_NUMPAD_DELETE + case .numpadPageUp: GHOSTTY_KEY_NUMPAD_PAGE_UP + case .numpadPageDown: GHOSTTY_KEY_NUMPAD_PAGE_DOWN + + // Function Section + case .escape: GHOSTTY_KEY_ESCAPE + case .f1: GHOSTTY_KEY_F1 + case .f2: GHOSTTY_KEY_F2 + case .f3: GHOSTTY_KEY_F3 + case .f4: GHOSTTY_KEY_F4 + case .f5: GHOSTTY_KEY_F5 + case .f6: GHOSTTY_KEY_F6 + case .f7: GHOSTTY_KEY_F7 + case .f8: GHOSTTY_KEY_F8 + case .f9: GHOSTTY_KEY_F9 + case .f10: GHOSTTY_KEY_F10 + case .f11: GHOSTTY_KEY_F11 + case .f12: GHOSTTY_KEY_F12 + case .f13: GHOSTTY_KEY_F13 + case .f14: GHOSTTY_KEY_F14 + case .f15: GHOSTTY_KEY_F15 + case .f16: GHOSTTY_KEY_F16 + case .f17: GHOSTTY_KEY_F17 + case .f18: GHOSTTY_KEY_F18 + case .f19: GHOSTTY_KEY_F19 + case .f20: GHOSTTY_KEY_F20 + case .f21: GHOSTTY_KEY_F21 + case .f22: GHOSTTY_KEY_F22 + case .f23: GHOSTTY_KEY_F23 + case .f24: GHOSTTY_KEY_F24 + case .f25: GHOSTTY_KEY_F25 + case .fn: GHOSTTY_KEY_FN + case .fnLock: GHOSTTY_KEY_FN_LOCK + case .printScreen: GHOSTTY_KEY_PRINT_SCREEN + case .scrollLock: GHOSTTY_KEY_SCROLL_LOCK + case .pause: GHOSTTY_KEY_PAUSE + + // Media Keys + case .browserBack: GHOSTTY_KEY_BROWSER_BACK + case .browserFavorites: GHOSTTY_KEY_BROWSER_FAVORITES + case .browserForward: GHOSTTY_KEY_BROWSER_FORWARD + case .browserHome: GHOSTTY_KEY_BROWSER_HOME + case .browserRefresh: GHOSTTY_KEY_BROWSER_REFRESH + case .browserSearch: GHOSTTY_KEY_BROWSER_SEARCH + case .browserStop: GHOSTTY_KEY_BROWSER_STOP + case .eject: GHOSTTY_KEY_EJECT + case .launchApp1: GHOSTTY_KEY_LAUNCH_APP_1 + case .launchApp2: GHOSTTY_KEY_LAUNCH_APP_2 + case .launchMail: GHOSTTY_KEY_LAUNCH_MAIL + case .mediaPlayPause: GHOSTTY_KEY_MEDIA_PLAY_PAUSE + case .mediaSelect: GHOSTTY_KEY_MEDIA_SELECT + case .mediaStop: GHOSTTY_KEY_MEDIA_STOP + case .mediaTrackNext: GHOSTTY_KEY_MEDIA_TRACK_NEXT + case .mediaTrackPrevious: GHOSTTY_KEY_MEDIA_TRACK_PREVIOUS + case .power: GHOSTTY_KEY_POWER + case .sleep: GHOSTTY_KEY_SLEEP + case .audioVolumeDown: GHOSTTY_KEY_AUDIO_VOLUME_DOWN + case .audioVolumeMute: GHOSTTY_KEY_AUDIO_VOLUME_MUTE + case .audioVolumeUp: GHOSTTY_KEY_AUDIO_VOLUME_UP + case .wakeUp: GHOSTTY_KEY_WAKE_UP + + // Legacy, Non-standard, and Special Keys + case .copy: GHOSTTY_KEY_COPY + case .cut: GHOSTTY_KEY_CUT + case .paste: GHOSTTY_KEY_PASTE + } + } + + // Based on src/input/keycodes.zig + var keyCode: UInt16? { + switch self { + // Writing System Keys + case .backquote: return 0x0032 + case .backslash: return 0x002a + case .bracketLeft: return 0x0021 + case .bracketRight: return 0x001e + case .comma: return 0x002b + case .digit0: return 0x001d + case .digit1: return 0x0012 + case .digit2: return 0x0013 + case .digit3: return 0x0014 + case .digit4: return 0x0015 + case .digit5: return 0x0017 + case .digit6: return 0x0016 + case .digit7: return 0x001a + case .digit8: return 0x001c + case .digit9: return 0x0019 + case .equal: return 0x0018 + case .intlBackslash: return 0x000a + case .intlRo: return 0x005e + case .intlYen: return 0x005d + case .a: return 0x0000 + case .b: return 0x000b + case .c: return 0x0008 + case .d: return 0x0002 + case .e: return 0x000e + case .f: return 0x0003 + case .g: return 0x0005 + case .h: return 0x0004 + case .i: return 0x0022 + case .j: return 0x0026 + case .k: return 0x0028 + case .l: return 0x0025 + case .m: return 0x002e + case .n: return 0x002d + case .o: return 0x001f + case .p: return 0x0023 + case .q: return 0x000c + case .r: return 0x000f + case .s: return 0x0001 + case .t: return 0x0011 + case .u: return 0x0020 + case .v: return 0x0009 + case .w: return 0x000d + case .x: return 0x0007 + case .y: return 0x0010 + case .z: return 0x0006 + case .minus: return 0x001b + case .period: return 0x002f + case .quote: return 0x0027 + case .semicolon: return 0x0029 + case .slash: return 0x002c + + // Functional Keys + case .altLeft: return 0x003a + case .altRight: return 0x003d + case .backspace: return 0x0033 + case .capsLock: return 0x0039 + case .contextMenu: return 0x006e + case .controlLeft: return 0x003b + case .controlRight: return 0x003e + case .enter: return 0x0024 + case .metaLeft: return 0x0037 + case .metaRight: return 0x0036 + case .shiftLeft: return 0x0038 + case .shiftRight: return 0x003c + case .space: return 0x0031 + case .tab: return 0x0030 + case .convert: return nil // No Mac keycode + case .kanaMode: return nil // No Mac keycode + case .nonConvert: return nil // No Mac keycode + + // Control Pad Section + case .delete: return 0x0075 + case .end: return 0x0077 + case .help: return nil // No Mac keycode + case .home: return 0x0073 + case .insert: return 0x0072 + case .pageDown: return 0x0079 + case .pageUp: return 0x0074 + + // Arrow Pad Section + case .arrowDown: return 0x007d + case .arrowLeft: return 0x007b + case .arrowRight: return 0x007c + case .arrowUp: return 0x007e + + // Numpad Section + case .numLock: return 0x0047 + case .numpad0: return 0x0052 + case .numpad1: return 0x0053 + case .numpad2: return 0x0054 + case .numpad3: return 0x0055 + case .numpad4: return 0x0056 + case .numpad5: return 0x0057 + case .numpad6: return 0x0058 + case .numpad7: return 0x0059 + case .numpad8: return 0x005b + case .numpad9: return 0x005c + case .numpadAdd: return 0x0045 + case .numpadBackspace: return nil // No Mac keycode + case .numpadClear: return nil // No Mac keycode + case .numpadClearEntry: return nil // No Mac keycode + case .numpadComma: return 0x005f + case .numpadDecimal: return 0x0041 + case .numpadDivide: return 0x004b + case .numpadEnter: return 0x004c + case .numpadEqual: return 0x0051 + case .numpadMemoryAdd: return nil // No Mac keycode + case .numpadMemoryClear: return nil // No Mac keycode + case .numpadMemoryRecall: return nil // No Mac keycode + case .numpadMemoryStore: return nil // No Mac keycode + case .numpadMemorySubtract: return nil // No Mac keycode + case .numpadMultiply: return 0x0043 + case .numpadParenLeft: return nil // No Mac keycode + case .numpadParenRight: return nil // No Mac keycode + case .numpadSubtract: return 0x004e + case .numpadSeparator: return nil // No Mac keycode + case .numpadUp: return nil // No Mac keycode + case .numpadDown: return nil // No Mac keycode + case .numpadRight: return nil // No Mac keycode + case .numpadLeft: return nil // No Mac keycode + case .numpadBegin: return nil // No Mac keycode + case .numpadHome: return nil // No Mac keycode + case .numpadEnd: return nil // No Mac keycode + case .numpadInsert: return nil // No Mac keycode + case .numpadDelete: return nil // No Mac keycode + case .numpadPageUp: return nil // No Mac keycode + case .numpadPageDown: return nil // No Mac keycode + + // Function Section + case .escape: return 0x0035 + case .f1: return 0x007a + case .f2: return 0x0078 + case .f3: return 0x0063 + case .f4: return 0x0076 + case .f5: return 0x0060 + case .f6: return 0x0061 + case .f7: return 0x0062 + case .f8: return 0x0064 + case .f9: return 0x0065 + case .f10: return 0x006d + case .f11: return 0x0067 + case .f12: return 0x006f + case .f13: return 0x0069 + case .f14: return 0x006b + case .f15: return 0x0071 + case .f16: return 0x006a + case .f17: return 0x0040 + case .f18: return 0x004f + case .f19: return 0x0050 + case .f20: return 0x005a + case .f21: return nil // No Mac keycode + case .f22: return nil // No Mac keycode + case .f23: return nil // No Mac keycode + case .f24: return nil // No Mac keycode + case .f25: return nil // No Mac keycode + case .fn: return nil // No Mac keycode + case .fnLock: return nil // No Mac keycode + case .printScreen: return nil // No Mac keycode + case .scrollLock: return nil // No Mac keycode + case .pause: return nil // No Mac keycode + + // Media Keys + case .browserBack: return nil // No Mac keycode + case .browserFavorites: return nil // No Mac keycode + case .browserForward: return nil // No Mac keycode + case .browserHome: return nil // No Mac keycode + case .browserRefresh: return nil // No Mac keycode + case .browserSearch: return nil // No Mac keycode + case .browserStop: return nil // No Mac keycode + case .eject: return nil // No Mac keycode + case .launchApp1: return nil // No Mac keycode + case .launchApp2: return nil // No Mac keycode + case .launchMail: return nil // No Mac keycode + case .mediaPlayPause: return nil // No Mac keycode + case .mediaSelect: return nil // No Mac keycode + case .mediaStop: return nil // No Mac keycode + case .mediaTrackNext: return nil // No Mac keycode + case .mediaTrackPrevious: return nil // No Mac keycode + case .power: return nil // No Mac keycode + case .sleep: return nil // No Mac keycode + case .audioVolumeDown: return 0x0049 + case .audioVolumeMute: return 0x004a + case .audioVolumeUp: return 0x0048 + case .wakeUp: return nil // No Mac keycode + + // Legacy, Non-standard, and Special Keys + case .copy: return nil // No Mac keycode + case .cut: return nil // No Mac keycode + case .paste: return nil // No Mac keycode } } } } + +// MARK: Ghostty.Key AppEnum + +extension Ghostty.Key: AppEnum { + static var typeDisplayRepresentation: TypeDisplayRepresentation = "Key" + + static var caseDisplayRepresentations: [Ghostty.Key : DisplayRepresentation] = [ + // Writing System Keys + .backquote: "Backtick (`)", + .backslash: "Backslash (\\)", + .bracketLeft: "Left Bracket ([)", + .bracketRight: "Right Bracket (])", + .comma: "Comma (,)", + .digit0: "0", + .digit1: "1", + .digit2: "2", + .digit3: "3", + .digit4: "4", + .digit5: "5", + .digit6: "6", + .digit7: "7", + .digit8: "8", + .digit9: "9", + .equal: "Equal (=)", + .intlBackslash: "International Backslash", + .intlRo: "International Ro", + .intlYen: "International Yen", + .a: "A", + .b: "B", + .c: "C", + .d: "D", + .e: "E", + .f: "F", + .g: "G", + .h: "H", + .i: "I", + .j: "J", + .k: "K", + .l: "L", + .m: "M", + .n: "N", + .o: "O", + .p: "P", + .q: "Q", + .r: "R", + .s: "S", + .t: "T", + .u: "U", + .v: "V", + .w: "W", + .x: "X", + .y: "Y", + .z: "Z", + .minus: "Minus (-)", + .period: "Period (.)", + .quote: "Quote (')", + .semicolon: "Semicolon (;)", + .slash: "Slash (/)", + + // Functional Keys + .altLeft: "Left Alt", + .altRight: "Right Alt", + .backspace: "Backspace", + .capsLock: "Caps Lock", + .contextMenu: "Context Menu", + .controlLeft: "Left Control", + .controlRight: "Right Control", + .enter: "Enter", + .metaLeft: "Left Command", + .metaRight: "Right Command", + .shiftLeft: "Left Shift", + .shiftRight: "Right Shift", + .space: "Space", + .tab: "Tab", + .convert: "Convert", + .kanaMode: "Kana Mode", + .nonConvert: "Non Convert", + + // Control Pad Section + .delete: "Delete", + .end: "End", + .help: "Help", + .home: "Home", + .insert: "Insert", + .pageDown: "Page Down", + .pageUp: "Page Up", + + // Arrow Pad Section + .arrowDown: "Down Arrow", + .arrowLeft: "Left Arrow", + .arrowRight: "Right Arrow", + .arrowUp: "Up Arrow", + + // Numpad Section + .numLock: "Num Lock", + .numpad0: "Numpad 0", + .numpad1: "Numpad 1", + .numpad2: "Numpad 2", + .numpad3: "Numpad 3", + .numpad4: "Numpad 4", + .numpad5: "Numpad 5", + .numpad6: "Numpad 6", + .numpad7: "Numpad 7", + .numpad8: "Numpad 8", + .numpad9: "Numpad 9", + .numpadAdd: "Numpad Add (+)", + .numpadBackspace: "Numpad Backspace", + .numpadClear: "Numpad Clear", + .numpadClearEntry: "Numpad Clear Entry", + .numpadComma: "Numpad Comma", + .numpadDecimal: "Numpad Decimal", + .numpadDivide: "Numpad Divide (÷)", + .numpadEnter: "Numpad Enter", + .numpadEqual: "Numpad Equal", + .numpadMemoryAdd: "Numpad Memory Add", + .numpadMemoryClear: "Numpad Memory Clear", + .numpadMemoryRecall: "Numpad Memory Recall", + .numpadMemoryStore: "Numpad Memory Store", + .numpadMemorySubtract: "Numpad Memory Subtract", + .numpadMultiply: "Numpad Multiply (×)", + .numpadParenLeft: "Numpad Left Parenthesis", + .numpadParenRight: "Numpad Right Parenthesis", + .numpadSubtract: "Numpad Subtract (-)", + .numpadSeparator: "Numpad Separator", + .numpadUp: "Numpad Up", + .numpadDown: "Numpad Down", + .numpadRight: "Numpad Right", + .numpadLeft: "Numpad Left", + .numpadBegin: "Numpad Begin", + .numpadHome: "Numpad Home", + .numpadEnd: "Numpad End", + .numpadInsert: "Numpad Insert", + .numpadDelete: "Numpad Delete", + .numpadPageUp: "Numpad Page Up", + .numpadPageDown: "Numpad Page Down", + + // Function Section + .escape: "Escape", + .f1: "F1", + .f2: "F2", + .f3: "F3", + .f4: "F4", + .f5: "F5", + .f6: "F6", + .f7: "F7", + .f8: "F8", + .f9: "F9", + .f10: "F10", + .f11: "F11", + .f12: "F12", + .f13: "F13", + .f14: "F14", + .f15: "F15", + .f16: "F16", + .f17: "F17", + .f18: "F18", + .f19: "F19", + .f20: "F20", + .f21: "F21", + .f22: "F22", + .f23: "F23", + .f24: "F24", + .f25: "F25", + .fn: "Fn", + .fnLock: "Fn Lock", + .printScreen: "Print Screen", + .scrollLock: "Scroll Lock", + .pause: "Pause", + + // Media Keys + .browserBack: "Browser Back", + .browserFavorites: "Browser Favorites", + .browserForward: "Browser Forward", + .browserHome: "Browser Home", + .browserRefresh: "Browser Refresh", + .browserSearch: "Browser Search", + .browserStop: "Browser Stop", + .eject: "Eject", + .launchApp1: "Launch App 1", + .launchApp2: "Launch App 2", + .launchMail: "Launch Mail", + .mediaPlayPause: "Media Play/Pause", + .mediaSelect: "Media Select", + .mediaStop: "Media Stop", + .mediaTrackNext: "Media Next Track", + .mediaTrackPrevious: "Media Previous Track", + .power: "Power", + .sleep: "Sleep", + .audioVolumeDown: "Volume Down", + .audioVolumeMute: "Volume Mute", + .audioVolumeUp: "Volume Up", + .wakeUp: "Wake Up", + + // Legacy, Non-standard, and Special Keys + .copy: "Copy", + .cut: "Cut", + .paste: "Paste" + ] +} diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index a6e80bd47..491ec86e1 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -337,9 +337,9 @@ extension Ghostty { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let inspector = self.inspector else { return } - guard let key = Ghostty.keycodeToKey[event.keyCode] else { return } + guard let key = Ghostty.Key(keyCode: event.keyCode) else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_inspector_key(inspector, action, key, mods) + ghostty_inspector_key(inspector, action, key.cKey, mods) } // MARK: NSTextInputClient From 71b6e223af1ef2e2033326792e86bc9587326e91 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 12:06:27 -0700 Subject: [PATCH 534/642] macos: input keyboard event can send modifiers and actions now --- .../Features/App Intents/InputIntent.swift | 60 ++- macos/Sources/Ghostty/Ghostty.Input.swift | 509 +++++++++++------- macos/Sources/Ghostty/Ghostty.Surface.swift | 14 + macos/Sources/Ghostty/InspectorView.swift | 2 +- 4 files changed, 382 insertions(+), 203 deletions(-) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 1b9f88c9f..6d3d60d59 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -44,10 +44,25 @@ struct KeyEventIntent: AppIntent { static var description = IntentDescription("Simulate a keyboard event. This will not handle text encoding; use the 'Input Text' action for that.") @Parameter( - title: "Text", - description: "The key to send to the terminal." + title: "Key", + description: "The key to send to the terminal.", + default: .enter ) - var key: Ghostty.Key + var key: Ghostty.Input.Key + + @Parameter( + title: "Modifier(s)", + description: "The modifiers to send with the key event.", + default: [] + ) + var mods: [KeyEventMods] + + @Parameter( + title: "Event Type", + description: "A key press or release.", + default: .press + ) + var action: Ghostty.Input.Action @Parameter( title: "Terminal", @@ -64,6 +79,45 @@ struct KeyEventIntent: AppIntent { throw GhosttyIntentError.surfaceNotFound } + // Convert KeyEventMods array to Ghostty.Input.Mods + let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in + result.union(mod.ghosttyMod) + } + + let keyEvent = Ghostty.Input.KeyEvent( + key: key, + action: action, + mods: ghosttyMods + ) + surface.sendKeyEvent(keyEvent) + return .result() } } + +// MARK: Mods + +enum KeyEventMods: String, AppEnum, CaseIterable { + case shift + case control + case option + case command + + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key") + + static var caseDisplayRepresentations: [KeyEventMods : DisplayRepresentation] = [ + .shift: "Shift", + .control: "Control", + .option: "Option", + .command: "Command" + ] + + var ghosttyMod: Ghostty.Input.Mods { + switch self { + case .shift: .shift + case .control: .ctrl + case .option: .alt + case .command: .super + } + } +} diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index b3060a44d..df93017c7 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -4,6 +4,8 @@ import SwiftUI import GhosttyKit extension Ghostty { + struct Input {} + // MARK: Keyboard Shortcuts /// Return the key equivalent for the given trigger. @@ -92,7 +94,175 @@ extension Ghostty { GHOSTTY_KEY_BACKSPACE: .delete, GHOSTTY_KEY_SPACE: .space, ] +} +// MARK: Ghostty.Input.KeyEvent + +extension Ghostty.Input { + /// `ghostty_input_key_s` + struct KeyEvent { + let action: Action + let key: Key + let text: String? + let composing: Bool + let mods: Mods + let consumedMods: Mods + let unshiftedCodepoint: UInt32 + + init( + key: Key, + action: Action = .press, + text: String? = nil, + composing: Bool = false, + mods: Mods = [], + consumedMods: Mods = [], + unshiftedCodepoint: UInt32 = 0 + ) { + self.key = key + self.action = action + self.text = text + self.composing = composing + self.mods = mods + self.consumedMods = consumedMods + self.unshiftedCodepoint = unshiftedCodepoint + } + + init?(cValue: ghostty_input_key_s) { + // Convert action + switch cValue.action { + case GHOSTTY_ACTION_PRESS: self.action = .press + case GHOSTTY_ACTION_RELEASE: self.action = .release + case GHOSTTY_ACTION_REPEAT: self.action = .repeat + default: self.action = .press + } + + // Convert key from keycode + guard let key = Key(keyCode: UInt16(cValue.keycode)) else { return nil } + self.key = key + + // Convert text + if let textPtr = cValue.text { + self.text = String(cString: textPtr) + } else { + self.text = nil + } + + // Set composing state + self.composing = cValue.composing + + // Convert modifiers + self.mods = Mods(cMods: cValue.mods) + self.consumedMods = Mods(cMods: cValue.consumed_mods) + + // Set unshifted codepoint + self.unshiftedCodepoint = cValue.unshifted_codepoint + } + + /// Executes a closure with a temporary C representation of this KeyEvent. + /// + /// This method safely converts the Swift KeyEntity to a C `ghostty_input_key_s` struct + /// and passes it to the provided closure. The C struct is only valid within the closure's + /// execution scope. The text field's C string pointer is managed automatically and will + /// be invalid after the closure returns. + /// + /// - Parameter execute: A closure that receives the C struct and returns a value + /// - Returns: The value returned by the closure + @discardableResult + func withCValue(execute: (ghostty_input_key_s) -> T) -> T { + var keyEvent = ghostty_input_key_s() + keyEvent.action = action.cAction + keyEvent.keycode = UInt32(key.keyCode ?? 0) + keyEvent.composing = composing + keyEvent.mods = mods.cMods + keyEvent.consumed_mods = consumedMods.cMods + keyEvent.unshifted_codepoint = unshiftedCodepoint + + // Handle text with proper memory management + if let text = text { + return text.withCString { textPtr in + keyEvent.text = textPtr + return execute(keyEvent) + } + } else { + keyEvent.text = nil + return execute(keyEvent) + } + } + } +} + +// MARK: Ghostty.Input.Action + +extension Ghostty.Input { + /// `ghostty_input_action_e` + enum Action: String, CaseIterable { + case release + case press + case `repeat` + + var cAction: ghostty_input_action_e { + switch self { + case .release: GHOSTTY_ACTION_RELEASE + case .press: GHOSTTY_ACTION_PRESS + case .repeat: GHOSTTY_ACTION_REPEAT + } + } + } +} + +extension Ghostty.Input.Action: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key Action") + + static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [ + .release: "Release", + .press: "Press", + .repeat: "Repeat" + ] +} + +// MARK: Ghostty.Input.Mods + +extension Ghostty.Input { + /// `ghostty_input_mods_e` + struct Mods: OptionSet { + let rawValue: UInt32 + + static let none = Mods(rawValue: GHOSTTY_MODS_NONE.rawValue) + static let shift = Mods(rawValue: GHOSTTY_MODS_SHIFT.rawValue) + static let ctrl = Mods(rawValue: GHOSTTY_MODS_CTRL.rawValue) + static let alt = Mods(rawValue: GHOSTTY_MODS_ALT.rawValue) + static let `super` = Mods(rawValue: GHOSTTY_MODS_SUPER.rawValue) + static let caps = Mods(rawValue: GHOSTTY_MODS_CAPS.rawValue) + static let shiftRight = Mods(rawValue: GHOSTTY_MODS_SHIFT_RIGHT.rawValue) + static let ctrlRight = Mods(rawValue: GHOSTTY_MODS_CTRL_RIGHT.rawValue) + static let altRight = Mods(rawValue: GHOSTTY_MODS_ALT_RIGHT.rawValue) + static let superRight = Mods(rawValue: GHOSTTY_MODS_SUPER_RIGHT.rawValue) + + var cMods: ghostty_input_mods_e { + ghostty_input_mods_e(rawValue) + } + + init(rawValue: UInt32) { + self.rawValue = rawValue + } + + init(cMods: ghostty_input_mods_e) { + self.rawValue = cMods.rawValue + } + + init(nsFlags: NSEvent.ModifierFlags) { + self.init(cMods: Ghostty.ghosttyMods(nsFlags)) + } + + var nsFlags: NSEvent.ModifierFlags { + Ghostty.eventModifierFlags(mods: cMods) + } + } +} + +// MARK: Ghostty.Input.Key + +extension Ghostty.Input { /// `ghostty_input_key_e` enum Key: String { // Writing System Keys @@ -146,7 +316,7 @@ extension Ghostty { case quote case semicolon case slash - + // Functional Keys case altLeft case altRight @@ -165,7 +335,7 @@ extension Ghostty { case convert case kanaMode case nonConvert - + // Control Pad Section case delete case end @@ -174,13 +344,13 @@ extension Ghostty { case insert case pageDown case pageUp - + // Arrow Pad Section case arrowDown case arrowLeft case arrowRight case arrowUp - + // Numpad Section case numLock case numpad0 @@ -223,7 +393,7 @@ extension Ghostty { case numpadDelete case numpadPageUp case numpadPageDown - + // Function Section case escape case f1 @@ -256,7 +426,7 @@ extension Ghostty { case printScreen case scrollLock case pause - + // Media Keys case browserBack case browserFavorites @@ -280,7 +450,7 @@ extension Ghostty { case audioVolumeMute case audioVolumeUp case wakeUp - + // Legacy, Non-standard, and Special Keys case copy case cut @@ -349,7 +519,7 @@ extension Ghostty { case .quote: GHOSTTY_KEY_QUOTE case .semicolon: GHOSTTY_KEY_SEMICOLON case .slash: GHOSTTY_KEY_SLASH - + // Functional Keys case .altLeft: GHOSTTY_KEY_ALT_LEFT case .altRight: GHOSTTY_KEY_ALT_RIGHT @@ -368,7 +538,7 @@ extension Ghostty { case .convert: GHOSTTY_KEY_CONVERT case .kanaMode: GHOSTTY_KEY_KANA_MODE case .nonConvert: GHOSTTY_KEY_NON_CONVERT - + // Control Pad Section case .delete: GHOSTTY_KEY_DELETE case .end: GHOSTTY_KEY_END @@ -377,13 +547,13 @@ extension Ghostty { case .insert: GHOSTTY_KEY_INSERT case .pageDown: GHOSTTY_KEY_PAGE_DOWN case .pageUp: GHOSTTY_KEY_PAGE_UP - + // Arrow Pad Section case .arrowDown: GHOSTTY_KEY_ARROW_DOWN case .arrowLeft: GHOSTTY_KEY_ARROW_LEFT case .arrowRight: GHOSTTY_KEY_ARROW_RIGHT case .arrowUp: GHOSTTY_KEY_ARROW_UP - + // Numpad Section case .numLock: GHOSTTY_KEY_NUM_LOCK case .numpad0: GHOSTTY_KEY_NUMPAD_0 @@ -426,7 +596,7 @@ extension Ghostty { case .numpadDelete: GHOSTTY_KEY_NUMPAD_DELETE case .numpadPageUp: GHOSTTY_KEY_NUMPAD_PAGE_UP case .numpadPageDown: GHOSTTY_KEY_NUMPAD_PAGE_DOWN - + // Function Section case .escape: GHOSTTY_KEY_ESCAPE case .f1: GHOSTTY_KEY_F1 @@ -459,7 +629,7 @@ extension Ghostty { case .printScreen: GHOSTTY_KEY_PRINT_SCREEN case .scrollLock: GHOSTTY_KEY_SCROLL_LOCK case .pause: GHOSTTY_KEY_PAUSE - + // Media Keys case .browserBack: GHOSTTY_KEY_BROWSER_BACK case .browserFavorites: GHOSTTY_KEY_BROWSER_FAVORITES @@ -483,7 +653,7 @@ extension Ghostty { case .audioVolumeMute: GHOSTTY_KEY_AUDIO_VOLUME_MUTE case .audioVolumeUp: GHOSTTY_KEY_AUDIO_VOLUME_UP case .wakeUp: GHOSTTY_KEY_WAKE_UP - + // Legacy, Non-standard, and Special Keys case .copy: GHOSTTY_KEY_COPY case .cut: GHOSTTY_KEY_CUT @@ -545,7 +715,7 @@ extension Ghostty { case .quote: return 0x0027 case .semicolon: return 0x0029 case .slash: return 0x002c - + // Functional Keys case .altLeft: return 0x003a case .altRight: return 0x003d @@ -564,7 +734,7 @@ extension Ghostty { case .convert: return nil // No Mac keycode case .kanaMode: return nil // No Mac keycode case .nonConvert: return nil // No Mac keycode - + // Control Pad Section case .delete: return 0x0075 case .end: return 0x0077 @@ -573,13 +743,13 @@ extension Ghostty { case .insert: return 0x0072 case .pageDown: return 0x0079 case .pageUp: return 0x0074 - + // Arrow Pad Section case .arrowDown: return 0x007d case .arrowLeft: return 0x007b case .arrowRight: return 0x007c case .arrowUp: return 0x007e - + // Numpad Section case .numLock: return 0x0047 case .numpad0: return 0x0052 @@ -622,7 +792,7 @@ extension Ghostty { case .numpadDelete: return nil // No Mac keycode case .numpadPageUp: return nil // No Mac keycode case .numpadPageDown: return nil // No Mac keycode - + // Function Section case .escape: return 0x0035 case .f1: return 0x007a @@ -655,7 +825,7 @@ extension Ghostty { case .printScreen: return nil // No Mac keycode case .scrollLock: return nil // No Mac keycode case .pause: return nil // No Mac keycode - + // Media Keys case .browserBack: return nil // No Mac keycode case .browserFavorites: return nil // No Mac keycode @@ -679,7 +849,7 @@ extension Ghostty { case .audioVolumeMute: return 0x004a case .audioVolumeUp: return 0x0048 case .wakeUp: return nil // No Mac keycode - + // Legacy, Non-standard, and Special Keys case .copy: return nil // No Mac keycode case .cut: return nil // No Mac keycode @@ -689,201 +859,142 @@ extension Ghostty { } } -// MARK: Ghostty.Key AppEnum +extension Ghostty.Input.Key: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Key") -extension Ghostty.Key: AppEnum { - static var typeDisplayRepresentation: TypeDisplayRepresentation = "Key" - - static var caseDisplayRepresentations: [Ghostty.Key : DisplayRepresentation] = [ - // Writing System Keys - .backquote: "Backtick (`)", - .backslash: "Backslash (\\)", - .bracketLeft: "Left Bracket ([)", - .bracketRight: "Right Bracket (])", - .comma: "Comma (,)", - .digit0: "0", - .digit1: "1", - .digit2: "2", - .digit3: "3", - .digit4: "4", - .digit5: "5", - .digit6: "6", - .digit7: "7", - .digit8: "8", - .digit9: "9", - .equal: "Equal (=)", - .intlBackslash: "International Backslash", - .intlRo: "International Ro", - .intlYen: "International Yen", - .a: "A", - .b: "B", - .c: "C", - .d: "D", - .e: "E", - .f: "F", - .g: "G", - .h: "H", - .i: "I", - .j: "J", - .k: "K", - .l: "L", - .m: "M", - .n: "N", - .o: "O", - .p: "P", - .q: "Q", - .r: "R", - .s: "S", - .t: "T", - .u: "U", - .v: "V", - .w: "W", - .x: "X", - .y: "Y", - .z: "Z", - .minus: "Minus (-)", - .period: "Period (.)", - .quote: "Quote (')", - .semicolon: "Semicolon (;)", - .slash: "Slash (/)", + // Only include keys that have Mac keycodes for App Intents + static var allCases: [Ghostty.Input.Key] { + return [ + // Letters (A-Z) + .a, .b, .c, .d, .e, .f, .g, .h, .i, .j, .k, .l, .m, .n, .o, .p, .q, .r, .s, .t, .u, .v, .w, .x, .y, .z, + + // Numbers (0-9) + .digit0, .digit1, .digit2, .digit3, .digit4, .digit5, .digit6, .digit7, .digit8, .digit9, + + // Common Control Keys + .space, .enter, .tab, .backspace, .escape, .delete, + + // Arrow Keys + .arrowUp, .arrowDown, .arrowLeft, .arrowRight, + + // Navigation Keys + .home, .end, .pageUp, .pageDown, .insert, + + // Function Keys (F1-F20) + .f1, .f2, .f3, .f4, .f5, .f6, .f7, .f8, .f9, .f10, .f11, .f12, + .f13, .f14, .f15, .f16, .f17, .f18, .f19, .f20, + + // Modifier Keys + .shiftLeft, .shiftRight, .controlLeft, .controlRight, .altLeft, .altRight, + .metaLeft, .metaRight, .capsLock, + + // Punctuation & Symbols + .minus, .equal, .backquote, .bracketLeft, .bracketRight, .backslash, + .semicolon, .quote, .comma, .period, .slash, + + // Numpad + .numLock, .numpad0, .numpad1, .numpad2, .numpad3, .numpad4, .numpad5, + .numpad6, .numpad7, .numpad8, .numpad9, .numpadAdd, .numpadSubtract, + .numpadMultiply, .numpadDivide, .numpadDecimal, .numpadEqual, + .numpadEnter, .numpadComma, + + // Media Keys + .audioVolumeUp, .audioVolumeDown, .audioVolumeMute, + + // International Keys + .intlBackslash, .intlRo, .intlYen, + + // Other + .contextMenu + ] + } + + static var caseDisplayRepresentations: [Ghostty.Input.Key : DisplayRepresentation] = [ + // Letters (A-Z) + .a: "A", .b: "B", .c: "C", .d: "D", .e: "E", .f: "F", .g: "G", .h: "H", .i: "I", .j: "J", + .k: "K", .l: "L", .m: "M", .n: "N", .o: "O", .p: "P", .q: "Q", .r: "R", .s: "S", .t: "T", + .u: "U", .v: "V", .w: "W", .x: "X", .y: "Y", .z: "Z", - // Functional Keys - .altLeft: "Left Alt", - .altRight: "Right Alt", - .backspace: "Backspace", - .capsLock: "Caps Lock", - .contextMenu: "Context Menu", - .controlLeft: "Left Control", - .controlRight: "Right Control", - .enter: "Enter", - .metaLeft: "Left Command", - .metaRight: "Right Command", - .shiftLeft: "Left Shift", - .shiftRight: "Right Shift", + // Numbers (0-9) + .digit0: "0", .digit1: "1", .digit2: "2", .digit3: "3", .digit4: "4", + .digit5: "5", .digit6: "6", .digit7: "7", .digit8: "8", .digit9: "9", + + // Common Control Keys .space: "Space", + .enter: "Enter", .tab: "Tab", - .convert: "Convert", - .kanaMode: "Kana Mode", - .nonConvert: "Non Convert", - - // Control Pad Section + .backspace: "Backspace", + .escape: "Escape", .delete: "Delete", - .end: "End", - .help: "Help", - .home: "Home", - .insert: "Insert", - .pageDown: "Page Down", - .pageUp: "Page Up", - // Arrow Pad Section + // Arrow Keys + .arrowUp: "Up Arrow", .arrowDown: "Down Arrow", .arrowLeft: "Left Arrow", .arrowRight: "Right Arrow", - .arrowUp: "Up Arrow", - // Numpad Section + // Navigation Keys + .home: "Home", + .end: "End", + .pageUp: "Page Up", + .pageDown: "Page Down", + .insert: "Insert", + + // Function Keys (F1-F20) + .f1: "F1", .f2: "F2", .f3: "F3", .f4: "F4", .f5: "F5", .f6: "F6", + .f7: "F7", .f8: "F8", .f9: "F9", .f10: "F10", .f11: "F11", .f12: "F12", + .f13: "F13", .f14: "F14", .f15: "F15", .f16: "F16", .f17: "F17", + .f18: "F18", .f19: "F19", .f20: "F20", + + // Modifier Keys + .shiftLeft: "Left Shift", + .shiftRight: "Right Shift", + .controlLeft: "Left Control", + .controlRight: "Right Control", + .altLeft: "Left Alt", + .altRight: "Right Alt", + .metaLeft: "Left Command", + .metaRight: "Right Command", + .capsLock: "Caps Lock", + + // Punctuation & Symbols + .minus: "Minus (-)", + .equal: "Equal (=)", + .backquote: "Backtick (`)", + .bracketLeft: "Left Bracket ([)", + .bracketRight: "Right Bracket (])", + .backslash: "Backslash (\\)", + .semicolon: "Semicolon (;)", + .quote: "Quote (')", + .comma: "Comma (,)", + .period: "Period (.)", + .slash: "Slash (/)", + + // Numpad .numLock: "Num Lock", - .numpad0: "Numpad 0", - .numpad1: "Numpad 1", - .numpad2: "Numpad 2", - .numpad3: "Numpad 3", - .numpad4: "Numpad 4", - .numpad5: "Numpad 5", - .numpad6: "Numpad 6", - .numpad7: "Numpad 7", - .numpad8: "Numpad 8", - .numpad9: "Numpad 9", + .numpad0: "Numpad 0", .numpad1: "Numpad 1", .numpad2: "Numpad 2", + .numpad3: "Numpad 3", .numpad4: "Numpad 4", .numpad5: "Numpad 5", + .numpad6: "Numpad 6", .numpad7: "Numpad 7", .numpad8: "Numpad 8", .numpad9: "Numpad 9", .numpadAdd: "Numpad Add (+)", - .numpadBackspace: "Numpad Backspace", - .numpadClear: "Numpad Clear", - .numpadClearEntry: "Numpad Clear Entry", - .numpadComma: "Numpad Comma", - .numpadDecimal: "Numpad Decimal", - .numpadDivide: "Numpad Divide (÷)", - .numpadEnter: "Numpad Enter", - .numpadEqual: "Numpad Equal", - .numpadMemoryAdd: "Numpad Memory Add", - .numpadMemoryClear: "Numpad Memory Clear", - .numpadMemoryRecall: "Numpad Memory Recall", - .numpadMemoryStore: "Numpad Memory Store", - .numpadMemorySubtract: "Numpad Memory Subtract", - .numpadMultiply: "Numpad Multiply (×)", - .numpadParenLeft: "Numpad Left Parenthesis", - .numpadParenRight: "Numpad Right Parenthesis", .numpadSubtract: "Numpad Subtract (-)", - .numpadSeparator: "Numpad Separator", - .numpadUp: "Numpad Up", - .numpadDown: "Numpad Down", - .numpadRight: "Numpad Right", - .numpadLeft: "Numpad Left", - .numpadBegin: "Numpad Begin", - .numpadHome: "Numpad Home", - .numpadEnd: "Numpad End", - .numpadInsert: "Numpad Insert", - .numpadDelete: "Numpad Delete", - .numpadPageUp: "Numpad Page Up", - .numpadPageDown: "Numpad Page Down", - - // Function Section - .escape: "Escape", - .f1: "F1", - .f2: "F2", - .f3: "F3", - .f4: "F4", - .f5: "F5", - .f6: "F6", - .f7: "F7", - .f8: "F8", - .f9: "F9", - .f10: "F10", - .f11: "F11", - .f12: "F12", - .f13: "F13", - .f14: "F14", - .f15: "F15", - .f16: "F16", - .f17: "F17", - .f18: "F18", - .f19: "F19", - .f20: "F20", - .f21: "F21", - .f22: "F22", - .f23: "F23", - .f24: "F24", - .f25: "F25", - .fn: "Fn", - .fnLock: "Fn Lock", - .printScreen: "Print Screen", - .scrollLock: "Scroll Lock", - .pause: "Pause", + .numpadMultiply: "Numpad Multiply (×)", + .numpadDivide: "Numpad Divide (÷)", + .numpadDecimal: "Numpad Decimal", + .numpadEqual: "Numpad Equal", + .numpadEnter: "Numpad Enter", + .numpadComma: "Numpad Comma", // Media Keys - .browserBack: "Browser Back", - .browserFavorites: "Browser Favorites", - .browserForward: "Browser Forward", - .browserHome: "Browser Home", - .browserRefresh: "Browser Refresh", - .browserSearch: "Browser Search", - .browserStop: "Browser Stop", - .eject: "Eject", - .launchApp1: "Launch App 1", - .launchApp2: "Launch App 2", - .launchMail: "Launch Mail", - .mediaPlayPause: "Media Play/Pause", - .mediaSelect: "Media Select", - .mediaStop: "Media Stop", - .mediaTrackNext: "Media Next Track", - .mediaTrackPrevious: "Media Previous Track", - .power: "Power", - .sleep: "Sleep", + .audioVolumeUp: "Volume Up", .audioVolumeDown: "Volume Down", .audioVolumeMute: "Volume Mute", - .audioVolumeUp: "Volume Up", - .wakeUp: "Wake Up", - // Legacy, Non-standard, and Special Keys - .copy: "Copy", - .cut: "Cut", - .paste: "Paste" + // International Keys + .intlBackslash: "International Backslash", + .intlRo: "International Ro", + .intlYen: "International Yen", + + // Other + .contextMenu: "Context Menu" ] } diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 10e699c1f..88d3f1d09 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -48,6 +48,20 @@ extension Ghostty { } } + /// Send a key event to the terminal. + /// + /// This sends the full key event including modifiers, action type, and text to the terminal. + /// Unlike `sendText`, this method processes keyboard shortcuts, key bindings, and terminal + /// encoding based on the complete key event information. + /// + /// - Parameter event: The key event to send to the terminal + @MainActor + func sendKeyEvent(_ event: Input.KeyEvent) { + event.withCValue { cEvent in + ghostty_surface_key(surface, cEvent) + } + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/InspectorView.swift b/macos/Sources/Ghostty/InspectorView.swift index 491ec86e1..8008e49c2 100644 --- a/macos/Sources/Ghostty/InspectorView.swift +++ b/macos/Sources/Ghostty/InspectorView.swift @@ -337,7 +337,7 @@ extension Ghostty { private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) { guard let inspector = self.inspector else { return } - guard let key = Ghostty.Key(keyCode: event.keyCode) else { return } + guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return } let mods = Ghostty.ghosttyMods(event.modifierFlags) ghostty_inspector_key(inspector, action, key.cKey, mods) } From 4445a9c63701c52906be2c7a4f987939e79fb237 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 13:49:36 -0700 Subject: [PATCH 535/642] macos: add mouse button intent --- .../Features/App Intents/InputIntent.swift | 58 +++++++++ macos/Sources/Ghostty/Ghostty.Input.swift | 117 +++++++++++++++++- macos/Sources/Ghostty/Ghostty.Surface.swift | 26 ++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 15 +-- 4 files changed, 206 insertions(+), 10 deletions(-) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 6d3d60d59..56af10ceb 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -95,6 +95,64 @@ struct KeyEventIntent: AppIntent { } } +// MARK: MouseButtonIntent + +/// App intent to trigger a mouse button event. +struct MouseButtonIntent: AppIntent { + static var title: LocalizedStringResource = "Send Mouse Button Event to Terminal" + + @Parameter( + title: "Button", + description: "The mouse button to press or release.", + default: .left + ) + var button: Ghostty.Input.MouseButton + + @Parameter( + title: "Action", + description: "Whether to press or release the button.", + default: .press + ) + var action: Ghostty.Input.MouseState + + @Parameter( + title: "Modifier(s)", + description: "The modifiers to send with the mouse event.", + default: [] + ) + var mods: [KeyEventMods] + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + // Convert KeyEventMods array to Ghostty.Input.Mods + let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in + result.union(mod.ghosttyMod) + } + + let mouseEvent = Ghostty.Input.MouseButtonEvent( + action: action, + button: button, + mods: ghosttyMods + ) + surface.sendMouseButton(mouseEvent) + + return .result() + } +} + // MARK: Mods enum KeyEventMods: String, AppEnum, CaseIterable { diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index df93017c7..a2d6b104d 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -215,11 +215,126 @@ extension Ghostty.Input.Action: AppEnum { static var caseDisplayRepresentations: [Ghostty.Input.Action : DisplayRepresentation] = [ .release: "Release", - .press: "Press", + .press: "Press", .repeat: "Repeat" ] } +// MARK: Ghostty.Input.MouseEvent + +extension Ghostty.Input { + /// Represents a mouse input event with button state, button type, and modifier keys. + struct MouseButtonEvent { + let action: MouseState + let button: MouseButton + let mods: Mods + + init( + action: MouseState, + button: MouseButton, + mods: Mods = [] + ) { + self.action = action + self.button = button + self.mods = mods + } + + /// Creates a MouseEvent from C enum values. + /// + /// This initializer converts C-style mouse input enums to Swift types. + /// Returns nil if any of the C enum values are invalid or unsupported. + /// + /// - Parameters: + /// - state: The mouse button state (press/release) + /// - button: The mouse button that was pressed/released + /// - mods: The modifier keys held during the mouse event + init?(state: ghostty_input_mouse_state_e, button: ghostty_input_mouse_button_e, mods: ghostty_input_mods_e) { + // Convert state + switch state { + case GHOSTTY_MOUSE_RELEASE: self.action = .release + case GHOSTTY_MOUSE_PRESS: self.action = .press + default: return nil + } + + // Convert button + switch button { + case GHOSTTY_MOUSE_UNKNOWN: self.button = .unknown + case GHOSTTY_MOUSE_LEFT: self.button = .left + case GHOSTTY_MOUSE_RIGHT: self.button = .right + case GHOSTTY_MOUSE_MIDDLE: self.button = .middle + default: return nil + } + + // Convert modifiers + self.mods = Mods(cMods: mods) + } + } +} + +// MARK: Ghostty.Input.MouseState + +extension Ghostty.Input { + /// `ghostty_input_mouse_state_e` + enum MouseState: String, CaseIterable { + case release + case press + + var cMouseState: ghostty_input_mouse_state_e { + switch self { + case .release: GHOSTTY_MOUSE_RELEASE + case .press: GHOSTTY_MOUSE_PRESS + } + } + } +} + +extension Ghostty.Input.MouseState: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse State") + + static var caseDisplayRepresentations: [Ghostty.Input.MouseState : DisplayRepresentation] = [ + .release: "Release", + .press: "Press" + ] +} + +// MARK: Ghostty.Input.MouseButton + +extension Ghostty.Input { + /// `ghostty_input_mouse_button_e` + enum MouseButton: String, CaseIterable { + case unknown + case left + case right + case middle + + var cMouseButton: ghostty_input_mouse_button_e { + switch self { + case .unknown: GHOSTTY_MOUSE_UNKNOWN + case .left: GHOSTTY_MOUSE_LEFT + case .right: GHOSTTY_MOUSE_RIGHT + case .middle: GHOSTTY_MOUSE_MIDDLE + } + } + } +} + +extension Ghostty.Input.MouseButton: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Mouse Button") + + static var caseDisplayRepresentations: [Ghostty.Input.MouseButton : DisplayRepresentation] = [ + .unknown: "Unknown", + .left: "Left", + .right: "Right", + .middle: "Middle" + ] + + static var allCases: [Ghostty.Input.MouseButton] = [ + .left, + .right, + .middle, + ] +} + // MARK: Ghostty.Input.Mods extension Ghostty.Input { diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 88d3f1d09..2cc85f1e4 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -62,6 +62,32 @@ extension Ghostty { } } + /// Whether the terminal has captured mouse input. + /// + /// When the mouse is captured, the terminal application is receiving mouse events + /// directly rather than the host system handling them. This typically occurs when + /// a terminal application enables mouse reporting mode. + @MainActor + var mouseCaptured: Bool { + ghostty_surface_mouse_captured(surface) + } + + /// Send a mouse button event to the terminal. + /// + /// This sends a complete mouse button event including the button state (press/release), + /// which button was pressed, and any modifier keys that were held during the event. + /// The terminal processes this event according to its mouse handling configuration. + /// + /// - Parameter event: The mouse button event to send to the terminal + @MainActor + func sendMouseButton(_ event: Input.MouseButtonEvent) { + ghostty_surface_mouse_button( + surface, + event.action.cMouseState, + event.button.cMouseButton, + event.mods.cMods) + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 2e7cf499b..d987b80be 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -1312,8 +1312,8 @@ extension Ghostty { // In this case, AppKit calls menu BEFORE calling any mouse events. // If mouse capturing is enabled then we never show the context menu // so that we can handle ctrl+left-click in the terminal app. - guard let surface = self.surface else { return nil } - if ghostty_surface_mouse_captured(surface) { + guard let surfaceModel else { return nil } + if surfaceModel.mouseCaptured { return nil } @@ -1323,13 +1323,10 @@ extension Ghostty { // // Note this never sounds a right mouse up event but that's the // same as normal right-click with capturing disabled from AppKit. - let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_button( - surface, - GHOSTTY_MOUSE_PRESS, - GHOSTTY_MOUSE_RIGHT, - mods - ) + surfaceModel.sendMouseButton(.init( + action: .press, + button: .right, + mods: .init(nsFlags: event.modifierFlags))) default: return nil From bc134016f7d978036103cd3c4532ab9636e445ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 14:07:09 -0700 Subject: [PATCH 536/642] macos: move mousePos and mousScroll to Ghostty.Surface --- macos/Sources/Ghostty/Ghostty.Input.swift | 120 ++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Surface.swift | 32 +++++ .../Sources/Ghostty/SurfaceView_AppKit.swift | 70 +++++----- 3 files changed, 183 insertions(+), 39 deletions(-) diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index a2d6b104d..bbc83c5e5 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -269,6 +269,40 @@ extension Ghostty.Input { self.mods = Mods(cMods: mods) } } + + /// Represents a mouse position/movement event with coordinates and modifier keys. + struct MousePosEvent { + let x: Double + let y: Double + let mods: Mods + + init( + x: Double, + y: Double, + mods: Mods = [] + ) { + self.x = x + self.y = y + self.mods = mods + } + } + + /// Represents a mouse scroll event with scroll deltas and modifier keys. + struct MouseScrollEvent { + let x: Double + let y: Double + let mods: ScrollMods + + init( + x: Double, + y: Double, + mods: ScrollMods = .init(rawValue: 0) + ) { + self.x = x + self.y = y + self.mods = mods + } + } } // MARK: Ghostty.Input.MouseState @@ -335,6 +369,92 @@ extension Ghostty.Input.MouseButton: AppEnum { ] } +// MARK: Ghostty.Input.ScrollMods + +extension Ghostty.Input { + /// `ghostty_input_scroll_mods_t` - Scroll event modifiers + /// + /// This is a packed bitmask that contains precision and momentum information + /// for scroll events, matching the Zig `ScrollMods` packed struct. + struct ScrollMods { + let rawValue: Int32 + + /// True if this is a high-precision scroll event (e.g., trackpad, Magic Mouse) + var precision: Bool { + rawValue & 0b0000_0001 != 0 + } + + /// The momentum phase of the scroll event for inertial scrolling + var momentum: Momentum { + let momentumBits = (rawValue >> 1) & 0b0000_0111 + return Momentum(rawValue: UInt8(momentumBits)) ?? .none + } + + init(precision: Bool = false, momentum: Momentum = .none) { + var value: Int32 = 0 + if precision { + value |= 0b0000_0001 + } + value |= Int32(momentum.rawValue) << 1 + self.rawValue = value + } + + init(rawValue: Int32) { + self.rawValue = rawValue + } + + var cScrollMods: ghostty_input_scroll_mods_t { + rawValue + } + } +} + +// MARK: Ghostty.Input.Momentum + +extension Ghostty.Input { + /// `ghostty_input_mouse_momentum_e` - Momentum phase for scroll events + enum Momentum: UInt8, CaseIterable { + case none = 0 + case began = 1 + case stationary = 2 + case changed = 3 + case ended = 4 + case cancelled = 5 + case mayBegin = 6 + + var cMomentum: ghostty_input_mouse_momentum_e { + switch self { + case .none: GHOSTTY_MOUSE_MOMENTUM_NONE + case .began: GHOSTTY_MOUSE_MOMENTUM_BEGAN + case .stationary: GHOSTTY_MOUSE_MOMENTUM_STATIONARY + case .changed: GHOSTTY_MOUSE_MOMENTUM_CHANGED + case .ended: GHOSTTY_MOUSE_MOMENTUM_ENDED + case .cancelled: GHOSTTY_MOUSE_MOMENTUM_CANCELLED + case .mayBegin: GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN + } + } + } +} + +#if canImport(AppKit) +import AppKit + +extension Ghostty.Input.Momentum { + /// Create a Momentum from an NSEvent.Phase + init(_ phase: NSEvent.Phase) { + switch phase { + case .began: self = .began + case .stationary: self = .stationary + case .changed: self = .changed + case .ended: self = .ended + case .cancelled: self = .cancelled + case .mayBegin: self = .mayBegin + default: self = .none + } + } +} +#endif + // MARK: Ghostty.Input.Mods extension Ghostty.Input { diff --git a/macos/Sources/Ghostty/Ghostty.Surface.swift b/macos/Sources/Ghostty/Ghostty.Surface.swift index 2cc85f1e4..c7198e147 100644 --- a/macos/Sources/Ghostty/Ghostty.Surface.swift +++ b/macos/Sources/Ghostty/Ghostty.Surface.swift @@ -88,6 +88,38 @@ extension Ghostty { event.mods.cMods) } + /// Send a mouse position event to the terminal. + /// + /// This reports the current mouse position to the terminal, which may be used + /// for mouse tracking, hover effects, or other position-dependent features. + /// The terminal will only receive these events if mouse reporting is enabled. + /// + /// - Parameter event: The mouse position event to send to the terminal + @MainActor + func sendMousePos(_ event: Input.MousePosEvent) { + ghostty_surface_mouse_pos( + surface, + event.x, + event.y, + event.mods.cMods) + } + + /// Send a mouse scroll event to the terminal. + /// + /// This sends scroll wheel input to the terminal with delta values for both + /// horizontal and vertical scrolling, along with precision and momentum information. + /// The terminal processes this according to its scroll handling configuration. + /// + /// - Parameter event: The mouse scroll event to send to the terminal + @MainActor + func sendMouseScroll(_ event: Input.MouseScrollEvent) { + ghostty_surface_mouse_scroll( + surface, + event.x, + event.y, + event.mods.cScrollMods) + } + /// Perform a keybinding action. /// /// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4` diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index d987b80be..83a8da29c 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -808,19 +808,23 @@ extension Ghostty { override func mouseEntered(with event: NSEvent) { super.mouseEntered(with: event) - guard let surface = self.surface else { return } + guard let surfaceModel else { return } // On mouse enter we need to reset our cursor position. This is // super important because we set it to -1/-1 on mouseExit and // lots of mouse logic (i.e. whether to send mouse reports) depend // on the position being in the viewport if it is. let pos = self.convert(event.locationInWindow, from: nil) - let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods) + let mouseEvent = Ghostty.Input.MousePosEvent( + x: pos.x, + y: frame.height - pos.y, + mods: .init(nsFlags: event.modifierFlags) + ) + surfaceModel.sendMousePos(mouseEvent) } override func mouseExited(with event: NSEvent) { - guard let surface = self.surface else { return } + guard let surfaceModel else { return } // If the mouse is being dragged then we don't have to emit // this because we get mouse drag events even if we've already @@ -830,17 +834,25 @@ extension Ghostty { } // Negative values indicate cursor has left the viewport - let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_pos(surface, -1, -1, mods) + let mouseEvent = Ghostty.Input.MousePosEvent( + x: -1, + y: -1, + mods: .init(nsFlags: event.modifierFlags) + ) + surfaceModel.sendMousePos(mouseEvent) } override func mouseMoved(with event: NSEvent) { - guard let surface = self.surface else { return } + guard let surfaceModel else { return } // Convert window position to view position. Note (0, 0) is bottom left. let pos = self.convert(event.locationInWindow, from: nil) - let mods = Ghostty.ghosttyMods(event.modifierFlags) - ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods) + let mouseEvent = Ghostty.Input.MousePosEvent( + x: pos.x, + y: frame.height - pos.y, + mods: .init(nsFlags: event.modifierFlags) + ) + surfaceModel.sendMousePos(mouseEvent) // Handle focus-follows-mouse if let window, @@ -866,16 +878,13 @@ extension Ghostty { } override func scrollWheel(with event: NSEvent) { - guard let surface = self.surface else { return } - - // Builds up the "input.ScrollMods" bitmask - var mods: Int32 = 0 + guard let surfaceModel else { return } var x = event.scrollingDeltaX var y = event.scrollingDeltaY - if event.hasPreciseScrollingDeltas { - mods = 1 - + let precision = event.hasPreciseScrollingDeltas + + if precision { // We do a 2x speed multiplier. This is subjective, it "feels" better to me. x *= 2; y *= 2; @@ -883,29 +892,12 @@ extension Ghostty { // TODO(mitchellh): do we have to scale the x/y here by window scale factor? } - // Determine our momentum value - var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE - switch (event.momentumPhase) { - case .began: - momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN - case .stationary: - momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY - case .changed: - momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED - case .ended: - momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED - case .cancelled: - momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED - case .mayBegin: - momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN - default: - break - } - - // Pack our momentum value into the mods bitmask - mods |= Int32(momentum.rawValue) << 1 - - ghostty_surface_mouse_scroll(surface, x, y, mods) + let scrollEvent = Ghostty.Input.MouseScrollEvent( + x: x, + y: y, + mods: .init(precision: precision, momentum: .init(event.momentumPhase)) + ) + surfaceModel.sendMouseScroll(scrollEvent) } override func pressureChange(with event: NSEvent) { From 2df301e2fb6abd5d37fef3f7d8808d24977ad89e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 14:14:09 -0700 Subject: [PATCH 537/642] macos: mouse pos and scroll intents --- .../Features/App Intents/InputIntent.swift | 116 ++++++++++++++++++ macos/Sources/Ghostty/Ghostty.Input.swift | 14 +++ 2 files changed, 130 insertions(+) diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index 56af10ceb..b8c248fe3 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -153,6 +153,122 @@ struct MouseButtonIntent: AppIntent { } } +/// App intent to send a mouse position event. +struct MousePosIntent: AppIntent { + static var title: LocalizedStringResource = "Send Mouse Position Event to Terminal" + static var description = IntentDescription("Send a mouse position event to the terminal. This reports the cursor position for mouse tracking.") + + @Parameter( + title: "X Position", + description: "The horizontal position of the mouse cursor in pixels.", + default: 0 + ) + var x: Double + + @Parameter( + title: "Y Position", + description: "The vertical position of the mouse cursor in pixels.", + default: 0 + ) + var y: Double + + @Parameter( + title: "Modifier(s)", + description: "The modifiers to send with the mouse position event.", + default: [] + ) + var mods: [KeyEventMods] + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + // Convert KeyEventMods array to Ghostty.Input.Mods + let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in + result.union(mod.ghosttyMod) + } + + let mousePosEvent = Ghostty.Input.MousePosEvent( + x: x, + y: y, + mods: ghosttyMods + ) + surface.sendMousePos(mousePosEvent) + + return .result() + } +} + +/// App intent to send a mouse scroll event. +struct MouseScrollIntent: AppIntent { + static var title: LocalizedStringResource = "Send Mouse Scroll Event to Terminal" + static var description = IntentDescription("Send a mouse scroll event to the terminal with configurable precision and momentum.") + + @Parameter( + title: "X Scroll Delta", + description: "The horizontal scroll amount.", + default: 0 + ) + var x: Double + + @Parameter( + title: "Y Scroll Delta", + description: "The vertical scroll amount.", + default: 0 + ) + var y: Double + + @Parameter( + title: "High Precision", + description: "Whether this is a high-precision scroll event (e.g., from trackpad).", + default: false + ) + var precision: Bool + + @Parameter( + title: "Momentum Phase", + description: "The momentum phase for inertial scrolling.", + default: Ghostty.Input.Momentum.none + ) + var momentum: Ghostty.Input.Momentum + + @Parameter( + title: "Terminal", + description: "The terminal to scope this action to." + ) + var terminal: TerminalEntity + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = [.background, .foreground] + + @MainActor + func perform() async throws -> some IntentResult { + guard let surface = terminal.surfaceModel else { + throw GhosttyIntentError.surfaceNotFound + } + + let scrollEvent = Ghostty.Input.MouseScrollEvent( + x: x, + y: y, + mods: .init(precision: precision, momentum: momentum) + ) + surface.sendMouseScroll(scrollEvent) + + return .result() + } +} + // MARK: Mods enum KeyEventMods: String, AppEnum, CaseIterable { diff --git a/macos/Sources/Ghostty/Ghostty.Input.swift b/macos/Sources/Ghostty/Ghostty.Input.swift index bbc83c5e5..e05911c06 100644 --- a/macos/Sources/Ghostty/Ghostty.Input.swift +++ b/macos/Sources/Ghostty/Ghostty.Input.swift @@ -436,6 +436,20 @@ extension Ghostty.Input { } } +extension Ghostty.Input.Momentum: AppEnum { + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Scroll Momentum") + + static var caseDisplayRepresentations: [Ghostty.Input.Momentum : DisplayRepresentation] = [ + .none: "None", + .began: "Began", + .stationary: "Stationary", + .changed: "Changed", + .ended: "Ended", + .cancelled: "Cancelled", + .mayBegin: "May Begin" + ] +} + #if canImport(AppKit) import AppKit From 0a27aef508ceb5b3376a77ac9fbb6dc6f30020dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 14:19:37 -0700 Subject: [PATCH 538/642] README: note Xcode 26 requirement --- .github/workflows/test.yml | 41 -------------------------------------- README.md | 22 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 41 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2eca0a41e..4d09603f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,6 @@ jobs: - build-nix - build-snap - build-macos - - build-macos-sequoia-stable - build-macos-tahoe - build-macos-matrix - build-windows @@ -310,46 +309,6 @@ jobs: cd macos xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" - build-macos-sequoia-stable: - runs-on: namespace-profile-ghostty-macos-sequoia - needs: test - steps: - - name: Checkout code - uses: actions/checkout@v4 - - # Install Nix and use that to run our tests so our environment matches exactly. - - uses: cachix/install-nix-action@v31 - with: - nix_path: nixpkgs=channel:nixos-unstable - - uses: cachix/cachix-action@v16 - with: - name: ghostty - authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - - - name: Xcode Select - run: sudo xcode-select -s /Applications/Xcode_16.4.app - - - name: get the Zig deps - id: deps - run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT - - # GhosttyKit is the framework that is built from Zig for our native - # Mac app to access. - - name: Build GhosttyKit - run: nix develop -c zig build --system ${{ steps.deps.outputs.deps }} - - # The native app is built with native Xcode tooling. This also does - # codesigning. IMPORTANT: this must NOT run in a Nix environment. - # Nix breaks xcodebuild so this has to be run outside. - - name: Build Ghostty.app - run: cd macos && xcodebuild -target Ghostty - - # Build the iOS target without code signing just to verify it works. - - name: Build Ghostty iOS - run: | - cd macos - xcodebuild -target Ghostty-iOS "CODE_SIGNING_ALLOWED=NO" - build-macos-tahoe: runs-on: namespace-profile-ghostty-macos-tahoe needs: test diff --git a/README.md b/README.md index d5c9dba02..b59964e61 100644 --- a/README.md +++ b/README.md @@ -224,6 +224,28 @@ macOS users don't require any additional dependencies. > source tarballs, see the > [website](http://ghostty.org/docs/install/build). +### Xcode Version and SDKs + +Building the Ghostty macOS app requires that Xcode, the macOS SDK, +and the iOS SDK are all installed. + +A common issue is that the incorrect version of Xcode is either +installed or selected. Use the `xcode-select` command to +ensure that the correct version of Xcode is selected: + +```shell-session +sudo xcode-select --switch /Applications/Xcode-beta.app +``` + +> [!IMPORTANT] +> +> Main branch development of Ghostty is preparing for the next major +> macOS release, Tahoe (macOS 26). Therefore, the main branch requires +> **Xcode 26 and the macOS 26 SDK**. +> +> You do not need to be running on macOS 26 to build Ghostty, you can +> still use Xcode 26 beta on macOS 15 stable. + ### Linting #### Prettier From f096675eaf389871e53140d0037a6ce208307654 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 19 Jun 2025 20:00:24 -0700 Subject: [PATCH 539/642] macos: Close Terminal Intent --- macos/Ghostty.xcodeproj/project.pbxproj | 4 + .../App Intents/CloseTerminalIntent.swift | 38 ++++++++++ .../QuickTerminalController.swift | 6 +- .../Terminal/BaseTerminalController.swift | 73 +++++++++++-------- .../Terminal/TerminalController.swift | 4 +- 5 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 macos/Sources/Features/App Intents/CloseTerminalIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index bbb34820f..acf4b0e43 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; }; 9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; }; A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; }; + A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; @@ -149,6 +150,7 @@ 857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = ""; }; 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 = ""; }; + A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; }; A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; }; @@ -625,6 +627,7 @@ isa = PBXGroup; children = ( A5E408412E0453370035FEAC /* Entities */, + A511940E2E050590007258CC /* CloseTerminalIntent.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, @@ -793,6 +796,7 @@ A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */, A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */, A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */, + A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */, A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */, A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */, A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift new file mode 100644 index 000000000..18079650b --- /dev/null +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -0,0 +1,38 @@ +import AppKit +import AppIntents +import GhosttyKit + +struct CloseTerminalIntent: AppIntent { + static var title: LocalizedStringResource = "Close Terminal" + static var description = IntentDescription("Close an existing terminal.") + + @Parameter( + title: "Terminal", + description: "The terminal to close.", + ) + var terminal: TerminalEntity + + @Parameter( + title: "Command", + description: "Command to execute instead of the default shell.", + default: true + ) + var confirm: Bool + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + @MainActor + func perform() async throws -> some IntentResult { + guard let surfaceView = terminal.surfaceView else { + throw GhosttyIntentError.surfaceNotFound + } + + guard let controller = surfaceView.window?.windowController as? BaseTerminalController else { + return .result() + } + + controller.closeSurface(surfaceView, withConfirmation: confirm) + return .result() + } +} diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 28dea9579..80b0c9413 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -218,19 +218,19 @@ class QuickTerminalController: BaseTerminalController { } } - override func closeSurfaceNode( + override func closeSurface( _ node: SplitTree.Node, withConfirmation: Bool = true ) { // If this isn't the root then we're dealing with a split closure. if surfaceTree.root != node { - super.closeSurfaceNode(node, withConfirmation: withConfirmation) + super.closeSurface(node, withConfirmation: withConfirmation) return } // If this isn't a final leaf then we're dealing with a split closure guard case .leaf(let surface) = node else { - super.closeSurfaceNode(node, withConfirmation: withConfirmation) + super.closeSurface(node, withConfirmation: withConfirmation) return } diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index 81b7d32b6..c93a9450d 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -300,6 +300,46 @@ class BaseTerminalController: NSWindowController, self.alert = alert } + /// Close a surface from a view. + func closeSurface( + _ view: Ghostty.SurfaceView, + withConfirmation: Bool = true + ) { + guard let node = surfaceTree.root?.node(view: view) else { return } + closeSurface(node, withConfirmation: withConfirmation) + } + + /// Close a surface node (which may contain splits), requesting confirmation if necessary. + /// + /// This will also insert the proper undo stack information in. + func closeSurface( + _ node: SplitTree.Node, + withConfirmation: Bool = true + ) { + // This node must be part of our tree + guard surfaceTree.contains(node) else { return } + + // If the child process is not alive, then we exit immediately + guard withConfirmation else { + removeSurfaceNode(node) + return + } + + // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog + // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that + // confirmationDialog allows the user to Cmd-W close the alert, but when doing + // so SwiftUI does not update any of the bindings to note that window is no longer + // being shown, and provides no callback to detect this. + confirmClose( + messageText: "Close Terminal?", + informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." + ) { [weak self] in + if let self { + self.removeSurfaceNode(node) + } + } + } + // MARK: Split Tree Management /// Find the next surface to focus when a node is being closed. @@ -460,42 +500,11 @@ class BaseTerminalController: NSWindowController, @objc private func ghosttyDidCloseSurface(_ notification: Notification) { guard let target = notification.object as? Ghostty.SurfaceView else { return } guard let node = surfaceTree.root?.node(view: target) else { return } - closeSurfaceNode( + closeSurface( node, withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false) } - /// Close a surface node (which may contain splits), requesting confirmation if necessary. - /// - /// This will also insert the proper undo stack information in. - func closeSurfaceNode( - _ node: SplitTree.Node, - withConfirmation: Bool = true - ) { - // This node must be part of our tree - guard surfaceTree.contains(node) else { return } - - // If the child process is not alive, then we exit immediately - guard withConfirmation else { - removeSurfaceNode(node) - return - } - - // Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog - // due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that - // confirmationDialog allows the user to Cmd-W close the alert, but when doing - // so SwiftUI does not update any of the bindings to note that window is no longer - // being shown, and provides no callback to detect this. - confirmClose( - messageText: "Close Terminal?", - informativeText: "The terminal still has a running process. If you close the terminal the process will be killed." - ) { [weak self] in - if let self { - self.removeSurfaceNode(node) - } - } - } - @objc private func ghosttyDidNewSplit(_ notification: Notification) { // The target must be within our tree guard let oldView = notification.object as? Ghostty.SurfaceView else { return } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index a224c9248..77eb079ad 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -519,13 +519,13 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr } /// This is called anytime a node in the surface tree is being removed. - override func closeSurfaceNode( + override func closeSurface( _ node: SplitTree.Node, withConfirmation: Bool = true ) { // If this isn't the root then we're dealing with a split closure. if surfaceTree.root != node { - super.closeSurfaceNode(node, withConfirmation: withConfirmation) + super.closeSurface(node, withConfirmation: withConfirmation) return } From 2c1e83ba2fd621d532ea0e7e4746f3f7ae19c069 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 07:03:40 -0700 Subject: [PATCH 540/642] macos: intent to open quick terminal --- macos/Ghostty.xcodeproj/project.pbxproj | 4 +++ macos/Sources/App/macOS/AppDelegate.swift | 15 ++++------ .../App Intents/Entities/TerminalEntity.swift | 27 +++++++++++++++++- .../App Intents/QuickTerminalIntent.swift | 28 +++++++++++++++++++ .../QuickTerminalController.swift | 6 +++- 5 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 macos/Sources/Features/App Intents/QuickTerminalIntent.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index acf4b0e43..6b0cfd6f8 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 */; }; A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; }; + A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; @@ -151,6 +152,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 = ""; }; A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = ""; }; + A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; }; A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; }; @@ -630,6 +632,7 @@ A511940E2E050590007258CC /* CloseTerminalIntent.swift */, A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */, A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */, + A51194102E05A480007258CC /* QuickTerminalIntent.swift */, A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */, A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, @@ -806,6 +809,7 @@ A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */, A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */, A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */, + A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */, C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */, A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */, A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */, diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 7336f18d6..4ffb9efa4 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -92,7 +92,10 @@ class AppDelegate: NSObject, lazy var undoManager = ExpiringUndoManager() /// Our quick terminal. This starts out uninitialized and only initializes if used. - private var quickController: QuickTerminalController? = nil + private(set) lazy var quickController = QuickTerminalController( + ghostty, + position: derivedConfig.quickTerminalPosition + ) /// Manages updates let updaterController: SPUStandardUpdaterController @@ -286,7 +289,7 @@ class AppDelegate: NSObject, // NOTE(mitchellh): I don't think we need this check at all anymore. I'm keeping it // here because I don't want to remove it in a patch release cycle but we should // target removing it soon. - if (self.quickController == nil && windows.allSatisfy { !$0.isVisible }) { + if (windows.allSatisfy { !$0.isVisible }) { return .terminateNow } @@ -919,14 +922,6 @@ class AppDelegate: NSObject, } @IBAction func toggleQuickTerminal(_ sender: Any) { - if quickController == nil { - quickController = QuickTerminalController( - ghostty, - position: derivedConfig.quickTerminalPosition - ) - } - - guard let quickController = self.quickController else { return } quickController.toggle() } diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 750512d02..1fb69f1f8 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -11,6 +11,9 @@ struct TerminalEntity: AppEntity { @Property(title: "Working Directory") var workingDirectory: String? + @Property(title: "Kind") + var kind: Kind + @MainActor @DeferredProperty(title: "Full Contents") @available(macOS 26.0, *) @@ -67,6 +70,27 @@ struct TerminalEntity: AppEntity { self.title = view.title self.workingDirectory = view.pwd self.screenshot = view.screenshot() + + // Determine the kind based on the window controller type + if view.window?.windowController is QuickTerminalController { + self.kind = .quick + } else { + self.kind = .normal + } + } +} + +extension TerminalEntity { + enum Kind: String, AppEnum { + case normal + case quick + + static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind") + + static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [ + .normal: .init(title: "Normal"), + .quick: .init(title: "Quick") + ] } } @@ -101,7 +125,8 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { @MainActor var all: [Ghostty.SurfaceView] { - // Find all of our terminal windows (includes quick terminal) + // Find all of our terminal windows. This will include the quick terminal + // but only if it was previously opened. let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift new file mode 100644 index 000000000..ee2761217 --- /dev/null +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -0,0 +1,28 @@ +import AppKit +import AppIntents + +struct QuickTerminalIntent: AppIntent { + static var title: LocalizedStringResource = "Open the Quick Terminal" + static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.") + + @available(macOS 26.0, *) + static var supportedModes: IntentModes = .background + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> { + guard let delegate = NSApp.delegate as? AppDelegate else { + throw GhosttyIntentError.appUnavailable + } + + // This is safe to call even if it is already shown. + let c = delegate.quickController + c.animateIn() + + // Grab all our terminals + let terminals = c.surfaceTree.root?.leaves().map { + TerminalEntity($0) + } ?? [] + + return .result(value: terminals) + } +} diff --git a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift index 80b0c9413..3bd8bc18f 100644 --- a/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift +++ b/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift @@ -42,7 +42,11 @@ class QuickTerminalController: BaseTerminalController { ) { self.position = position self.derivedConfig = DerivedConfig(ghostty.config) - super.init(ghostty, baseConfig: base, surfaceTree: tree) + + // Important detail here: we initialize with an empty surface tree so + // that we don't start a terminal process. This gets started when the + // first terminal is shown in `animateIn`. + super.init(ghostty, baseConfig: base, surfaceTree: .init()) // Setup our notifications for behaviors let center = NotificationCenter.default From e6c24fbf0a634a28d8c079948d982636f00cf197 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 07:22:12 -0700 Subject: [PATCH 541/642] macos: remove confirmation option for close terminal --- .../Features/App Intents/CloseTerminalIntent.swift | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 18079650b..4de415494 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -12,13 +12,6 @@ struct CloseTerminalIntent: AppIntent { ) var terminal: TerminalEntity - @Parameter( - title: "Command", - description: "Command to execute instead of the default shell.", - default: true - ) - var confirm: Bool - @available(macOS 26.0, *) static var supportedModes: IntentModes = .background @@ -32,7 +25,7 @@ struct CloseTerminalIntent: AppIntent { return .result() } - controller.closeSurface(surfaceView, withConfirmation: confirm) + controller.closeSurface(surfaceView, withConfirmation: false) return .result() } } From f8bc9b547c2a7e37de80cad6709936852126c46e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 10:09:01 -0700 Subject: [PATCH 542/642] macos: support env vars for surface config, clean up surface config --- include/ghostty.h | 10 +- macos/Ghostty.xcodeproj/project.pbxproj | 4 + macos/Sources/Ghostty/SurfaceView.swift | 72 ++++++++++++--- .../Sources/Ghostty/SurfaceView_AppKit.swift | 6 +- macos/Sources/Ghostty/SurfaceView_UIKit.swift | 6 +- .../Helpers/Extensions/Array+Extension.swift | 25 +++++ .../Extensions/Optional+Extension.swift | 10 ++ src/apprt/embedded.zig | 92 ++++++++++++------- src/config/Config.zig | 5 + 9 files changed, 180 insertions(+), 50 deletions(-) create mode 100644 macos/Sources/Helpers/Extensions/Optional+Extension.swift diff --git a/include/ghostty.h b/include/ghostty.h index fc2c915cb..0c5a63448 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -385,6 +385,11 @@ typedef struct { bool rectangle; } ghostty_selection_s; +typedef struct { + const char* key; + const char* value; +} ghostty_env_var_s; + typedef struct { void* nsview; } ghostty_platform_macos_s; @@ -406,6 +411,8 @@ typedef struct { float font_size; const char* working_directory; const char* command; + ghostty_env_var_s* env_vars; + size_t env_var_count; } ghostty_surface_config_s; typedef struct { @@ -807,7 +814,8 @@ void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e); ghostty_surface_config_s ghostty_surface_config_new(); -ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*); +ghostty_surface_t ghostty_surface_new(ghostty_app_t, + const ghostty_surface_config_s*); void ghostty_surface_free(ghostty_surface_t); void* ghostty_surface_userdata(ghostty_surface_t); ghostty_app_t ghostty_surface_app(ghostty_surface_t); diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index 6b0cfd6f8..a64e6038e 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+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 */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; @@ -153,6 +154,7 @@ A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+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 = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; }; A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; }; @@ -506,6 +508,7 @@ A586366E2DF25D8300E04A10 /* Duration+Extension.swift */, A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */, A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */, + A51194122E05D003007258CC /* Optional+Extension.swift */, C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */, A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */, A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */, @@ -786,6 +789,7 @@ CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */, A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */, A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */, + A51194132E05D006007258CC /* Optional+Extension.swift in Sources */, A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */, C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */, A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */, diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 371e4ff41..2f0623b79 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -418,18 +418,36 @@ extension Ghostty { /// Explicit command to set var command: String? = nil + + /// Environment variables to set for the terminal + var environmentVariables: [String: String] = [:] init() {} init(from config: ghostty_surface_config_s) { self.fontSize = config.font_size - self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8) - self.command = String.init(cString: config.command, encoding: .utf8) + if let workingDirectory = config.working_directory { + self.workingDirectory = String.init(cString: workingDirectory, encoding: .utf8) + } + if let command = config.command { + self.command = String.init(cString: command, encoding: .utf8) + } + + // Convert the C env vars to Swift dictionary + if config.env_var_count > 0, let envVars = config.env_vars { + for i in 0.. ghostty_surface_config_s { + /// Provides a C-compatible ghostty configuration within a closure. The configuration + /// and all its string pointers are only valid within the closure. + func withCValue(view: SurfaceView, _ body: (inout ghostty_surface_config_s) throws -> T) rethrows -> T { var config = ghostty_surface_config_new() config.userdata = Unmanaged.passUnretained(view).toOpaque() #if os(macOS) @@ -438,7 +456,6 @@ extension Ghostty { nsview: Unmanaged.passUnretained(view).toOpaque() )) config.scale_factor = NSScreen.main!.backingScaleFactor - #elseif os(iOS) config.platform_tag = GHOSTTY_PLATFORM_IOS config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s( @@ -453,15 +470,42 @@ extension Ghostty { #error("unsupported target") #endif - if let fontSize = fontSize { config.font_size = fontSize } - if let workingDirectory = workingDirectory { - config.working_directory = (workingDirectory as NSString).utf8String - } - if let command = command { - config.command = (command as NSString).utf8String - } + // Zero is our default value that means to inherit the font size. + config.font_size = fontSize ?? 0 - return config + // Use withCString to ensure strings remain valid for the duration of the closure + return try workingDirectory.withCString { cWorkingDir in + config.working_directory = cWorkingDir + + return try command.withCString { cCommand in + config.command = cCommand + + // Convert dictionary to arrays for easier processing + let keys = Array(environmentVariables.keys) + let values = Array(environmentVariables.values) + + // Create C strings for all keys and values + return try keys.withCStrings { keyCStrings in + return try values.withCStrings { valueCStrings in + // Create array of ghostty_env_var_s + var envVars = Array() + envVars.reserveCapacity(environmentVariables.count) + for i in 0..(_ body: ([UnsafePointer?]) throws -> T) rethrows -> T { + // Handle empty array + if isEmpty { + return try body([]) + } + + // Recursive helper to process strings + func helper(index: Int, accumulated: [UnsafePointer?], body: ([UnsafePointer?]) throws -> T) rethrows -> T { + if index == count { + return try body(accumulated) + } else { + return try self[index].withCString { cStr in + var newAccumulated = accumulated + newAccumulated.append(cStr) + return try helper(index: index + 1, accumulated: newAccumulated, body: body) + } + } + } + + return try helper(index: 0, accumulated: [], body: body) + } +} diff --git a/macos/Sources/Helpers/Extensions/Optional+Extension.swift b/macos/Sources/Helpers/Extensions/Optional+Extension.swift new file mode 100644 index 000000000..a844c0fe9 --- /dev/null +++ b/macos/Sources/Helpers/Extensions/Optional+Extension.swift @@ -0,0 +1,10 @@ +extension Optional where Wrapped == String { + /// Executes a closure with a C string pointer, handling nil gracefully. + func withCString(_ body: (UnsafePointer?) throws -> T) rethrows -> T { + if let string = self { + return try string.withCString(body) + } else { + return try body(nil) + } + } +} diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index 01e287d16..02f143985 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -376,6 +376,14 @@ pub const PlatformTag = enum(c_int) { ios = 2, }; +pub const EnvVar = extern struct { + /// The name of the environment variable. + key: [*:0]const u8, + + /// The value of the environment variable. + value: [*:0]const u8, +}; + pub const Surface = struct { app: *App, platform: Platform, @@ -407,7 +415,7 @@ pub const Surface = struct { font_size: f32 = 0, /// The working directory to load into. - working_directory: [*:0]const u8 = "", + working_directory: ?[*:0]const u8 = null, /// The command to run in the new surface. If this is set then /// the "wait-after-command" option is also automatically set to true, @@ -417,7 +425,11 @@ pub const Surface = struct { /// despite Ghostty allowing directly executed commands via config. /// This is a legacy thing and we should probably change it in the /// future once we have a concrete use case. - command: [*:0]const u8 = "", + command: ?[*:0]const u8 = null, + + /// Extra environment variables to set for the surface. + env_vars: ?[*]EnvVar = null, + env_var_count: usize = 0, }; pub fn init(self: *Surface, app: *App, opts: Options) !void { @@ -443,41 +455,59 @@ pub const Surface = struct { defer config.deinit(); // If we have a working directory from the options then we set it. - const wd = std.mem.sliceTo(opts.working_directory, 0); - if (wd.len > 0) wd: { - var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| { - log.warn( - "error opening requested working directory dir={s} err={}", - .{ wd, err }, - ); - break :wd; - }; - defer dir.close(); + if (opts.working_directory) |c_wd| { + const wd = std.mem.sliceTo(c_wd, 0); + if (wd.len > 0) wd: { + var dir = std.fs.openDirAbsolute(wd, .{}) catch |err| { + log.warn( + "error opening requested working directory dir={s} err={}", + .{ wd, err }, + ); + break :wd; + }; + defer dir.close(); - const stat = dir.stat() catch |err| { - log.warn( - "failed to stat requested working directory dir={s} err={}", - .{ wd, err }, - ); - break :wd; - }; + const stat = dir.stat() catch |err| { + log.warn( + "failed to stat requested working directory dir={s} err={}", + .{ wd, err }, + ); + break :wd; + }; - if (stat.kind != .directory) { - log.warn( - "requested working directory is not a directory dir={s}", - .{wd}, - ); - break :wd; + if (stat.kind != .directory) { + log.warn( + "requested working directory is not a directory dir={s}", + .{wd}, + ); + break :wd; + } + + config.@"working-directory" = wd; } - - config.@"working-directory" = wd; } // If we have a command from the options then we set it. - const cmd = std.mem.sliceTo(opts.command, 0); - if (cmd.len > 0) { - config.command = .{ .shell = cmd }; - config.@"wait-after-command" = true; + if (opts.command) |c_command| { + const cmd = std.mem.sliceTo(c_command, 0); + if (cmd.len > 0) { + config.command = .{ .shell = cmd }; + config.@"wait-after-command" = true; + } + } + + // Apply any environment variables that were requested. + if (opts.env_var_count > 0) { + const alloc = config.arenaAlloc(); + for (opts.env_vars.?[0..opts.env_var_count]) |env_var| { + const key = std.mem.sliceTo(env_var.key, 0); + const value = std.mem.sliceTo(env_var.value, 0); + try config.env.map.put( + alloc, + try alloc.dupeZ(u8, key), + try alloc.dupeZ(u8, value), + ); + } } // Initialize our surface right away. We're given a view that is diff --git a/src/config/Config.zig b/src/config/Config.zig index 2df66ba45..e9370d9b3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3004,6 +3004,11 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void { } } +/// Get the arena allocator associated with the configuration. +pub fn arenaAlloc(self: *Config) Allocator { + return self._arena.?.allocator(); +} + /// Change the state of conditionals and reload the configuration /// based on the new state. This returns a new configuration based /// on the new state. The caller must free the old configuration if they From 027171bd5db7f1ffd199fffeed4e8ef41f7a30d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 10:40:33 -0700 Subject: [PATCH 543/642] macos: can set env vars on new terminal --- .../Features/App Intents/NewTerminalIntent.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 55f33bd46..444f3d7c0 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -30,6 +30,13 @@ struct NewTerminalIntent: AppIntent { ) var workingDirectory: IntentFile? + @Parameter( + title: "Environment Variables", + description: "Environment variables in `KEY=VALUE` format.", + default: [] + ) + var env: [String] + @Parameter( title: "Parent Terminal", description: "The terminal to inherit the base configuration from." @@ -58,6 +65,15 @@ struct NewTerminalIntent: AppIntent { config.workingDirectory = dir.path(percentEncoded: false) } + // Parse environment variables from KEY=VALUE format + for envVar in env { + if let separatorIndex = envVar.firstIndex(of: "=") { + let key = String(envVar[.. Date: Fri, 20 Jun 2025 11:06:05 -0700 Subject: [PATCH 544/642] macos: intents all ask for permission --- macos/Ghostty.xcodeproj/project.pbxproj | 8 + .../App Intents/CloseTerminalIntent.swift | 4 + .../App Intents/CommandPaletteIntent.swift | 4 + .../GetTerminalDetailsIntent.swift | 4 + .../App Intents/GhosttyIntentError.swift | 2 + .../Features/App Intents/InputIntent.swift | 20 +++ .../App Intents/IntentPermission.swift | 37 ++++ .../Features/App Intents/KeybindIntent.swift | 4 + .../App Intents/NewTerminalIntent.swift | 3 + .../App Intents/QuickTerminalIntent.swift | 4 + macos/Sources/Helpers/PermissionRequest.swift | 162 ++++++++++++++++++ 11 files changed, 252 insertions(+) create mode 100644 macos/Sources/Features/App Intents/IntentPermission.swift create mode 100644 macos/Sources/Helpers/PermissionRequest.swift diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a64e6038e..a203ad682 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 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 */; }; + A51194172E05D964007258CC /* PermissionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194162E05D95E007258CC /* PermissionRequest.swift */; }; + A51194192E05DFC4007258CC /* IntentPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194182E05DFBB007258CC /* IntentPermission.swift */; }; A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; }; A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; }; @@ -155,6 +157,8 @@ 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 = ""; }; + A51194162E05D95E007258CC /* PermissionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionRequest.swift; sourceTree = ""; }; + A51194182E05DFBB007258CC /* IntentPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentPermission.swift; sourceTree = ""; }; A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = ""; }; A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = ""; }; A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = ""; }; @@ -355,6 +359,7 @@ A59630962AEE163600D64628 /* HostingWindow.swift */, A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */, A59FB5D02AE0DEA7009128F3 /* MetalView.swift */, + A51194162E05D95E007258CC /* PermissionRequest.swift */, A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */, A5CA378D2D31D6C100931030 /* Weak.swift */, C1F26EE72B76CBFC00404083 /* VibrantLayer.h */, @@ -640,6 +645,7 @@ A5E408462E0485270035FEAC /* InputIntent.swift */, A5E408442E0483F80035FEAC /* KeybindIntent.swift */, A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */, + A51194182E05DFBB007258CC /* IntentPermission.swift */, ); path = "App Intents"; sourceTree = ""; @@ -821,6 +827,8 @@ A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */, A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */, AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */, + A51194172E05D964007258CC /* PermissionRequest.swift in Sources */, + A51194192E05DFC4007258CC /* IntentPermission.swift in Sources */, A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */, A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */, A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */, diff --git a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift index 4de415494..923d22c97 100644 --- a/macos/Sources/Features/App Intents/CloseTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/CloseTerminalIntent.swift @@ -17,6 +17,10 @@ struct CloseTerminalIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surfaceView = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift index 2c1ff3386..fa983054b 100644 --- a/macos/Sources/Features/App Intents/CommandPaletteIntent.swift +++ b/macos/Sources/Features/App Intents/CommandPaletteIntent.swift @@ -24,6 +24,10 @@ struct CommandPaletteIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift index 5c41908f4..1cbaa9d68 100644 --- a/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift +++ b/macos/Sources/Features/App Intents/GetTerminalDetailsIntent.swift @@ -26,6 +26,10 @@ struct GetTerminalDetailsIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + switch detail { case .title: return .result(value: terminal.title) case .workingDirectory: return .result(value: terminal.workingDirectory) diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift index 34a0636d9..635250f72 100644 --- a/macos/Sources/Features/App Intents/GhosttyIntentError.swift +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -1,11 +1,13 @@ enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { case appUnavailable case surfaceNotFound + case permissionDenied var localizedStringResource: LocalizedStringResource { switch self { case .appUnavailable: return "The Ghostty app isn't properly initialized." case .surfaceNotFound: return "The terminal no longer exists." + case .permissionDenied: return "Ghostty doesn't allow Shortcuts." } } } diff --git a/macos/Sources/Features/App Intents/InputIntent.swift b/macos/Sources/Features/App Intents/InputIntent.swift index b8c248fe3..17c97fbbb 100644 --- a/macos/Sources/Features/App Intents/InputIntent.swift +++ b/macos/Sources/Features/App Intents/InputIntent.swift @@ -29,6 +29,10 @@ struct InputTextIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -75,6 +79,10 @@ struct KeyEventIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -133,6 +141,10 @@ struct MouseButtonIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -190,6 +202,10 @@ struct MousePosIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } @@ -254,6 +270,10 @@ struct MouseScrollIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift new file mode 100644 index 000000000..e02c4591d --- /dev/null +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -0,0 +1,37 @@ +/// Requests permission for Shortcuts app to interact with Ghostty +/// +/// This function displays a permission dialog asking the user to allow Shortcuts +/// to interact with Ghostty. The permission is automatically cached for 10 minutes +/// if the user selects "Allow", meaning subsequent intent calls won't show the dialog +/// again during that time period. +/// +/// The permission uses a shared UserDefaults key across all intents, so granting +/// permission for one intent allows all Ghostty intents to execute without additional +/// prompts for the duration of the cache period. +/// +/// - Returns: `true` if permission is granted, `false` if denied +/// +/// ## Usage +/// Add this check at the beginning of any App Intent's `perform()` method: +/// ```swift +/// @MainActor +/// func perform() async throws -> some IntentResult { +/// guard await requestIntentPermission() else { +/// throw GhosttyIntentError.permissionDenied +/// } +/// // ... continue with intent implementation +/// } +/// ``` +func requestIntentPermission() async -> Bool { + await withCheckedContinuation { continuation in + Task { @MainActor in + PermissionRequest.show( + "org.mitchellh.ghostty.shortcutsPermission", + message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", + allowDuration: .seconds(600), + ) { response in + continuation.resume(returning: response) + } + } + } +} diff --git a/macos/Sources/Features/App Intents/KeybindIntent.swift b/macos/Sources/Features/App Intents/KeybindIntent.swift index adeb64331..b31da4a50 100644 --- a/macos/Sources/Features/App Intents/KeybindIntent.swift +++ b/macos/Sources/Features/App Intents/KeybindIntent.swift @@ -21,6 +21,10 @@ struct KeybindIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let surface = terminal.surfaceModel else { throw GhosttyIntentError.surfaceNotFound } diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 444f3d7c0..3c36bed87 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -51,6 +51,9 @@ struct NewTerminalIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } guard let appDelegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } diff --git a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift index ee2761217..2e6c9850c 100644 --- a/macos/Sources/Features/App Intents/QuickTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/QuickTerminalIntent.swift @@ -10,6 +10,10 @@ struct QuickTerminalIntent: AppIntent { @MainActor func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> { + guard await requestIntentPermission() else { + throw GhosttyIntentError.permissionDenied + } + guard let delegate = NSApp.delegate as? AppDelegate else { throw GhosttyIntentError.appUnavailable } diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift new file mode 100644 index 000000000..35694081c --- /dev/null +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -0,0 +1,162 @@ +import AppKit +import Foundation + +/// Displays a permission request dialog with optional caching of user decisions +class PermissionRequest { + /// Shows a permission request dialog with customizable caching behavior + /// - Parameters: + /// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults + /// - message: The message to display in the alert dialog + /// - allowText: Custom text for the allow button (defaults to "Allow") + /// - allowDuration: If provided, automatically cache "Allow" responses for this duration + /// - window: If provided, shows the alert as a sheet attached to this window + /// - completion: Called with the user's decision (true for allow, false for deny) + /// + /// Caching behavior: + /// - If user checks "Remember my decision for one day", both allow/deny are cached for 24 hours + /// - If allowDuration is provided and user selects allow (without checkbox), decision is cached for that duration + /// - Cached decisions are automatically returned without showing the dialog + @MainActor + static func show( + _ key: String, + message: String, + informative: String = "", + allowText: String = "Allow", + allowDuration: Duration? = nil, + window: NSWindow? = nil, + completion: @escaping (Bool) -> Void + ) { + // Check if we have a stored decision that hasn't expired + if let storedResult = getStoredResult(for: key) { + completion(storedResult) + return + } + + let alert = NSAlert() + alert.messageText = message + alert.informativeText = informative + alert.alertStyle = .informational + + // Add buttons (they appear in reverse order) + alert.addButton(withTitle: allowText) + alert.addButton(withTitle: "Don't Allow") + + // Create checkbox for remembering + let checkbox = NSButton( + checkboxWithTitle: "Remember my decision for one day", + target: nil, + action: nil) + checkbox.state = .off + + // Set checkbox as accessory view + alert.accessoryView = checkbox + + // Show the alert + if let window = window { + alert.beginSheetModal(for: window) { response in + handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, completion: completion) + } + } else { + let response = alert.runModal() + handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, completion: completion) + } + } + + /// Handles the alert response and processes caching logic + /// - Parameters: + /// - response: The alert response from the user + /// - rememberDecision: Whether the remember checkbox was checked + /// - key: The UserDefaults key for caching + /// - allowDuration: Optional duration for auto-caching allow responses + /// - completion: Completion handler to call with the result + private static func handleResponse( + _ response: NSApplication.ModalResponse, + rememberDecision: Bool, + key: String, + allowDuration: Duration?, + completion: @escaping (Bool) -> Void) { + + let result: Bool + switch response { + case .alertFirstButtonReturn: // Allow + result = true + case .alertSecondButtonReturn: // Don't Allow + result = false + default: + result = false + } + + // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set + if rememberDecision { + storeResult(result, for: key, duration: .seconds(86400)) + } else if result, let allowDuration { + storeResult(result, for: key, duration: allowDuration) + } + + completion(result) + } + + /// Retrieves a cached permission decision if it hasn't expired + /// - Parameter key: The UserDefaults key to check + /// - Returns: The cached decision, or nil if no valid cached decision exists + private static func getStoredResult(for key: String) -> Bool? { + let userDefaults = UserDefaults.standard + guard let data = userDefaults.data(forKey: key), + let storedPermission = try? NSKeyedUnarchiver.unarchivedObject( + ofClass: StoredPermission.self, from: data) else { + return nil + } + + if Date() > storedPermission.expiry { + // Decision has expired, remove stored value + userDefaults.removeObject(forKey: key) + return nil + } + + return storedPermission.result + } + + /// Stores a permission decision in UserDefaults with an expiration date + /// - Parameters: + /// - result: The permission decision to store + /// - key: The UserDefaults key to store under + /// - duration: How long the decision should be cached + private static func storeResult(_ result: Bool, for key: String, duration: Duration) { + let expiryDate = Date().addingTimeInterval(duration.timeInterval) + let storedPermission = StoredPermission(result: result, expiry: expiryDate) + if let data = try? NSKeyedArchiver.archivedData(withRootObject: storedPermission, requiringSecureCoding: true) { + let userDefaults = UserDefaults.standard + userDefaults.set(data, forKey: key) + } + } + + /// Internal class for storing permission decisions with expiration dates in UserDefaults + /// Conforms to NSSecureCoding for safe archiving/unarchiving + @objc(StoredPermission) + private class StoredPermission: NSObject, NSSecureCoding { + static var supportsSecureCoding: Bool = true + + let result: Bool + let expiry: Date + + init(result: Bool, expiry: Date) { + self.result = result + self.expiry = expiry + super.init() + } + + required init?(coder: NSCoder) { + self.result = coder.decodeBool(forKey: "result") + guard let expiry = coder.decodeObject(of: NSDate.self, forKey: "expiry") as? Date else { + return nil + } + self.expiry = expiry + super.init() + } + + func encode(with coder: NSCoder) { + coder.encode(result, forKey: "result") + coder.encode(expiry, forKey: "expiry") + } + } +} From b6559d08994ebb39ef66d210cd5461e03c594637 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 11:54:19 -0700 Subject: [PATCH 545/642] macos: add a macos-shortcut config --- .../App Intents/IntentPermission.swift | 21 ++++++++++++- macos/Sources/Ghostty/Ghostty.Config.swift | 17 ++++++++++ src/config/Config.zig | 31 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index e02c4591d..78efb3d5d 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -1,5 +1,7 @@ +import AppKit + /// Requests permission for Shortcuts app to interact with Ghostty -/// +/// /// This function displays a permission dialog asking the user to allow Shortcuts /// to interact with Ghostty. The permission is automatically cached for 10 minutes /// if the user selects "Allow", meaning subsequent intent calls won't show the dialog @@ -25,6 +27,23 @@ func requestIntentPermission() async -> Bool { await withCheckedContinuation { continuation in Task { @MainActor in + if let delegate = NSApp.delegate as? AppDelegate { + switch (delegate.ghostty.config.macosShortcuts) { + case .allow: + continuation.resume(returning: true) + return + + case .deny: + continuation.resume(returning: false) + return + + case .ask: + // Continue with the permission dialog + break + } + } + + PermissionRequest.show( "org.mitchellh.ghostty.shortcutsPermission", message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index fcbea2a12..241c10632 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -558,6 +558,17 @@ extension Ghostty { _ = ghostty_config_get(config, &v, key, UInt(key.count)) return v } + + var macosShortcuts: MacShortcuts { + let defaultValue = MacShortcuts.ask + guard let config = self.config else { return defaultValue } + var v: UnsafePointer? = nil + let key = "macos-shortcuts" + guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue } + guard let ptr = v else { return defaultValue } + let str = String(cString: ptr) + return MacShortcuts(rawValue: str) ?? defaultValue + } } } @@ -584,6 +595,12 @@ extension Ghostty.Config { case always } + enum MacShortcuts: String { + case allow + case deny + case ask + } + enum ResizeOverlay : String { case always case never diff --git a/src/config/Config.zig b/src/config/Config.zig index e9370d9b3..aee670213 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2355,6 +2355,30 @@ keybind: Keybinds = .{}, /// @"macos-icon-screen-color": ?ColorList = null, +/// Whether macOS Shortcuts are allowed to control Ghostty. +/// +/// Ghostty exposes a number of actions that allow Shortcuts to +/// control and interact with Ghostty. This includes creating new +/// terminals, sending text to terminals, running commands, invoking +/// any keybind action, etc. +/// +/// This is a powerful feature but can be a security risk if a malicious +/// shortcut is able to be installed and executed. Therefore, this +/// configuration allows you to disable this feature. +/// +/// Valid values are: +/// +/// * `ask` - Ask the user whether for permission. Ghostty will by default +/// cache the user's choice for 10 minutes since we can't determine +/// when a single workflow begins or ends. The user also has an option +/// in the GUI to allow for the remainder of the day. +/// +/// * `allow` - Allow Shortcuts to control Ghostty without asking. +/// +/// * `deny` - Deny Shortcuts from controlling Ghostty. +/// +@"macos-shortcuts": MacShortcuts = .ask, + /// Put every surface (tab, split, window) into a dedicated Linux cgroup. /// /// This makes it so that resource management can be done on a per-surface @@ -5961,6 +5985,13 @@ pub const MacAppIconFrame = enum { chrome, }; +/// See macos-shortcuts +pub const MacShortcuts = enum { + allow, + deny, + ask, +}; + /// See gtk-single-instance pub const GtkSingleInstance = enum { desktop, From e4c13cdba87761dfd8feb77b7e57231f8032415f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 12:09:03 -0700 Subject: [PATCH 546/642] macos: Optional/Array extensions need to build for iOS too --- macos/Ghostty.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/macos/Ghostty.xcodeproj/project.pbxproj b/macos/Ghostty.xcodeproj/project.pbxproj index a203ad682..416d8b106 100644 --- a/macos/Ghostty.xcodeproj/project.pbxproj +++ b/macos/Ghostty.xcodeproj/project.pbxproj @@ -58,6 +58,8 @@ A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; }; A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; }; A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; }; + A553F4062E05E93000257779 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; }; + A553F4072E05E93D00257779 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; }; A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; }; A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; }; A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; }; @@ -892,6 +894,7 @@ buildActionMask = 2147483647; files = ( A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */, + A553F4062E05E93000257779 /* Optional+Extension.swift in Sources */, A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */, A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */, A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */, @@ -901,6 +904,7 @@ A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */, A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */, A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */, + A553F4072E05E93D00257779 /* Array+Extension.swift in Sources */, C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 020976bf8859324f2cd653988988b08b60ae300c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jun 2025 06:42:31 -0700 Subject: [PATCH 547/642] macos: address some feedback --- .../App Intents/Entities/TerminalEntity.swift | 6 +++--- .../Features/App Intents/GhosttyIntentError.swift | 6 +++--- .../Sources/Helpers/Extensions/Array+Extension.swift | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift index 1fb69f1f8..e29fbba3f 100644 --- a/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift +++ b/macos/Sources/Features/App Intents/Entities/TerminalEntity.swift @@ -130,10 +130,10 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery { let controllers = NSApp.windows.compactMap { $0.windowController as? BaseTerminalController } - + // Get all our surfaces - return controllers.reduce([]) { result, c in - result + (c.surfaceTree.root?.leaves() ?? []) + return controllers.flatMap { + $0.surfaceTree.root?.leaves() ?? [] } } } diff --git a/macos/Sources/Features/App Intents/GhosttyIntentError.swift b/macos/Sources/Features/App Intents/GhosttyIntentError.swift index 635250f72..c52b7a52e 100644 --- a/macos/Sources/Features/App Intents/GhosttyIntentError.swift +++ b/macos/Sources/Features/App Intents/GhosttyIntentError.swift @@ -5,9 +5,9 @@ enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible { var localizedStringResource: LocalizedStringResource { switch self { - case .appUnavailable: return "The Ghostty app isn't properly initialized." - case .surfaceNotFound: return "The terminal no longer exists." - case .permissionDenied: return "Ghostty doesn't allow Shortcuts." + case .appUnavailable: "The Ghostty app isn't properly initialized." + case .surfaceNotFound: "The terminal no longer exists." + case .permissionDenied: "Ghostty doesn't allow Shortcuts." } } } diff --git a/macos/Sources/Helpers/Extensions/Array+Extension.swift b/macos/Sources/Helpers/Extensions/Array+Extension.swift index fac340472..4e8e39918 100644 --- a/macos/Sources/Helpers/Extensions/Array+Extension.swift +++ b/macos/Sources/Helpers/Extensions/Array+Extension.swift @@ -34,12 +34,12 @@ extension Array where Element == String { func helper(index: Int, accumulated: [UnsafePointer?], body: ([UnsafePointer?]) throws -> T) rethrows -> T { if index == count { return try body(accumulated) - } else { - return try self[index].withCString { cStr in - var newAccumulated = accumulated - newAccumulated.append(cStr) - return try helper(index: index + 1, accumulated: newAccumulated, body: body) - } + } + + return try self[index].withCString { cStr in + var newAccumulated = accumulated + newAccumulated.append(cStr) + return try helper(index: index + 1, accumulated: newAccumulated, body: body) } } From 296f340ff425c9986fedc2dbb4b5faa496dc63e3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Jun 2025 06:46:33 -0700 Subject: [PATCH 548/642] macos: the approval dialog is now forever --- .../App Intents/IntentPermission.swift | 5 +- macos/Sources/Helpers/PermissionRequest.swift | 87 +++++++++++++++---- src/config/Config.zig | 7 +- 3 files changed, 75 insertions(+), 24 deletions(-) diff --git a/macos/Sources/Features/App Intents/IntentPermission.swift b/macos/Sources/Features/App Intents/IntentPermission.swift index 78efb3d5d..2ec4f2bd9 100644 --- a/macos/Sources/Features/App Intents/IntentPermission.swift +++ b/macos/Sources/Features/App Intents/IntentPermission.swift @@ -46,8 +46,9 @@ func requestIntentPermission() async -> Bool { PermissionRequest.show( "org.mitchellh.ghostty.shortcutsPermission", - message: "Allow Shortcuts to interact with Ghostty for the next 10 minutes?", - allowDuration: .seconds(600), + message: "Allow Shortcuts to interact with Ghostty?", + allowDuration: .forever, + rememberDuration: nil, ) { response in continuation.resume(returning: response) } diff --git a/macos/Sources/Helpers/PermissionRequest.swift b/macos/Sources/Helpers/PermissionRequest.swift index 35694081c..9c16c7163 100644 --- a/macos/Sources/Helpers/PermissionRequest.swift +++ b/macos/Sources/Helpers/PermissionRequest.swift @@ -3,17 +3,25 @@ import Foundation /// Displays a permission request dialog with optional caching of user decisions class PermissionRequest { + /// Specifies how long a permission decision should be cached + enum AllowDuration { + case once + case forever + case duration(Duration) + } + /// Shows a permission request dialog with customizable caching behavior /// - Parameters: /// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults /// - message: The message to display in the alert dialog /// - allowText: Custom text for the allow button (defaults to "Allow") /// - allowDuration: If provided, automatically cache "Allow" responses for this duration + /// - rememberDuration: If provided, shows a checkbox to remember the decision for this duration /// - window: If provided, shows the alert as a sheet attached to this window /// - completion: Called with the user's decision (true for allow, false for deny) /// /// Caching behavior: - /// - If user checks "Remember my decision for one day", both allow/deny are cached for 24 hours + /// - If rememberDuration is provided and user checks "Remember my decision", both allow/deny are cached for that duration /// - If allowDuration is provided and user selects allow (without checkbox), decision is cached for that duration /// - Cached decisions are automatically returned without showing the dialog @MainActor @@ -22,7 +30,8 @@ class PermissionRequest { message: String, informative: String = "", allowText: String = "Allow", - allowDuration: Duration? = nil, + allowDuration: AllowDuration = .once, + rememberDuration: Duration? = .seconds(86400), window: NSWindow? = nil, completion: @escaping (Bool) -> Void ) { @@ -41,24 +50,28 @@ class PermissionRequest { alert.addButton(withTitle: allowText) alert.addButton(withTitle: "Don't Allow") - // Create checkbox for remembering - let checkbox = NSButton( - checkboxWithTitle: "Remember my decision for one day", - target: nil, - action: nil) - checkbox.state = .off - - // Set checkbox as accessory view - alert.accessoryView = checkbox + // Create checkbox for remembering if duration is provided + var checkbox: NSButton? + if let rememberDuration = rememberDuration { + let checkboxTitle = formatRememberText(for: rememberDuration) + checkbox = NSButton( + checkboxWithTitle: checkboxTitle, + target: nil, + action: nil) + checkbox!.state = .off + + // Set checkbox as accessory view + alert.accessoryView = checkbox + } // Show the alert if let window = window { alert.beginSheetModal(for: window) { response in - handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, completion: completion) + handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } else { let response = alert.runModal() - handleResponse(response, rememberDecision: checkbox.state == .on, key: key, allowDuration: allowDuration, completion: completion) + handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion) } } @@ -68,12 +81,14 @@ class PermissionRequest { /// - rememberDecision: Whether the remember checkbox was checked /// - key: The UserDefaults key for caching /// - allowDuration: Optional duration for auto-caching allow responses + /// - rememberDuration: Optional duration for the remember checkbox /// - completion: Completion handler to call with the result private static func handleResponse( _ response: NSApplication.ModalResponse, rememberDecision: Bool, key: String, - allowDuration: Duration?, + allowDuration: AllowDuration, + rememberDuration: Duration?, completion: @escaping (Bool) -> Void) { let result: Bool @@ -87,10 +102,21 @@ class PermissionRequest { } // Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set - if rememberDecision { - storeResult(result, for: key, duration: .seconds(86400)) - } else if result, let allowDuration { - storeResult(result, for: key, duration: allowDuration) + if rememberDecision, let rememberDuration = rememberDuration { + storeResult(result, for: key, duration: rememberDuration) + } else if result { + switch allowDuration { + case .once: + // Don't store anything for once + break + case .forever: + // Store for a very long time (100 years). When the bug comes in that + // 100 years has passed and their forever permission expired I'll be + // dead so it won't be my problem. + storeResult(result, for: key, duration: .seconds(3153600000)) + case .duration(let duration): + storeResult(result, for: key, duration: duration) + } } completion(result) @@ -130,6 +156,31 @@ class PermissionRequest { } } + /// Formats the remember checkbox text based on the duration + /// - Parameter duration: The duration to format + /// - Returns: A human-readable string for the checkbox + private static func formatRememberText(for duration: Duration) -> String { + let seconds = duration.timeInterval + + // Warning: this probably isn't localization friendly at all so we're + // going to have to redo this for that. + switch seconds { + case 0..<60: + return "Remember my decision for \(Int(seconds)) seconds" + case 60..<3600: + let minutes = Int(seconds / 60) + return "Remember my decision for \(minutes) minute\(minutes == 1 ? "" : "s")" + case 3600..<86400: + let hours = Int(seconds / 3600) + return "Remember my decision for \(hours) hour\(hours == 1 ? "" : "s")" + case 86400: + return "Remember my decision for one day" + default: + let days = Int(seconds / 86400) + return "Remember my decision for \(days) day\(days == 1 ? "" : "s")" + } + } + /// Internal class for storing permission decisions with expiration dates in UserDefaults /// Conforms to NSSecureCoding for safe archiving/unarchiving @objc(StoredPermission) diff --git a/src/config/Config.zig b/src/config/Config.zig index aee670213..aabf4f6ba 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2368,10 +2368,9 @@ keybind: Keybinds = .{}, /// /// Valid values are: /// -/// * `ask` - Ask the user whether for permission. Ghostty will by default -/// cache the user's choice for 10 minutes since we can't determine -/// when a single workflow begins or ends. The user also has an option -/// in the GUI to allow for the remainder of the day. +/// * `ask` - Ask the user whether for permission. Ghostty will remember +/// this choice and never ask again. This is similar to other macOS +/// permissions such as microphone access, camera access, etc. /// /// * `allow` - Allow Shortcuts to control Ghostty without asking. /// From c1c3f639c5d50e15c8890ecfa56d82f52072deea Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 20 Jun 2025 21:08:06 -0700 Subject: [PATCH 549/642] macos: Ghostty Icon Update for macOS Tahoe This updates the Ghostty icon to be compatible with macOS Tahoe (supports glass effects, light/dark, tinting, etc.). This icon is made in the new Apple Icon Composer as the source format, and all other formats are exported from it. This commit also updates the icon for non-Apple platforms because the icon is fundamentally the same and I don't see any reason to maintain multiple icons of fundamentally the same design and style. This commit also includes updates to the macOS app so that the About Window and so on will use the new icon. --- .prettierignore | 3 + dist/macos/Ghostty.icns | Bin 382978 -> 0 bytes dist/macos/Info.plist | 17 -- images/Ghostty.icon/Assets/Ghostty.png | Bin 0 -> 106126 bytes .../Ghostty.icon/Assets/Inner Bevel 6px.png | Bin 0 -> 435672 bytes images/Ghostty.icon/Assets/Screen Effects.png | Bin 0 -> 92547 bytes images/Ghostty.icon/Assets/Screen.png | Bin 0 -> 143481 bytes images/Ghostty.icon/Assets/gloss.png | Bin 0 -> 3353 bytes images/Ghostty.icon/icon.json | 170 ++++++++++++++++++ images/icons/icon_1024.png | Bin 464853 -> 2365230 bytes images/icons/icon_1024@2x.png | Bin 0 -> 2365230 bytes images/icons/icon_128.png | Bin 15177 -> 15089 bytes images/icons/icon_256.png | Bin 68189 -> 237699 bytes images/icons/icon_256@2x.png | Bin 221047 -> 237699 bytes images/icons/icon_512.png | Bin 221047 -> 667563 bytes images/icons/icon_512@2x.png | Bin 0 -> 667563 bytes .../AppIcon.appiconset/Contents.json | 74 -------- .../macOS-AppIcon-1024px 1.png | Bin 464853 -> 0 bytes .../macOS-AppIcon-1024px.png | Bin 464853 -> 0 bytes .../macOS-AppIcon-128px-128pt@1x.png | Bin 15177 -> 0 bytes .../macOS-AppIcon-16px-16pt@1x.png | Bin 666 -> 0 bytes .../macOS-AppIcon-256px-128pt@2x 1.png | Bin 68177 -> 0 bytes .../macOS-AppIcon-256px-128pt@2x.png | Bin 68177 -> 0 bytes .../macOS-AppIcon-32px-16pt@2x.png | Bin 1562 -> 0 bytes .../macOS-AppIcon-32px-32pt@1x.png | Bin 1564 -> 0 bytes .../macOS-AppIcon-512px-256pt@2x.png | Bin 221047 -> 0 bytes .../macOS-AppIcon-512px.png | Bin 220725 -> 0 bytes .../macOS-AppIcon-64px-32pt@2x.png | Bin 4485 -> 0 bytes macos/Ghostty.xcodeproj/project.pbxproj | 18 +- .../ColorizedGhosttyIconImage.swift | 14 ++ 30 files changed, 199 insertions(+), 97 deletions(-) delete mode 100644 dist/macos/Ghostty.icns delete mode 100644 dist/macos/Info.plist create mode 100644 images/Ghostty.icon/Assets/Ghostty.png create mode 100644 images/Ghostty.icon/Assets/Inner Bevel 6px.png create mode 100644 images/Ghostty.icon/Assets/Screen Effects.png create mode 100644 images/Ghostty.icon/Assets/Screen.png create mode 100644 images/Ghostty.icon/Assets/gloss.png create mode 100644 images/Ghostty.icon/icon.json create mode 100644 images/icons/icon_1024@2x.png create mode 100644 images/icons/icon_512@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px 1.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-1024px.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-128px-128pt@1x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-16px-16pt@1x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x 1.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-256px-128pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-16pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-32px-32pt@1x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px-256pt@2x.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-512px.png delete mode 100644 macos/Assets.xcassets/AppIcon.appiconset/macOS-AppIcon-64px-32pt@2x.png diff --git a/.prettierignore b/.prettierignore index 490538680..f131a5edc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,6 +11,9 @@ zig-out/ # macos is managed by XCode GUI macos/ +# produced by Icon Composer on macOS +images/Ghostty.icon/icon.json + # website dev run website/.next diff --git a/dist/macos/Ghostty.icns b/dist/macos/Ghostty.icns deleted file mode 100644 index 44a44711aac562d23a82630e3b374ba9c5325b14..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 382978 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

    rGRIzDlyBzE>tHuFoGQ?9ocTyboB}&T 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⪙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 AasU7T diff --git a/dist/macos/Info.plist b/dist/macos/Info.plist deleted file mode 100644 index 8283cc529..000000000 --- a/dist/macos/Info.plist +++ /dev/null @@ -1,17 +0,0 @@ - - - - - CFBundleExecutable - ghostty - CFBundleIdentifier - com.mitchellh.ghostty - CFBundleName - Ghostty - CFBundleDisplayName - Ghostty - CFBundleIconFile - Ghostty.icns - - - diff --git a/images/Ghostty.icon/Assets/Ghostty.png b/images/Ghostty.icon/Assets/Ghostty.png new file mode 100644 index 0000000000000000000000000000000000000000..49795c006c646e8a580759f1790d838912ffa932 GIT binary patch literal 106126 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
    bqgF|0mCwXU@@ltKPp7>~uV_ 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

  1. 5(rFcQuO8|?Gf6}6#LMYBq9d-P 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

    9lv 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 ZV%}I#zxS{A!Aqc}TAr?cF6*2UngGdL?MVOt literal 0 HcmV?d00001 diff --git a/images/Ghostty.icon/Assets/gloss.png b/images/Ghostty.icon/Assets/gloss.png new file mode 100644 index 0000000000000000000000000000000000000000..f11196010eedf64765003a7d8783bca33b49c1a7 GIT binary patch literal 3353 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 CC~mU= literal 0 HcmV?d00001 diff --git a/images/Ghostty.icon/icon.json b/images/Ghostty.icon/icon.json new file mode 100644 index 000000000..b29c9d81f --- /dev/null +++ b/images/Ghostty.icon/icon.json @@ -0,0 +1,170 @@ +{ + "color-space-for-untagged-svg-colors" : "display-p3", + "fill" : { + "linear-gradient" : [ + "display-p3:0.87945,0.87945,0.87945,1.00000", + "display-p3:0.40000,0.40000,0.40392,1.00000" + ] + }, + "groups" : [ + { + "blend-mode" : "normal", + "layers" : [ + { + "blend-mode" : "overlay", + "fill" : { + "linear-gradient" : [ + "srgb:1.00000,1.00000,1.00000,1.00000", + "srgb:0.00000,0.00000,0.00000,1.00000" + ] + }, + "hidden" : false, + "image-name" : "gloss.png", + "name" : "GlossTop", + "opacity" : 0.25, + "position" : { + "scale" : 0.98, + "translation-in-points" : [ + 0.90625, + -236.4609375 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : "automatic", + "hidden" : false, + "image-name" : "gloss.png", + "name" : "gloss", + "position" : { + "scale" : 0.98, + "translation-in-points" : [ + 0.90625, + -236.4609375 + ] + } + } + ], + "lighting" : "individual", + "name" : "Group 4", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + }, + { + "blend-mode" : "overlay", + "layers" : [ + { + "blend-mode" : "overlay", + "fill" : "automatic", + "glass" : false, + "hidden" : false, + "image-name" : "Screen Effects.png", + "name" : "Screen Effects" + }, + { + "blend-mode" : "overlay", + "fill" : "automatic", + "glass" : true, + "hidden" : false, + "image-name" : "Screen Effects.png", + "name" : "Screen Effects" + } + ], + "lighting" : "individual", + "name" : "Group 3", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : false, + "value" : 0.5 + } + }, + { + "blur-material" : null, + "layers" : [ + { + "blend-mode" : "normal", + "fill" : "automatic", + "hidden" : false, + "image-name" : "Ghostty.png", + "name" : "Ghostty", + "position" : { + "scale" : 1, + "translation-in-points" : [ + -185.015625, + -143.8359375 + ] + } + }, + { + "blend-mode" : "normal", + "fill" : { + "solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000" + }, + "glass" : true, + "hidden" : false, + "image-name" : "Ghostty.png", + "name" : "GhosttyBlur", + "position" : { + "scale" : 1, + "translation-in-points" : [ + -186.59375, + -143.8359375 + ] + } + }, + { + "hidden" : false, + "image-name" : "Screen.png", + "name" : "Screen" + } + ], + "lighting" : "individual", + "name" : "Group 2", + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : false, + "value" : 0.5 + } + }, + { + "blend-mode" : "normal", + "blur-material" : null, + "hidden" : false, + "layers" : [ + { + "image-name" : "Inner Bevel 6px.png", + "name" : "Inner Bevel 6px" + } + ], + "lighting" : "individual", + "name" : "Group 1", + "shadow" : { + "kind" : "layer-color", + "opacity" : 0.2 + }, + "specular" : false, + "translucency" : { + "enabled" : false, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/images/icons/icon_1024.png b/images/icons/icon_1024.png index a0b716c8781aeb5f1d3fa13512cc771d2a669345..22361edcbb4d517db5bd02eb6b95064c007b3150 100644 GIT binary patch literal 2365230 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≨O}VGb67t24B{Nwhl{g7IAM*JIPwZ`0l}Ci)y}y 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

    ifV`@cNV$TtO_1iUyh&x85x&&77qcdo3mR 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

    Xn%?n-} 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

    -jV(nx_)9GHrKH34I--{w{*+lFe*S)pxb^G8 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

    uqCALdv54f|rA+qk9m>qV8yo3k`_%-oZyV^Chp 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 h zi9){(t{Ae~ZN2z#p67pF*{ojzTD5)8<#k+UGC8_07n*;J`Sro;!ppb62+#fz$nKWj z|1pR+-p`yZn(y?i=Yl$jI{ycON}sYcjiL$&yBdyO6U&OOtuSs;J7+~}KoTlRya zh4cTbvK06`=jsJyxEh4Meg1Rmwf$F%Vk5lwNpW4TI+zg2neDf~+|RlFTb4oZwOjw4 z9!@)Fx_7Da@ztOI9=Z1Ll*HzR0c~Y7J$7ns{2g1EIGK~@^@gxN>z$ULmsLL%;hQ(v z>$z==l7Y+q+WM1g{$F8UH}T#qRo5M_Ecyy6AO2p#xhmA_yOpJ{hvfF?;1?cVz4MLh zT8)azz!q=}Np7mD6=^NPR-jnVwIsLXIW4)Qx zil!r?b06;Xj@@qg?tWNBe1Uy>@4n^XX^pAZ&O`;*%oO#S?{s{*kC*TI!=Xt%hj_ou zHa;af@Ae;uYFmkqma&1E2Fnae_1|*JWl461CCFTh_^;k|zjTwA)Y0UbAza-m;x>}| z-4ab#i{7ug=D2a`m(sON?o7$uKXg}mUr_k5)y>@?^!&+t(^+eN$^ZTxrfa1dyGhU@ z`P7R&XG?6tP9{y$j%$~?uadox-}B#s->&jsYF)*zME*^Ay}F}!=bDE1pC5C?e|8b# zUwZDhiGf|i&Pz`x6`i@i_0sbrEQ&j9HS77TV$XI(u6kT)viWb0x9GXp=B?gV`I6V% zmd{xDf9k{b;zLqQOuRZFvYaL1l0NG8{{ZHi2={@%{*E?8v|N1)Tg8?rW|Mu=L%#iG7J0F@M&eUvZ?y>rB;kB=ep6^*Q zW9#PLZMV;VjWOCiD|Oinv*|_CZPvBUO@HY1uqA5Cb|>qpPWIaq=9Ppzc3Z+CcR-!5 zYx`X0ydCyC->KJZx?JpRd-(6#T^V~*YRs(qGB;Fj7h_ts^}vk_oaYR~k1gh2zRmve zo=NR|-`|JN+P5rKB{p5W?4HQKX_`JqrJk!F`f0wkSoGaYG0&CjlEasIw7s9%J)u2@ zbyxP!d$Z=t)V$i6FLZG}-+l?9w)dAz|E9M6*Ax9R-DIz#AWwy1jakfrrcd%pUH6yR ztAGA>lHd04OLh6lrAmz%<%)@quG{Q=Dpx%6+7;p>#kR;b!F!Qo5TuWKK!9Vp~IxBk<8>+3=tb;XAlK7DR3b4&C8PCq7R z-pLAwHay#)^e5rd{{U9LS-YzqO}DLAJs2C)?EiGyR@a9gjAE?rHC%rGU?R(}3;W|U z>z4kWDl_Zr4QyXhH$uD*m8bD1NZlXtAr1# z`MltnmDa##>htY33%AF1@y2hf4!LMEXwOcVJU!ZHgV#L%850ib%dz@Tl{Wknvi#y* znIBUtR5I^JHMBAu-rsygcj3LZH8vkMc7Cq<_Upqh*4h{MSFiU}n5C~Guy^}}2Tcq= ze5IfNUhk=JD=UEQ@5Ao)xO1k@zSrNL=luMF@AN(99tu@O8jUa3URUucxbnb$`jslx z=K)6lEZZ-!^2q&q*!rM}eIKLU_coTk{fdn*-sXEe__a+a<^ZF&&)N4 zHVSloW1xxqiX5n!Y!uV)qIkY|h$#@1|o{a@2X%8r?jW zH+h!(TU<&xPX=CUj+Xke;P8wYoTpy1%+hvzBwOS8)u7VoNU_uvm6wwiI2vcaakbxG zI(hbc>k8FfP3td+i8F;STD-Gt*H?7UJdwNLV+~lBB?K*}w=a>(w zhp+m2_t5oQ$5^;}3Vkf+I$r*lG);4_*}~?OO_>TMKQ`RUsty0hB`?RU=dexj@*lsK z8$4Idlv^|DaplUM|8ng6*u)#BdKv|{b=z>Q+$-UEDSY!kg>c4(tBa$2dGktSyghQ( z9a>OwUh;B`s(yIaf7Rn(4>g3%+V2%5WAid_;m3yu^jyv?Ih*N`J~i%5o=|O}N|vWm zj`L#q6!B#T4R%+A?7Lv>BRT2Y@@s!|UJC2Y@6csz`|SPg+j_PC>7|RrKW_HBHUH{o zH~lBOD{a?HKI^ZGxK(Rczp#3a;h%@kXFb>2`J;ZTy@2q6uik(6G%uLqaQ(?ti*JQ( z<#+XrPg?C^pYo7bV?Wyl?zxBKF2^47e{34L^OW8OC$_ugwfqY2da|1{mUn#MWnr88 z_HaVTPQI$+8;UYs9*wI?eA*!Tt8o5C%ZDEsR1Wx^%GOqyV09r;e$oF0+zTGIbE`c$ zEWTnBs??J}qz~5WhmzVS?n|1^T*t~nlYQtz--?Y%FUc&mly-u}7 z3FGwY!t?4yzh*x0?EM-1qsDB8pvL<`hqe4~5*?OJ{wn^}%1(GoQT5ASTY0T$`9|hz zkFUtt@^so+$o`se@19Biw--(gk{_I{o@qb#+I7gT;{W$swF3SgInF{dn*5iJfBgFK z_Htpnj;zJiIdZT5+x_LKe{k9N_49}SnC1PySFlH%ERND=&3`Vr;l}an&jepN%T&Dm zdjH`+JCBCy$C6g7&A(-&=l>0U{yHb>f9sUP6`>V;O3cbJJMW|}zWVyTq4=k14?ZN< zMx8i%{{6{1_s>bX&s(1Fc3;A@=k1)w{bB4=qnIMji=MmAzLd-F-kIH%$3DI)Tq!oy zPfoNydvC%f?zyFt&Rg5w-)h{L?rFH1rL9Ko?fgei3Is1LG4fRxwXpKL#y;o#%Abrk zkD8mj>Q#)MX!t;U(Z#GUCdW9w@1OFiL@r47{bHB<+9y|L`Zyx0 zq35u0lUu$){OuhVrVDSVRo95KeLBT)@v6qOguMH&C;79Q{AJg(zfoz)wdYyDsm~Wu zZx!UN`zq2lzh+~@#p@a>ueQwLyfsha-ju>QCiQ~h&rb2oO3^-eeBFV$jEbc)Nnh_c zIQ)0I?45D#ev7KfVy{;3>ql(3gv6Vdt<7AvaBgr$ol~*&x_wq{vh%bLMtHyd%{YzY z+CumL(V5~+^RIt2TQ}9O?Y*d^$hS)jGq~cWJXWfCIE^VQSHG3*)zsJ!=f!W`Ua;+v z+G8Jn>zrjx|9U6BJsfXME_^)I$nUwAr?Gwi{$SxF9KDC6cev#IHA`ZUV=@;mVLWw6 zVk=Xu^4jG_A!hFm)O$)turS|h9a zwRWnW`nB~JnizMg3Hu)R^?!{f?>}@Rzwx-#`P>VBtzTyxEYIgZ z=DxU)L*kS0+-XeB9AaWBOOIFNh}`EsGh16}dHbP+Cf4O2e%4ER+zM<|IV1dF@dS}y zRi}1@aPCT-@jv^hG5_3W^1pd1HIoL`Oy?>Qf|QvFHAms?D<-1-~vEP+gBv(DZq%on; z!2PV-`b!dF0tz<-Ol(u9@W}gp{unUP$cQcHLRrhi_Mh_)eXmJ-yG#GQ&}XiFZyL|D zH5DG`d}+Z~r=)iDKhImI<~f>~JpaR9mdx4mtIqmiQSY{D&(^QEk1xOYPu6u)tz}X@ z_knJ%2bs@PlS}wRG-A&?8NOa3yF+T{w0{Zml56DFOrH~Ue|t!Pk7@h#Wjl2w9{v-1 z?4Tiksk*>>^7J~{_%+Q+mZpx(F&p$>aIKSA|8ZXa;Vy6KWSkzrP^{lC~=IKDZ9%idY%<2+Rk z6R&@PMN`%!G;;ioV!69`?|!KZ@sb@My9^aur(fvFH{Cb$p8GyMkKd7J%&hv)l=7|* z%2Yp9oTjx$+$Q2r$Vsf1`dzR0RQ2qKg?)*qH?YsseYXE< zapq^^HIvx>=Jve1@m_Dk|I%fNvVP1jf3Cb#S}VmTT4r@vX8sP=oR9zZ7}iYTTD!iy zQTN-!>{EvFXM+yE-19+S^X~tRe&TKHbDb33bQc(zo||I0|H%}-=j)R>-!yKo$i#PDC2zLfytV$aUV}SoBXO*{6;>@DT`-Tzdxy5uRi{H=C#wR zM(gh%YhLqTu;hSc*Z)n`cOx1dJ>$+j3taj7Z_c)hb=yCCJ>Gkv@pD$IWvuGjW%Je2 zR(>l>dv0{E|LtFqE%SMdv+XthJ?m<@)$up>S?|Tz7f(MNdlni|cvfqNwpPG~3Ab*) zoxd#n;67u$Loa*2)NPqj?loUnSfESy*mI%x^@r+qgjRHa_w};SzvP?4duvb9u5CA# zZ`rrOh;i>stvJ=EXJQKUXCB;p?aOS-!%JrBKd-u$v?n@NzkNeM!*{)N-&b-k(AP9x zQIRuIZ+BvvUbDk;?ck8bsVfZhwIXEVRzBTi^zhGTtb|wEwxE!uj9W{t!O%bHRtlUy`%VX7=dvKI2mOvu7vMh1##8#~Mx_wKw_q z;fJ{8Z#Sl!=S9u$25tF&uj=Dk^LtgzFZTK|nY_H*xzKKl(24qfJ=KC*X}-5do;%*D z@82%8?8oVqEF5w>91bo?`1LJYNR9Q1dp73-ky0@5$#93d7KFYLi0*jg3^9#Zvnfj@K3$2UHV(tn;RT{-u^^>+OG{n_9A z*FBKjf9kjW$NK7sUHJ)}n?BD@+;NWIRV1Tp(!KvnJT*Av>aG5i{t}+Qrv5eyL*D)m zug~AFYm_@*r}BsW%W}J@Gn*=HjY||58F_1dL~KiBeVFBVVfCqo0@0JE-|nhw+<22x zD%9Kiv##$B7stAmki&eUKC8XznB%9!L^w}?4^EF@8=P%Q5I%4f3Q+YP}@6@kF zS2|4(xx420Y*4&1u_0bO$0_T7{QpwbQn{?hsXym*g>lOpB|AMwy+DntJ&vQePLbXU;oeeYr0>@57T>k*XG2@ zZ8~h_ioe}3Y#WROrzL#pWu?mnsleB{ge%5`nPWH?j6yYb)DVQwys+~ z?{W5->sM4PGB2g5TTL%r{4}I#)~y{4EiWZioh;Maz4Mc2PO{sfWgM6=D|TJsT!i%K4ea{IFutD6cqtSD48xpC0+Ma83}O(yay=WO%K zk|{9O>@PCk7#u%W`dm08$12HJ?HASr*qoX<>rL9V{WCj!`M>OBILKJ|>A(lAE1g+? z)K?n)`&qwt#{2Z%T`Vh}FsypLa(YoeyYb!M9YU!aM_h<1stEgvF0CPyeR)5^TJ2vTg@-_Urz4#Q(MEn zD}-ljUE_)Ke|E}MG#|_q*!SJmzCrKzmvX5TDH(oE**ll|m$t8rPg%~m#XaM`;yh*^ zFZlyp%T{GSSv>#u7lBo~z2%QGUwCQUySRV$Rqtv2{}%UG$i20&oyd34^>zHSy?P9p z`85vu{oxwn4D-({{JMS9CqCl~%#Tfvs^5Rea{v8`%g=lNOEo>OXNZXW*eu({{m`|0O1_H!Jy`>Wv4 zQ2TJ{bM z;r3_cZ))#X{rG!v^8XFzUt1*3O^R>jZgHyheI;G&zIbW)h9!sh26p_dx~J-UNw8My zcZB}BMmt%1FZ~s%OK#fFF|9gh>h}LvLT!M-nWLOy&UUFXSDpzu=rT?DT&MR`FMaNn z>2p+Cj~uQHdinFIiHY%yce6czU5k-y4Gqfp)8QkqiAU7u?)T62E0`wS%$ZOue0{^V zZr?fPVqEd;Cq<^Tp&q%i3$=>koS99VpPZ*!!@= zkJb6p43&Li@q2IiF*-IgE&FTyYT-)OvndAae3|>_Z|^=G(bTwi+OLE2CVvTjwPmhw z#woqUw(%Fe&ex`KF6ckDV2{ZCLltok+-gpE?Yg1Ee2@9C-mIk?+|Ip>ikF%?vE%)0 zyLsAgc9iG}=yBcJaa6{>)xpSq{a1al05#?%5I0^jz-VFa<^u1py^LXmPuZgK;ERVbrVkTXb7URy4)_xGIA-7$lT#EI7+v6P@y3F#eUWrQ8 zvhBL~?_R-KQxngkEwd%(E#P-OD*R#A$IaCfvW0mL>@b?Q;yII4|MakViL5p%b#nR! zb?RpS0_XQLFTMDpL~8d++r-Iki~Tdj?@Vc)cfF!+@sEORsr?<&ZDoSK2NrMp$!O!( zqsUspdgA=T81_9Bx@PMW7@dm3qxN+i^SF1b;%Mtb>0*u*I=zA?&zt`Gld1Ei z^74#qrJGd=XL4(}r1LMVfBZ#;$<)kF^w>umGl}(K54#rjMxP3J=9&MwGiP7pL+z5| z6<0FupZd!5bz0tr%FfCQ+(*_O`6+NCSJC5S#jib@7vDQw+G&&dQ#mU0*F;-+?V9rW z!pCm@lAQYR_3K4XKWZ8NWBwH%T`O?HqU%^i%K{IR-j96WnPP2@?y_4d`bl+fE5j)^ zsl&1{lj9!md)Sr8X6-cht@{l11KhK==-LM^`rrDn^JhWs=lQq&bNA%bJW1pGVSa-@ zE870z2Me}Zj=$!OecoP)lXmq=K3n=V+gJBPj!2NsoV*$K+x(m4L>v7s&Fk`CV{mTi zv~1-U&41S)`+nT(;i328e9o(QcDcs{6PCluGR^cB-|ON2^+kZ$oeD0 zb5T)jT3_87Wyj>5Mm2@?RbB>D^YxSp`F3^4zNnGO%H7_ndyiXX!sD%nl-FFBTF0+& zL*Bfl_SUia3m+Z)alK*Nd7b3G?=ubg@()Qh*|(Lq{VWh+PquKo&Hmt!!0%Gwe3Lru zDaZZ_oAhn;^UskxSe^)7r@<+t_ODS27eIcmC*JAH3wbpxSTebS9Qn0m{2KIJ|s*V8ZF( z{Erzww9t-#91UYW+|yC?(0~4CHtpSrpCLKsT?0u-oO9K z`{L5#cr*Pzjjtcuv{|nNd+eQ(WFO)y=!UETc89J%Zxj^}4ErN(JW3@@gc6WS^KC|VH(G1qv-3j|%^E>hf?J<{GFR^V((BcmQ zuM-$M;$~!i|+DKHQ7-{9p4o z=dt9u(BApwZ|4+e{J6whb3*)tx%dq8L$`wWY&s(G-Z|v7+GER@^z{o)=7?FCZBVK9 zl4Nn8wk7Znr_{~kSF&ubm)xwcUV3}tUj3I6&wH;tTDDSWv)(hWaM1uR5izIw!W+ig ziVk{D&&}A^{1X?-J~#bf@4Hq;>=LWh ze`Blv!LQ$2>`1bFCw=nWS*GJ(&Gxn}+w+K-d#%X(kM*8WSAQ9vKls15l*RqVqeg{< zfC*FF|M#k-1%@y9*ndEM!>^0=<)N1+T)iH!_N`a_lDLRxF8^Z>-~S!&uCb?o4ioD} zIXyn9;>J}?G5@pwG(VJ<*RA^-z5n0(|Gz$e`LSJx;lszT$N#Ut|1sX~-{#Hn^Zx(* z`EG%?R9=S<)8Vh#Qmz+Fo`ooLOXsk*u`9~Y`gTD_TO=)HhhC>pT;C113m2mj4$aU{ z$oO=#n3wO})G6UknvFFM4_Al1Qa`-7*};KVc#}b{^~_5p!F#&GFYt;)Ic>;j=uG>2 zK0run>V>=6O_@cUWkGQ%{#G&v7XHjADGt6m_1osI3q7rInvTk%LdH$L2bz?eta1!eO*i>* zUh>`kof96MtmdifPfGsUD`fXubeX&`=hC$-RWI6k&-%`L@Vtrn!^ij1+P_{`>~gDD zI_SI2;@^tv^UFKe?UtXyFkgFprhA>M@AW_Lzu46Jsdi8QQZ7+9|9_NltN2UxD;>A9 zAD+Lc|I+cn`-{4li~o83@A}afY4b|HJS%E%UET%p;J*?)YiBqeu0No4)4;~qW3Hjd zd%yCRciSFT+1x$&Ttjbx__9t$-~O;7g_Rl~x#W4m4Q#h8Ej2j1$9Tr0T?@~t^osH= zY;JipF;?PXQi(6GPuau6bG|rTietZZ-&yNSi@3^#>uQWzQeQ5-xNdRvN5bl}*ERGa zrhj-?pDVugV~gI3_Vf*5uXxR8?dPddcy>o;TGw^ngC*0qIUjuW(sFr|ziZ?64@Zl5 z&n}-AZR&aIj-AYs_g(qf;@dBLjBQsGFzF4_oj=pljf25JR7mo$>CBWjkrpg+J0kez zJ_+{QI)(eS!-X{&lj9V0SeAVFSt_DcX}5;y?3tE1aWgJ!MXJw{Kj*qA{P=E9tK^RJ z9NLTSk=_+IIp6#o#o*=1s?t< z{ymltw)*QYXM6EvX9eHWzZD$6e%sugecaV$_eO_(*-sYz#hllE^PcPaV&BpW84AZ&zB!H>^*)l z|9VW_1M%JIM`r&n=?&Zey*@v-dY5y2e)Z=~-@eJ%{rLQRuF$%Pe;szu`^QjcC#N@I zmZjW}*EcH^@7F%h+*98m*T(*+>eYMuj=I8G`;XTBpWl1^-=A+)FCDEcu#|@jL?D%W{|DJv%fBrh(4Hdo(dN*9TvK@|me`fmadgflMX`=TY zcYQo(_eW9c={nz)`v`9JYRej=jKZlX4y}EUMSc#PdU+Xm8X?$ ze@x&+jl&vKE~K3FRZzIYR-9Nm&E%PO^r=>^eb=XZPCHOiyWw-*!j~-a*A@wK9t^!Y z!>Vc72dj{iZ2Kmw&cDeeV}Ad}g(=|`vaeLkxx=QsI(jGfK&8f&G_UB#x-~!ZB)d=9 zeA2R8nQ`Ueqper!HLpZOU{ND1{u^XkgQ?zy~Jzc>3{G+ z&YJ%QfA6qY*LjOil9OgSeE0t6XHJcW*qX$SyP5CWVli*`LYr-F(=*lyS+GjY(XQFQ z<<`85o+it4?yOtUqN>{&AK3LzH^%wgt=yGi2cHWcSQ^Rp;M3M;+IEE;;at|IGMl?o zuD`hUi#yq7c6Z|sRX<(RIww(+zHqB`s=ww7>1<%Jo#w`n9J5^ zWT)N9{ddATfzKS`@2+&8;Cdu_>+F`f$0S&vbz6N~SajB5b4!5J;T!i)&k)c4vB|#i z{f*$557(F7%=|KQpLVL6w$0%kvQCS4@oTqw+iYiW-fwhFH#T`sQPx?LZ3k`?+jC#; zxcbSqR6_phnU?Na-9Zb#S#`Q)p%)jNTkvp)Q{BSH?RAHOu3UE% zU%G3peCzZr9NL$+9IlcsnUj~<%YFWKfL2V~t!vg#U%2eEEF(OPd2i4ksMe z=NxL!-o4<;hXYo6&dk1tqmDVR`y=^jU&GhRbtRdSQg(YUtxk>jVZp_6c(T`=$3=X% z{Lft0y0K?x$geKXS8j2y9 z4M%>dAB=v+tP?v!b;slzw|7X`o?+krcsFPJ!VTZ~uO*9L-`1!2VPn_aLp?qUPn*J` z{>eO!xa^kb&U}pfGvDWhN=6*M8=Hc5xST1mTO(38*X-hla}DY~osU~A3ihzeHLo>q z%Mkw}bR+1Q9w*DEoQ7jxwF&yOYNzzCiSu`T{a2PZ(e*KtqTTZMb0;kR zKXuAv^95U_r4Bd0*~`_fly_?n-z~qEXS%Y7mChDUinb20i41Dr!Kq{#;_y)QYSo1I z6>FW_e(N8Wzt5Qw{w?`RzSIRp{@#o1vvx~fZSz+s{@V2Sy~dRt^G`FB+RTY#K9=pd zbZK+poWre-&XTXL7U^kRY}x7%P~zNtZ04#kJ00i6FD>8w<@;#2$H8`c%Qpx22ZuLo zvN(0{xs^^@*V{9UUsNMFR$lb$`&XhdpLspoU$wXW@9wYSa-N~TqCe=%{w@F7y>7e9 zR3ywhYA4gm_S^gz$v3Rkv~xYi{CdJ`MmdS_v-6+vet7s#@KSY~c|*i{ z!D*!uX*b-@ZTZtA742Tw{Htj8{T1^SS*GpS^VL2tU|&P-PMJSFT%Qkqn||fg(PP(d zvpv3Y`-gU%>AAv{?~Q#bzw8g*#Z#vD@Ji6bR?amMb|*8d8d4dIGyM(Ug`p)vPH zfi1Th>U6hC{P()N{PTIMB{SaJp4*vbxrDbscY2E3{Ep&`n1{Qy*qirlt4`ao!s=q3 zh$6qHPGrVkne_(!-Vyt>4D~smS4G@ARCUg00r!_n#(OUv)%h|l$(WfdUsqyQ486Kbt>&bkbF;@3UL0`rqaT&-r+(WV#S=o^6BC4XT`ql zT5v9XR;f@&TyFf`J-Z(6xYN+Ba9}FS`u8*Kudv%qyUxDvCcEY)uNfkK@v?Eg@xJPe zt$|J43OgE@>&~q&^1gnK{qlpqNAs)Ry$oXtTx4JM?EPG4G0h($mp4{zu2^lznq~0f z;Qu8#ZxU9;f4UpKPc!ZJcUOIZX`Txe%kBN0X zXj4C)^uFtE-G4d79S_!7aC4Xyjg`G0k}8Cv$s|9kx7e$A8V&um|(%y=TQZdKCb@Y_2KLL(GJnMJc@ z1hh8n$ZqmHcabA-wnk(8|Lcpd-frRAC-Exd0$YUBSp(_p>nbX{AM{7*oBT6;u#n?| zPSeTuV;?$a`iOXM|F`30>ZC6L+-(L&&tLGh3uxKKFEJ(Zi9(^r5s!K;^R?Q4&o_B4 z%iQf4eaYfeGTUW|71MvWcE96jJ~frUUmumes{VChfAe!yhsCy~@k;v) zAF;b_&rhlDslUGA`;YJ9LLK+2eVeA2xBWlX;nDqIsX}zd3Ifd^!EmTIPT4 z$6k$n*?ITw=kf2n@i11o=XYhb1GnV#vNdH%O=&x_Rc)7DIo6n=lWc4zWFFj5ZX;CQ zyl#H>T=CMdHqZ5Xq?UH;i^{^8vpTrG z>RkTe^=J#v%bqLjbFz-L7Js%B+kM0C>fekv@2Y;}tafF1FfneAy7Q&;R*jq00^9z_ z)Sf*XWt@8?zp?C^@fp6Vy)*KkO^sOfqvV*>|G6{%)b5;K<&?H;Q7(I{f5}1lo(HkI z?#!LX&zWpFd&Pf^rM!L>pV0E(7qUttTAeO$q&qW@JRhrzawnf=kkCADkW#hEJ_g>Sw5_ORBbQ)gW}U&aRiYXS+D%QyUM zKlOWk!6N>9i@W$^-W_n?A8^-qwK$V)T!IbDu4f(3(>L;eh;6AU`Znnj zZ-1Zu3*(C`neIBM%hzwJTdimCY~|}jYdc|=+85%XE7)s4|7QJep)L)vbt;}Po}IZn|Asqddo?OJgDZ^`oT^%IzC zIu_REr}KYfvH6z3ZQkftqsrjkIY{xgBjL$i6rf_@!>yy{ByQ`{EZ*haq!%T~wJl4c?zet>-#z7$*K5m#UXONs|KVA4+~7;a+8?pncB-+Sm6D=I zzq729Jg32Y_$}8D!4D^r78-x*Db9mg!atq&G$SaR?A?1NHYBD8hp>WN-@Yf~d_{Ws6Z?Omcz za#qEKTQ3BQv&9RXvzPjNt?f}}xfA~I%rX6YE_cNX3U@48x@|w(<5zQT-MGDnu`ZxO z@$208B~NNyAA2t=>8)(}DO`Nq`?&AJ^QV3=tQYy2z4ZOcYs!z_NN3JJ%wUyb7AN@L zwpIJoW2qk=KPSD`-1=8q_I-@Wnk97x2kg&34Zfg0^;@pqC2^LlUkg4we{^((*sB8v za$QyA{vQ7`vF?Y(+Qg5J;np8=wwyCQ^V&5gd#_EuLH=$2HoJVU*4Y*fb}P>*8!xQ$ z%Stqumsp=rw7ChHSXp~c>ODk zkv#Ha_OY*)&y#IiPbusRzufUnbbp&)t^I}4&g!X6A2YYsKaEJP$@;Q;+P4D=zbvz- z>%BPgQuV2SuKlt}A2-zHYaX}wvY>lz^0bW%d!wGLpW0_$(7Wqm<-V@j8BfIvTmP=z z@bitpV;9*^=5F=}rfjJ*nCJZ_T2yx8TlLmQwKCSTuTT7F;34_#?6FUx^9*FR{4smt z^?r`o40&zYE{J+?eEOq^PasrKOncHbM1ER0?)mNl%wyb8QGos zv%Rw3_nYjjtKb7xkcC9>bi0A**_`8Q!n{rgWwE0r(zO={9;mYb+mlT=*&7P4scVV3G z`iAhCBF5p@<>)Lyi(1h@kMLv-qvZ(F|vU-_uY3r6TIn5_My6# z_q%&9%K!dr_&mCH?ptnwHy3O<-~SJJemr-L!1l6j8v{JH$-8_u)&H4&kh{rOyx{4v zk}H3Lk6o8I@4h8$#$%rXN#i%kyZW6SzL>}a_7|QsnRnAtZ;plztA6$Q3sTP$qrdvNT^Ka-Of^I~VsA?dP*Ey;xsyP3C&_#{x+&jtH?`@3`-TuNR%y<)HoUc<_b& zVK$F6zw0x9Tb+CPJml5qM;tz<;schxbltP5 zu#n$y+1-|vq-FdGIsa?PPzTUUV_`*Z3gd)uk z$DEq7&pB_tXZm*T{jV4J_x%f;ANlU#gRWKiITl+cbN)LzU3JG(#`H7$_g3z%js9Uj zmu0)l1GRtmZxv+hj48iv%OxtF#a9a!vOWJUc2*8 z6EYdLKiR;*<=PtIs60Qo=Rdcr_T&FyGQT?iueL2_||Cxzj^7xbGHk&5qL85vsO9^9q~nf&*;a zI~F85d9Ll6|KGHB&cE&be=g-mGAPv9{L}yP>;0V#j&8G@=X(U|7X&i!x?KOU;=lua zi!Y}H)-#!yJq+I7#d1Y^3VXNoORq(NCwYW9O-&57w*)sQrnfv0li&(tvi6vFA)_U` z20*3lF9}SiI#hOK2lwYQn=Lp?xnTTI_b6 zPdC?n$jM|a zZNHbR>ZA59{+}r)-@zZ>c~ANNK5oKy(cD9CzE^>7orC`SCbq?F_fOpUa%yr2)5Y?9 ziZlMo75@a#qJLqy5%Pp z=Ptb~%PE(%eX-QEXM0e6 z%)Pbqmhtc(&p9KSzq;Hv{ou8WlQ{NCvwvxR81bI(y_ILXOXabOrw4*fTi+ZuKhdZj zTC;9tz^~XFpGzdQVnm)_`WO2#? z@7~G96w{)kxH3EJeD!4?>ja70H{Pb~-R^&>{Oh60HA(B6bh2*Q?=%(O#aS5htlBKR zo1tcJ+-ufayL^#ppX*oIRh@4BH}S_0{mXfdMH~NLe7121%L%?$MQYPi+ew6-3gj7Rk`zV<&x?>?t5EpR#r6LGq^KFQm#*)#a||@Z~4~C zmH(^0oUXOIyExeN;^_sKJ#TM+oqgqU<{i`j&d)0RKi~XNC~@-E-GipbBBE{Qv^SfY z`my&K=e#@aIGyRaM)DQIxTtegJbS(=eAx0&dWqM<=lvI+%WuB;Vda^Sx!a?CD|}ua z$j{%C6W4U__m2xA?GHRn9lx{($o&nykr;QSf%Py?VUfmp$N6_p^_ob<8lTB#a`|(x zq<^%S# z_Fi0{elb~IKUqKF|A)$lkDtD^k~=DAS8?vSds;=0k@c+hU;jT?^o89!ZePZ~rT<5q z)x7PsH?LR!FZeHeiTPzivb|K?S(E;nfA`wA9=`YZ-=7HSx#bf27vA37FzHw6zr?x5 zF1t@0uUqkB=hIoSW@1c{J*Lca-&miWr`92Nk^AeSNuS^Dzg54hzO~qH*8WR67Iv%C z>wR^(I~VLca5bj3@m$zuzgY35SNfQ`9X19sHaMNvT`l>4t=O3t3j%p(l!Y0~I9VAV zWIfB@?rCl=yl!^j^ZR}8{&62IX=q)UlP13XdSZZMQ?w_)I7|HTkd{le@w1M$M?F7( z(&E6C2l{laJMn0mFZ+}@0TS0pmcJKcW%QPLldS?;fI^k?U8zgWgP zoAG@4k9iYzWd5-#x%|d_$AWtgs$YH%l>Gl?>BX{T>le(+kNPe5e`|$Qc5BqVhvAmB zy`OlJjn^gf+Hhs_OHEfb^~i4se}2KBsZ~~Xd-ugMjb*`i7W|#M)t&j)`NroAGnh`j z7Z2PwwL3TC(YK8KrGFg6A6#TUe=t_ba3%f1mak2^7U!3b60^>6dY91_^NQUK2bOOwEU5~WGI#9V z9MONx&rzGBD}PVzV*jp+Tmy-JTMtd^Q?;$rTvs|b?o&mh-9}b9&11TErPm+&`0Qhn zjmrLq6WME<9)_}R;r4x$t@PtW*{+AbZ^ww=u=*QhBY%Cxeuv+xkCt9`JnsK+uEWvh zDd&Rr1=olf8rM9!^_>5MAjd)RnTZFa#T?G58rGTSP1zUUvwh-)I<^__4`$`dH0r+y zJtp+o!}Q}$yVu_HA*Z2V4e+{{*=y zy(pD_eWUrD>N7{aW3K5Ff)4O}j{106Sn-+hCG)8Hk+UZJytnY)p{nt!Y3)UW$fd1msCpbF;eD;nnX zedW8(EIx73+S8|waXcj>Zt#r@lB?Y-tanAG|}YJroz`K0{!|4+R2it*R9 z43qm{#HN2N`c!$@xg}l;x7u?=3w~cM?J%jNt>C8PvWNAY*TvN|b{ev6xc_`vqNh~9 z?6cp7J{RLNYaBP6NqDX&dfa5!dc_Zg-l`vR-u!L*eV1(-8{66YyIPgAkKbNgJmbv% z1-$d(Gk@wnvp%O-xVGT5tZ44YrGHZ5H~=`r)UM(H-oyb`WG-%l*uIqB@f zuH;;`cME@P=43sldbKRNNn2y_?8qHYw-`nI_cq{L|5)mET*CSDycX+u-&GxZmbmzX zNQr%*PURe*0|hzBeAD%w?fQO3=da72{f`Z=9W{KnzEECneM!B&wEm*E{^}>LmrC$u z@_GwC<6?OGyK3K2Th*Afi#zw;PZeXa-haPEWSuEn+wzYSq^!f+r>^~fA-a0U{q5f) zPMUh}z5OvQB>ro6{+=VY9{0RX?Tk}sH;Xa)^ZfUR&E#_o#vpSs3>GIi+>7sVIP|W3fKO^7gsqS!E zWqi;6O@2gigzwXDx_&4+Xa82q;< z^xl7x_28+wip7^-@nC&PP^95Eked; z)j7VENa?SUNlosrGOn%b|1T{av1Pu__pr1s`8_FjdW!ZG?_2XEe8ZMt*UfL@qU{5$ zpC5jd#ag%NNrS&%0LvsZmg+mtQv&v^RgQ6G%Ks3m;{G!Bq(#TARHd)~E?OKpe1GRp zU2cX8^Ou{yTs_}%ez*z*mGke4&ziLHz-6w$_+^`<4E8pBW2v8UMSYzp&!L{I7=v&#ffMhQ z6;xLJ4!GUW!Xw;$&_Yry^pE0uM?tl}skd%22(r#HDV(IFnX@Q4$$={3XbhGX+@kH+=& z96}wQ|3jL%SsEt3msZl@_IWyAPi4n}rtQD?HXc64%5vqq{a>xa{g>u*9_Oh~x8Tng zR(AW}^N%g?hoRPC`wQJ`l?vv(d%=FSE76Zez;9Lv?%X0odDz{ez=_ZD+EM(o?4 zR&lH(@-?gS+o-l)POcBZHz&6}eCEgMnIvky`0V@SAD?`ZH80d&kIlBae6ePAkwJV~ z_=5CzYS!AK9y{)cy4-nPr(MBQe)IRO^Ak&C=N|R@^!RJu(jRd)cXO)SmY092TmJR? z#)f4#8IC>XV zJ=t~UgYNH4?5Czo%#eC|L4-dcqv7JJ-AC8Bhex6UZv$o_cl{K@~E?w^tTYp_47c(<|5!HDw*46>UzjV4U_Ubr&Z@FDN7 z2Y()1__*P_&5O9e)UVe6_C8K5)?$A1WxnyJ0}DzVnd8GV&R_3-8r~+K@c8x0k2MB6 zLJe9M9ra}Oin-u^UEsz0Li5_Z$I>AJtR-g|cFG>e(&zJxcxA`YH)DI3Z~8h}13Mpm zha-;W;ynSb^+aer^@K4#@}%UX6%ZG8BF!ppzcGpL)nemQ^B)Wh@CX1wM6BJknaof|)Qu^ri$^LUZ` zDQl^X^ET8UHWE5$dOLmB{lNVZf`9LAuM7X^<$L5?dspT9Y5TP{vlzU8E_ry$W1h^? z>vs%$zFz)f+qT&(bCJ#UGiICTJ-+^@=IDnWS7A%rNcE$0LKbWbx$bVk-@D!4{Jod$ zQ;({^$*j8%M6@vR?76M*()&k(FSCTH!PYs!>pn<4KQvo5+2zRA&p(Cj)?IC|z8?Lk z=3A#9i!Mw{i?pWQiBt*t!zp8dUtNbco7PV)m=g)7*kGyR1 zWODsl@rh?|WdH15(WA!URoA-EXp6+83%v`CDkP?LUDx`yC{iO{Piw~QcWRbmUFodH%GNzH&NJk@ww;6pN7Bnl3!g(e)l%gdh4U@%lr~q zs~YMWuFsj5Rqt1}@>~8+WzX=7Rz2Le-ZFMqKQ5Ik)-5+$=hNW&YT>;39&2ltJG1WD zWIID?|9^%*OYaGN*4+Ai;p3}}^QFEVsyL`#C&+#Db&1Rpxw{Ju?RkQV>*pO|{CMhP z!JgD@{i`mX`4Z?Y^07+hf%INhyVhgTw{BPjacR8%DgHQjOC8T{C&_oym2Z76`}RDa z?blO2^*!f~86SPlBKCQojHcNP^_dr&Puya+Y4-oARA%-5QlKvHsm}8Jx^LB=E$%LM z-`=i&Z2FQr;zzE{ePF6rmi>9BP1phFX@J=lq9%?@HMG%)3x|@H218DPH~yxyv8>`rNL`k`TLB-+W-Q!F~;9 z7n@03+j5Sddu4c?^N@v`^}T=08^k((?_?|amy~l{=Iyq7^Nt#~&s+CR%0a!cP}kh% zo$}p#lU0u`WVmyDank|c#Lql-XW9c}8+r!N&P{W>kN2T=xE77bTu? zYrKegY@7A_TFZH_hrHhx9N9a&_w&n)-`5RhEx2Zs6t{S;*Y)S-D-w5Hw|lW=TVY@G zrI%%AO!m6jM`^8*nP(qf;Vlrt^!TsT?|W5yf@=PG-RZINSGp$rIKOE=^Mkc-nT_N> z#&AX4tl*pyo28Px;?^F4#lN`DFIo~Gz4L}-Z4$pXi>_(a`ctp#&TRdk*7$7p2gd)t zef(VOlk+BMweEWFe&@T}8~?ylH-8sc^DV1fRlq&@*Y-L0BV?L9!grXzefvkE@58+H z$AZ59tZM%qeD=Mee12g3`HHPvh5x38KcQ`^KWtT z*Edcoe)VYiyzKaWid?aq0$F!Vv~#bY@z~zcUhqiiC#8Mv%7^c}?ibj9_(SW5<4q6D z7r(!JU*&i0w2bemoG!Q5dnW9dc%VPlvmpLg!WQfO>{5RxH$DH%&FQj|ZBdG1;v@Zi z_2(XnFtKX~9(cS#<=4Gm)#CS~8|9yft(<%C-MX#mV0lKQJCYBE0D-s>pOF>Qv%l^w^6|EXVL%X(gKbfrX>xlzR?za~!N;&hu2 z#g=(B;dh!3rPi=n{;cSv7k}b8JW5{FGqSpUkq={O*`3w3xW4gl{O>bA^J5tv zOn=1Rdj9{roM~rwGrx9d z4{y%vU&{Y)`x=pu+~~Id_X-^rr%!zMzT`e=I;Ldux$vW&X4h;4h% z)x@Umvq@~Bq^LWOL)t$1CA}HU*@fA%`darr3<@S#th`X)Xsf0d_*6LJcV{ch-1N==n8Wfs<>syX-)A3@{II!|=f{T=`$;CBMqWdpxL`F4I@*eDu67&yLd#+oL&y zZS~_k6{>Qz8(&QIWBR^(`hll;mP=ed?%c6>zpu;x7O_uEi~qN?G76RM`?U6huIn2Q zR;EiImVRJOa(UsN;rnHyx!t#q{Zm~O&F6pFoET&4NfSyBCmBi@_wyxw@JZtbF< zJ-fH{nk-o;uw!?^uPHqFWrrgioKNy;ac^S&7PTNR)8YcFwa3hse7ENpdemoly*VBu z`NzEc(2IBPzqE>n@4md;^l)SJrEZfoo2TUYGRsYWdb&W$>;i{U@AGG;?dKd++3oov ztwglIfyYvZaYbhJ4>O;qnWwk^P1|#;@Lxsg_L@ye71iN(g1_$A?5&)%a#`)aEti@j zjE#6yWp5wk{kyL7rTxHwRtf-_@L^_wse~d7Gfgd)U7| zp7mmK{oAcRd?EG|WUoCs9WKk(kmz;8-I@JZpsvs?Yo&wRrHv);%YHi>SYDCAb*pa8 z1gRs5P8^?(@EJxWoN-z#TWCBtU+sJ9`e{Gb_06&iOV}W=a%DoIHT$HJ9V`nRD(WBB zGIsox6!^*ST)$2^f{ml==ekKUZMW7rJh)!4D|5@CCPoRKLl&7^>=sFtT&+47aP$5C z|Y=#F&1-N|Kz8$ZtZ zaG|7CnML8fg2KkvN$zwJMQa^o4@*(y!>G>`-AX`$Jc)DSzRh;ci}(# z!pFS37TL9nf2EM z$NAx3E~aRqE&4k4}WDX6fZW> zdoQ*0{K{Pdt7k5ZddXT~zi!IJ0~XqLw*xk*D%(3FDtD>uzi`$|W#k*e`Qd&T-}Aptrv+N^CW2U$2m0A0X^`xjHq&Ou*sC zq|CU>GIr0z)5^l!)0T=qRxzmIE9Z)?)?1Uf@`1XfeC4~jGhA1`e}1eaq>*v=jn4Z$ zSz6IIUOkRFZCInj>yD?wA5KhNxAUR!=N*B)Pmk&aA3O6hBD-4Bgx&wxH3`M8_u-FE>8&_k zGCk|(>x-|V+qmAEzNlxeQt(^Q|LElpqbZ*>w_2TZ;=lBBwsx$`W82H$K4-k0e5qBk zxa;}P44$*oT#vFJWbZj9FC-|jh5uK;!@t|hv>$YSpr%um!eWupAN|YWbzfiIH^%8plg-YK0Q~mhY?Pruz{jl1GZx3Vf>F^(+21{lY zE`0sWM(+KTkIJ*YoI3IDk<^!se-}=v$#PL}JN{Z@o7umGZvTtF*x!G@fBWm7eD<#2 z{@&|V`|X)!fr{-6}5q@j}jZhh6VkKib9km)*8T=G(FJkAMGI zH!e~~i7~XniQP;Zgo%Q2|MlbeTeA<^#aWUcRuU+eN z;|?yV)2y(ZbGeBBesJr3gYLdBscA1(uWm2g8J)R%>$d-{$2MQ7+&vuxeYD}5du($; zg}m0AM?y@NwxWUO#EaoAXSyf0g^b&Y#F>QL7o(yNta?ta$HntH!0rb=F+S zYpq!NvvS2jkL)kD%Xsqd?)3g*ez{Cb>&@R+zqh{C*ABY;;$fH7kk`M`47{G^4S%Qzd42)Dl6BCk}N(D8lUQ|5e!p4(<} zM?&*^&8de`?~KZlm;JuL^d&NA)%p3Ef4!GEr@n1x z`w?GDsXg;@lNTc7%^YyvxzrPuZXyv(yhBQB&ugmh7+iE%Ak1el)YuA3|zp!I} z-{Bk07w3D~r`E7W{e8Ik$m7)?o!ex#7dg(Q8t8L3Z z+E0sk-a9WppGni^@(-nZ8M~QTSbpfHGg&R(_rBzsh}U$fbzHA5+}Ja<=))0z$9qYV zI&ua)-WQ$=iShXz&)Gil*W)x3#ZKYUA`m>8TI%x)LY) zpSRv@k+?4PNj?WdR&}Pvg}z7Wiw=mjw}x|N>ux__vqSVxrP$@XqaRA#tvkQVoDn^{ zR5Rg2rFyXI-kj-NceYwb*mY=MiJy5$dac5OjNes)Jl7T)cSq}S#U;sa{B(W8hDo!t z4Vh%u#9D5>x7A|3c$dHRtkVy(7fSEns@K~0xa;HP+FLrV9j+groAK-~`yehS|GN4Y z@6t1~znp)0Kg}XpoT1m~-~YKEK2JJtIe$YT_kQ=EdgWiXFI@4n*+swBmC3h8?eEUk z%lK{%pf67Ev*&J##zr%g;TW@`9Jw<^D?*f!cCy z#~=0IbsFkl++Q#E@71^Pr$2ebEK$t#bJblnkR z;%#)!d}+e-Yh52Ue_}cP$#C=Mj@>E-Jg+>;*2~}RYq<9KPkh_iBPW(?-6+@ivH#Rs zy*tnMN7pS^JEXewF1Pjn?F{zL#S4F=et)(u!u;DcJr=$PzZke8B`|=(RlukH|m@GL;eT; zFZ%2CzbJ9pm$O@Tcy4-=@X7R=%=PZd%e@c(&knSXK{l43`O>f1Cs>Z$Tn(N1)x3EX`_6FtNEy@Ser+fZ&jQ@~e;8>)zB2BQy z@_@Q)$fsEj%}aKli&EA!Gf-RTWOdQGty}S6>?_qhKa3X zufamb*f~txOE-KvcVoo~UBQAowY#RVKb6;=ue|H;(c90yHO>3^d*PJBlQeWJcqXU) zKN!}O@AJcVl2h})7^jowxB7Eb1@`T$Gi4l-@V;h{jR5Su|@KPMbj+iY+dBg?qI}u=+%sxh$bhsBS*Rg%Wltj z;^89U(IYPGr2gVCn~#zFqzx?!Rw{zsyk)}DPd!*({V`Ubb3-_K2Jg=WcHT#%pDTTT zd2ZL;y>nMysebQndi_?SK$8aJ_2#u}SA~bJU$txbs@H!nEA0EsT2o)!xZtI{&$5!k z7xy`w{;@w@>q3-KhaUG9F@YbO7|bTkdhzPt)#>}bwk&@=GkX2qU$r+QcbtF!?PLAt z`|k3J_Z4zKuK)Gp@*f*}wa{Ba|^L+EankSEork$y&nIiAs?`gg&;=cXG0L`a+XXc(Mj=yJr zYud-%vb&fj2ewO1zp&x{^C@o+*Go&+sAbv~iK+=os2IAxa<1dZz1*fIpmU+2`HSN5 z30JJ1_`I_IDtRjU27^$7f907~5BY^#c)xS9=Y2LjTACs>Q`6+L^xN$RTQ79HRa$gj z^m^3JQyIqP~#w`Jw-KOVY7?axf5 z%yK31OJ?U*2-SSP>u-5nTc0C#N5#DdQ6G)xuV0<4_NQ`wYoPwytE-#B-Br}`7r*pU z{(13XR;8NRvHwwSTg2A)R~@<`|D*fco#pQ7LYnc?>(jF}FIGKGp8rnuwS268jg4jW z_a`=CbI!l}{PMf~zr!;#KB#8K+;Pq4o3L(~`KRQdV2_)eKHnQIUT6z8*KB+LFyI!a zhHB2t+!Hykc@>YcD={rUl@TXn)N0QD<3;7|g@@`c@wPgt&rq1N+ArHZDk{MKf%B%1 zTP~a|{Tk%6#3sah!z;djN@re)?5VP39dl9JNy<_7=Gn`zA(D}M^h&wej{lUI)JZMAAU@)kDT>lDymsJ--&uKn{^pDXcQ z&d;RvJzia3A+9s?Kq==*%cRi#>F1VZvxH}S%>4a+|B?S;v8Ox!HSUj-6>+(*eg8zC z&AjPi|84}XDJ;x+e#=SX*Y;oAS4G>|-P}EQ&WWGp-s_IlR6Q=Mn)9$#=&Z?$7gI@H6$TnP2Ur z+0Q25Zf%=c6+NxSnAL0PizD0Dt*%+iJY&t>*31ZN`{^RPB_8cBoA=69$o!m1gnls7 z{khJUXDqoSXdVBw{Dr*;F*_s7|Pt-k!-Km6XzXMA(>y8gWTZ1?Ix>2Y(WSRwubx!9+2 zK3m^SycoJm=fUFre%7At=U5rP?*6U!s?|0zNBme$$=#ChGgo#*ZeYlEWENk(rJRYE z@sQQpuHNlOzm&b?`dZDxD_=8z9al2HnEaGL?$dk=qMbW`op{dlH+|usl3lxBhq3*7 z&(nKx;a9d(L0KQyb-tT_)-U2YSK%$m^vw%uUro{H{dTWu%d=GzzkZ)={!!-pnI&c-TjGkQ zKQMZ9YR(za3A0OHNa-$I{CT&e#;Fxc-`AarI5q!?$)s;`k2TFFov(6wqORa@fn#x; z!rZMMQ0*@m8-Jc z@hX4ewdcQZkA6G1!k5K`&eJoyYM2gJ=R7VBo^t(!x$87LiA%pH8dYqZ|1GmwYDUrd z7DN8azw2K;KgzPFDE)VA^`*QsD%Stjo7`1Tjc6;~?HO8e?|Au}`LjbFmq$eXQ24Ig zf8O6U`P|!bjXU>WT0D%7HJMi&QKN9d^x*r@uG(d7tVc>@Lgu z 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

    8YZ&ZgI{OxE z+!vIZANO+2r9+7m9`J@Qw3&I8NhEEWF!MyKtYUfh{LrauxEz)RF0+?PZJZo%S@L{j z;ep((C6ar#KH%|vsQ=0^E;I43LE(p=2R)XYcyGm#vPtY(zd>ffC(BJ~DyL5;?z*@z zq4VBT@thOE>z{}`$Y;A1n!D0Bds@0xRP0N0|H#bNlCX5UH|mkRHq%RdYWXx?B)pB5 zm{oN)CxMlJ>D=Zvp|z{8U-DXT^`zz6qnZ!zxELCpao;gJUHZ<)4>MVAyj^`M{MaLv z?5n5xjtQ7New4wVef83Y89}V^OCOf&T#;g9Df2lq@%zSD-J@**zf96^pZdxFJa@Im z;*=BXE-vysKH+`@Z+ytTyon7d&Bu@MFkio8rEz9Ors>XEa#^uS_2-sfkzWB zS~Tjzk-J+$>-)c$o;KD!;jZAGwmEr0{{16mpVlcaYgw?JwcmxyY2IELX@B3jP*AoyjW%yxdo-(kd^JMJo0Ro<+oR|8QY}RK78_PfAq;Pr^*gS z$KbuJ+eF!~$F&`co%P`A{h11PZaCbisavs)RqR@z<-J2E*R-7I`cZk^K&|)!qr)P` zFo}})VW)TH8ik&FchB^5sjF$&z6j&fE*=@XR_Wy0_4$CN`l zbDuuk`R-I$$k_+Q*AF~*{ck!mS=3TrX5zP_9;ruTHD_9K#F?#`VH+~3t?+n4cx!&p z$@lBhKQL~;=&6>s{h6}#o>tK+wSZ39!{@c$hPs|B@9^7_ar{Krf~?HTim$(H`SHuz zv+mj@OL5Hy^L8GVjS9XVrT6{8`t0s`m$$e_RB+)l zuJiJ(iPGnKXSQxq5O_6v(~;7h-%DOcoUi6I{V`$Q1om@YY`H4l+E?OyYPve z$(u0dTc^%`z4fqR+O7R>H(I=3l9FNR)x9=BvrN7F^X}g_PWbE<(rM1J4u1C}u6pkQ z*^KxWqboO0omII#^`Yd3)02Z-Sr-&D>fSM5V5z+Oj-`?5V&#JZoC{{`^T;*dv*i3g zC!@Xd)0W7-_@$<==GZJXf5DS^UuF7szq+9I)xkQdedDaj&e4hMY@_GdUDEu1RnHN&SbE5>t)qFU+;_RF9cdAMj>8|(hTLfQSmtS2JAsMr6o#~V>Mw?G^%{%zuL6(PHo$rPG z2Q3$-pXGU(aZPSgU;EMKqM2{rf8|-dHT`W(i^*TnT?gf#gzvCx^}6-t!t{4n6+R_? z5lpK&t$df~dFezNU2&;Ot0Cx@c=mCySwZFl@t8uO1;*7d~S z-u(&>WAB_`71BGiZNt`A-}&eLOFn+Cc=LQ~|5%Bwo;M##XLQFW?EQV{+%@Aj+*^(v z6uxj=^bTwJx-~V8Oc^b~>oT{AKlnUh7B|;}*ArUqO$}}Ecl>;~>enI5c#~tRl&`00 zKGj$q6I1&59s|F)hRl?mIJMlaFM^l>wY@zrel2<$`&rrhe$1*bn_7KC{%=@b`D2A*Ftcq=^vaUuJ0>O34>591?yX$IV-a!JR=VZNMu}BvVZnbHz9+7pTrj<{--_#IzSuLKSLMh1 zRy4&~uS+}4qBrxaDC7Nwjb9vE-#B<^YP7T0oIA5`2iu)bM^5bO&RQ0`C@`M;pmf`! z?kkr15?gNH?3*$9i|JvV$Ilf4zWxs?-;u`W&fjY)$&^yf_`WG--37zLYoERO#l1!7 zl<`c#+@JNQu6%rFdEWD+v!^3xx{7STn~Q{{+o6gf=?GP@U2@^dG6uE zLl)5&t{vU5Zd*~#BH7;y*M+sSeA#gEwC*|?A+8TwL58zeoL%_r!|SKz=gU<4o}cjURa!S? zo}|T<1CMU3e#)|v>C%zln@A_<3D=UX6+GuJd8(lL_ry=> z-i54oI$OlL#r77>O0etiyYxBMB-m-iV%B}@J^GGJ)xLNqc!~P5nlJrFK2H4{`1b9> z$t@N$d#m2b@Aofe;oA|}II~$}kHib5x%U;jp7}(tZ{ao;fA(Z~7Sj=@kPN;UN5yUJ z1xF6#ahg28uE4U=@{7WZgn2i`&SsUgY+dzvmZW&r!kOuP`#AeHhVd=o@_M1y(`P5n z_t{qV7*lBYIsvgIH`l8e&p5!@b*6@w%|XHP?cRh!58L9U8wGEfnI2XDBjB{RI#B<@ z@m)I<=JLJ^ms|Cg+uy?@viDHGluNR>`ONuOXPnC}*>ZKlmNtWDd$Ls&4c0C)v|P07 z%FRTMi|xWv>&Ke5=&mCkk=y0X5I1WP1$w-(Kj*7R;Z$=i#oW zPcD27;GJoAZR=#q=771y|LCZsRc&J4Y~xM*!3W9hA)PVqU?S6m-XaE+cHalEH* z_TIL=vrA;}1nyrd6%fL<|9b5%mFG_0JC5FsbZqbZbD^PRgXWJ1m9P0Tu1yj@=M{EU zc#@7w>;lHw+h!e6^w#{ksO0GZ*|UnbmaP8pRq@vAgaz&sHsOQCg}Z@aDJ|| z?t|~r6;p+F{R`rMx^BCvrd+jc)){gBA~*KN`HQEy?=V)3+@!o;VSZ%aq?K#7dI_*R zE&cL+>A{d6XLypHOy--KVHLjo)IAnIo=o>{sjHifj31s^aBh!W;ki|1Jv-cfEqXff z-m6ZtFJ|hKkFwNk%>MW@`2x$8?$XH8hPBGmw_Y+_erjR-4(5$9>fHMAuX&d>U4LWi zm~9)Rs%=m%`-;_|&mb^aLSxqbCkqN5_dKpDy|&1DQDIWwyVnw28+6O&m0gaQ@v=aI zd-@so(A*W%&96PXAR5m7H$3^`t82H?r(lh9%GcMJi8IHo4Jp2FyS(7U z)6nT16E!8{o{2kkZ`F3x4Bjcuuv+~5)bK`cJ5%S?^3s<-C9$5|{OWp_`+>Z=G^ca? ze5aC4ca<%8c&V!O%K@G(3eDOFCY11A3tVe*ML@-&D6E$6R^i8(wQut;*eo%4&a&!( z>h`MET_(MCUh}yc%e7y3Z#^#fwS4vzMW%b~vHSg|Fz;zqyzDpMcE)kB6`ed+)Za$h zoPA^&Gjn3Ia&UuV?Sywz>~88jEtss@+hpdvYKJ0^z>%rkbC>?%eSGxunbl~0&!g{A-#O%t{#kKF>}6`5t<0zAt8`tvl)RzO_@h4ypuaBp5c{@wI$bqW0{oXWoR2saq13);PY~tthj8ZOe?sRTBc1*x5fE8JO`?=UkaeNyrguaup7 zx{hzpsj2A~m^~BzitJf){>FP3r!}JGUu>!lKaIC%@eN^>6xhGW>;IcgwQmpRZz_21 z9)5mnR8V})NeR9MdxV0YN?tv>v8a`S^`f?m{K{0(G z=KGWDCvSA!FsJqH->Z9^!f&S;J-(~EV%jwxxxKNK4@74B4to{FLd%5 z_wP$JB(}U({9F86?`_^Kvy&Y=_?DVY=3RO2T*Q2rD8|%@%9&!Effr@6clQ*pUZ{Mx zXp>M?@$0@bA4KKYQg=@~UhkRu+hP&R=TEN`LIW36f7%??^fM=@&u;sotENkCo?%PB z#8i7q@=PP2Tce%q>jPh9Z$&n54cl-c@WI0_`-3dIjxg0^J*_dFsmRfOCp6o}x_y0} zynnW(*r|m}%*ubS&W(3q%oALzxqI<5vlT7Mw~vHfdvLEbv;Wr}&Wq~|9G3~EvYbBkgDu+U774D+-nx48PVS>R)*9PaU0-&zGwROIKNpVqq%K%nn_9~IZfW1u zPlC60>i&*hZ`82)sy4TB_`J)zJ@0G1v%JGC6VV&#GC^)psd&oAaO;`!3ArKn_Rd}K z|FYiWj0uw_ntJ5kdc4~Ffy}vHvG8M0F8XscpPoGT{41}!uNtol+?y?PGjbwJWy5}( z_h)ouE8pKMbdYPmVi5j5a!pZCxwlPs@C9Ry#6#A{;|qRIo!a>1!oQvir_-meN-FT?FdWZ8t?pM`<9J-wKmQAY;6|y_-c)l(QEjCXHi?YnsB zMc3vG`(n#oPJXLzmT%ZRWtP$A4%XZkj#@v?#8<{o58qY)QD#YD*rz4FGiIH7e(JBS zwfnNrZEM%)rC(Dp&HQt5l|o;#;8_Kw%tzKX>Kz`usg0Aa{7(%mE1Z(*A+qtq!OG4_ zueBTv_CI%a)t&D3a6OTMO@*-&VZDpGUYlYOB zmx=61{2s6QChBg*lH)gbMeZqDm-E>m?DxXGDlC!zi*umZ+vKPHQ#X$Q6|5&Lw`Nri}fT|3k~?{{r0Cgin}rdOu^_mq{DF z8*Ngv5*HWn`YWwoqF~;*?3deto7+}1#!r@Y5f7OubC+AUd}HTsowGkzy`0&`J7@R8 z>pJt){q^%YO{+N+5}rO^VKRX)$no5xd(1A2?p~6!d&(bsLj2s~Ux~Wi&mO#sv$eXm z#MCCu(>NqbcK0Fo7waDKynXgcCg9|qZ5+HSKFGP3Kbtqz`094u>xY?pPM>_$(H^w* z(29j+9j6W&s#u1qaIetGk?#5?Bb)3qxjk#z`W&fSQx9g@9(*0^Rd!L>q5Vb3^)pN7 zK5)Hde|oK4->u1K+^?ExB&ojt`F4x;j4kunk2fpxt#H!!y8WeddG-Cx+nrZOxXKsl%C6b>%)#%`ij)}N$ok*a$+5?qcYpc5DY{_{q@p`Y!6FK$b!qsvaOBuHsNVI;|V3~a}ZCBt`uJEr5>i!75y1(Il z(V?C{W*gU=#+P$AnsOFhW%V|C^RtLYFM^e6;i2^pnJ)!5+=p7+U|nGB;ez0Xu04#w zX$M&rNN1U}TAxqhxv((K>hqn6-KEp?ZcVWNV#Ks=$6baJ78b_EnkNI?j%#R0{5<|| ztLR%LvkBZ*w``Spn|`%Fd-YoA?VQN7-_9AaYkSS+D{fVMP>tl`wkM6xz!?pNBZ5+SptxF1X*B#~9pnEIEwVQGBrK%MN1LLQz4J%)_ z=&oOc;`HTOzD``?GLJzbPLU%w$qZ`0-1{=8RG*@RA&OU4R6eiXZ+ z_$%Ya!W9)7XH^1s?Nr{7_1`(JI$HE<`HYob)&XYhOYMT)Oyf`8;k=sm#9{ixZ68>U zwLd!|mp$d{|Lt7+cR#rHb4KgT-CD}8e|@@j^UYL)rd@`no6V%|o|?C(cjts%Z|(Lm z-|xD~)$#n#O2&ShMT@VtKGQMOoQ9ApX&VvQI%>|`=8CULYRl4l5SvPD0TTyS3`P!HgR?mjr zS69sZ6(^c&IzKf=s`~prQ?Jg9(yi+r=<$bXCl!mHK5?^c@k4W-hXJL%R~HtU7t|V_ z(fc;(9An4E0$!o)Pv;|Tq?`_$EpQBpYUR49`#*u@_ojU-o?Y3s^YyXjz8w>0p8i`T zw}=1IxqFi>~s&lQ!d>bX}M`hK*Gh>&)d5F#SMpA(F^7e z>=rKgE_mg~`_HwBd|ho!|G3)RwinlzUvJp|lu5Naq~ESQ`RMGUHfij+%H?^hKH45C zj|_k3f8o&Ygo|5GT)nj8;`)EPoDTL1?EYr=?8vGM=YKkTsnw;gE3+t{S~K;qEY}^; zZ!^E|Et6W(cyk@wht6vDUnV;gw~M|Ln;$*(d`qP7^9#%N-75Zi^o!;_y=~joh2@`| zti8kZ#s0_J!<*TYZ$1$hwEoh&kKbaaR}`;j_|vkATETAlKl5UyTokXEX#e7h$&ysw z$Kh_L%B?=Ssy>T9db+RG!26c9gWvAMO4aWCQBH};f~oz1@jGFQD zl+?2RfL+dq_1>v1RsVm1k$2ILReBdyEEt^kW-#A!`uO(BowR*5I#mu_EHi~Zlsqw= zlVFIEUDs{aPEgMTKVpw}069am{W+8^*>vXVzJpWJqmkkiK3NEF-zQ?P1l) z0|BvD`&}MaOfS1L$w*Z|TibMUF&mGXj+Nknr7bM+noa4+k~W_*|4Iin3Oo9JU3z%; zqa}vY8cg@t6teA9^Cq0!IOB$d&9pO_iDJ=*Gbg&Zt(;>TdqT8)@=D%t*AqW8wWr@} zy7qzh%ZKo)FR7xf?`*>k&+G{Mw&>KQ zDcK!T$;>Wl4H7O$nP?w%YtgQq*?Arw-;y~MJ};ZOxlfC2NrTDsuN+=CSvAZ~G|K$F z^e!Uj^j7i17hie&mEWV+c6`sKeNQ~3whJsP{c7nkc?PfB^cx;wM&-|u$uMk$M(B9h(f?7js~JG5ztJqu1b$Q>#pYP&*9j>SGZgP6f7iK77JZ-MIZ|Gw4`7x)LH#n_giqp7Z z!?CLL%ba?{_3zGad%7su>W9sP(pKbz%RQha{Ipbv@vTmDt!L*{DEe1t*TiYwY zsFcQim8x37yVF_wSi4Bs&ULE1z3Gx0q^{4rsB&bR^zkVh6^hrzu4!7j;5_H#3x8+7 zyHK|3q#N_L=|}QfolPRWC*N~ADRSP$^x@sRM?HCJJDH8{zSwnDY+J8(m7ibKmh~6? z|LJ>`Z|xG+afn$mt@PLYMH%+rJx;MJiTg@wRb{z5r+lz-{Ta~Q?$>{Odn677?-!NrjCtIY0g&CX$)Tgxq$wOu)P zL2QoG@=&KwhK+~6&sgy=@Co-8t>1SWc1u0;o|Ns=$97)(2czk=hm+^A&AFPeUv$e? zHMXU$kv>(%&9$|g1*4s3ors#VN`GET>0}vQp;cZ}UkG~WUkTwhn(w}_Z}px8k+SfO zjd^buDzd)YVW_ly$&4W9g0EJJ3adq5y&ZohwTU4=;x2zY z@3HN(orQ!S^ch{|3}cxRSo7u9LIwpf$7Hp6vBHNlmd(EWY}U>6tyho!UVX*G@%8;8 z<;5Z~XTI4NDY7u9D;k^4c(lv5Xxhg%sa287J~tW|8vb-;`(Rz#YRmUv$zk0SeyoNn zfqS23u~lVcPR}&gF!Gwb^xJbwK8rm4a7*U!=TEu}c4giP>tZ{|6D09BR6%O(Br(2o z(w@&5c08)6m74pyNzM5Gp?z+hyG@oVt$F2kearq?frpu^mj0|h6~XyG?1uT_qqB4@ z7crb#T6L+k=VFax^7HG?(^P(X?7y_0?V$1Vo-JF=4{;uxcJfwN{z@-1FJgZx zL_1%2ct>%K5}RjqfsYzfc**HKNxEy>%yb1)CZ4%^&CTl17IpFb@D(*Caci<3ho`pw z53If}AIFk(d)|ukN8(G4@$9DG(?jbvzMIW_E-brwyUdR%OjnwGa=G%;{xj@+;BocIvR$haXBC`# zV?E_wg_`7yhCfyEEw$Q?+ipz#^UZGGz3=MR=3Y_FUc&tDmC}_?t67$huku~^_}cdN zl4Wa8?Qmn*qWiP^Yn`HavFo||mtFb{f8R+YoZ2DyDvzx}fRXJ_)JBz`a>{ST4H@zk z9_TGwUvTFAQi1qKZ7r@hSHG5d#K*3De%-mUt835iJFvk~wWv|-g2=x}%k<4{z1^a} zr~mwY#4D_SUaX^xc6;;0sr3i%9+Rw$Po|w}Yn~~V{xckIMf!PmsR@GdWe)Vw5y=A(ScFhPnzvi9!^#hz9U+!_t zx^BYc8~L(l>zSnA=ZtOcUiUsz&}|>QOZs(!=nJ8>X~)fXH&^Y5`+Iwh^G4pZ!*W+` zt%BS(FW&f7=u)rokB4)zZPI^yKiTQL(`f&^KRH_y6Zy&$)P0++pU?PuenAuS@2y@( z!@m5q4%>Z**KS$eN|}YL@2$9yY#gS)VOCDYe*3NI|CdVWY4L7Xw+TPWBf+bt;gt4n z)!)-PTn1ta-}#tUx3R8aSh;TPg{IXPN*`OW9(-cOICo=ga%{c0$Lv$H&OCkCXze7d zRnor3N;~J!%*VO@ob66~R^QCIl-s{+&P>sXim)^j*drTsxlu8&>ZT+f{> zJ7#Zo(vV82&f+6twVlSf-bZipAzG)CpRF>Xp{edhe#Z28t&m2Q^B^| zPf1@p_aau_)!El%H%*vvF{@wD>u_Sq}{ z%)V?{_(X*Hnn`k?vbb8yy!8tw8-F}rbMsJ_Zq1*7%vt6Cxfabb_U3Az^L`B{lVPK! z*E+{DbJk7}c<6BPb=Q%J<%hq`I<52FHAMThHQTl`U%9_XbxxSGsAZ<{o7EHIOQ)rI zx7Ik<1Sn*D&^f>&wUb?A_kx^^b$UAs%fr$pU!TzS!6bgmPtNPRMCa;hi(Ht*K5ON& zduuIMGe20wutaU8DWqCC0V zmv6py_i65_z0>AMn4~-J>p1cA+dKue;I?vz95I zF_*P5Z0`-NB5l>QXB(E3MbFP()_W%0K&0I>i}!F4yLy$V_Tgyl#>RQdlKM*)75@l1 z9QAaX{ideJocue?dv50MR9hRMw12M%`_l~N-dQ%TjGqD){;_tWP4648{x#D+ z2xgU@5p$aCFQ#RCxV`giWZ&kH-&QYT@0%uVU97x`->z10(oW6>QTuyai&}5hFi3NV zn!XIriGMA(;QpSI6_*l{Bji~QtlgCA@-Hsx0fUu3>vfeY!PBceJ@)eIpVr^F^%VPF zp0_6Nm+M|Q`}?K$inj-}dAgt7vE*}CXXBXrDXW8lW$ITsA-}~e2df7|02nh`K8K=uu`TaGTUc*onbO~{OskwZjL#9rwY8I zTg;z^n+7bdHkFzD__fCZjVU3HkK1w-(!6#rxI1O4>6V2}qH`NJ2X_}-(s-3=ahYX* z5X;>p{*!B4ebTRqEIGi!CeFZGtb0s!&Bkv#?Ht>Kb*pb#taH4Oc<0tc9^QQtKX-bn zu1otZw!~ZU`13P&w>xn>JO9wi?4o<~;c1U=+}hXR{?+>Go?Tw5J053VJYg$i`Z@6N zw?pT{HBWNvdbBg{$eWrwx;OToFWq09cXjiZeLIaz_ubc=EyDZb#7eE(VyjNfFPeFw z&n$eS>B|P+duv{&3vcQ?Uw-209TUBs8?yG)c4ls`&MJLvdWH9`+jHsLyV>7vjWvI% zc%?nZcFnKNqFF+AJO4JH-EisT<}M+oEq^TyU+S=R1Y2zuHJiM5hM@Y}E$`dZzNRkn zvC0Th$TV=N|C!Iz>LPsYK>i1jIjhW{T;Duh>Escu0;RNq}z?&a}nS7wSB3-FXpM ze7M$lQS92NtzC?wLM5h-e2u!3TMMtPa$JznWcu9V%F1Vtd$hPS5B@ zi~VyZH|+BhYB+13`$U&~9kGh~-e`Z?jNmNwrRS8+uro>@yB zXCJ(@r1RpIOT4MZCTDHtCMvv4dB5+XhMd`Xo*u<6pNQq0^N#dPO-vN+Y^eF7Ht&7k z+*AF$Tw$%VpBGoZT9$Ojlr#6uj4PMK%F{OLS=|a>9QO6)rPH%ssLf0|BISSGwsl3| zZV^qp#BN`k__fAMFHh{*lsqA{MPRqdvg4mL;(ml|+dsu(Dr?X&;}iCui(edZ<@|j{ zsr{$p(R))RX9}Cd-Ze7}zbPD6#UeaeIau+_-_>d+nrUT0n@)YUS)KNQ;hWu;IfZc! zCOLPu%1=Cd^@-5=6APwr)ITY{v3=A0t5;aoy?e2-$$8g;>&;h!`8nIySz9qhEogcW z`_g3bQm@Ht?ws1j;>~q;y>HT|vol|Bf1J10wyk61fp_v7^$Ux&m@fX@!m%^kG-t^l5fgjCH{sRt+{dhbmMe&>OO|K^ckg?@F)~a0(E9T` zKR(r#Inx~dY-e|4>igB(@~p1c@9&SQPIH-kM*rVJwOf2Pr!VijUQznD_h`Px4_wVyePxzxU?|{4-D|^7Hz@<$`u3l67_3qJMZx`Kb(JfPd zq^laRXgypXdi4D>ZJig^H}5}qC;mzLR_oj=YC&P{(H3|2Z2N5U)5usS(R2Gs(Z(Fz zpGun?Ts)!|{axiV)g&a9ah|B&QRCI0XNNu432KUa5j1t3<3_*wg7m4@7gTQfu5@6Z zb@ca>KY5?Fd;Xpm9rvq9W1fq(cuwu3;zPG}erfVA@vc&u-(2Mqrg|iQ?aWz=CErS0 zhh^U~Fg~>U*6gLsbE2)|cTDe94R2Pmx%1`dAHGi$8#nFgl3jB6)P?CXwO8AVYjs3; zqnn^^v8~2@U+H;}m zMv7hOl5eZui*G&u;)#y#rw7&Ff1Kf!TP`2R(sXyqlG}fS3f^ln6#ot|^(d^ebPnOl z5XeuGd*$+2=%=~C_3--@Y0r1xm7TR{!yXUDt7=*cw!K~JcW3U*xN4?J?Q=ULb#Y#yQ}Hin`CV})843lwzOI@F|0{9a zHop*>_(Uye_n$x3GIwsjZ)A!&qyJ^k>0;3keU}7fi z%zdmf?{+#%zIu9Lxrgw1A^)h!^Q*%-{s*uh^Sw}3;Gv;&aU$E#BP-{oZ0NSX5W>or z=5RvwyF*ahS(EuD$#?ti-YN`V7ZIa>Q{chz$YFR?d)YNf5dzWL%; zcyG$?f+b<;*NdN}^qcquanFBrO7Q2w*^QpO)n%pA=AQTER=H^rS=2d)^LyNdLu`-b z?>+RJ{k}GTW17(Bmq#7fg?ln`T?<>j^!lsiB1Zyril%gbzr5u91K)~u8=@XumfpTB z*<02;P+9HirkkFO9?nNk_unrHx*%dQ+w^YuLpDVbnG?m_eMT!jRYk9t?T&An|7Ye* z{|S$7oN7uqnmy%iz=G9U%TN4a)q8ijQbLbLfp62;@0G>oz&Q< zbXdN0M}#BWy}AGP^W6){34gxj!eoJnQzs`msxOV<3h`L^%U=GG*33Nh$#?d!ue-By zW4*#Gu33sXt2s3`U7hG?ajWvs7uCDlZaSWKdtPkHniQJqx?Qze<3v%37*o5DV72SQ zO%Fsv<3ks2)@MJGF79AD*#-3`cFL?4>_LaG{*42nN#uI(A zuAWkzrOj>Qoz=JC^5b82fu{NWT30e}ww{?S#${FaOJDu}a_6&~6{^A(noTKDPIERD zxBS61_4i8O4`#iy#JtaT-|v{QXVJ6%t8Mx}dH$4K-L|p*x|MXuprztV&McK9Vm+v2PMVS> z|J~u&b4r)|j@rG;`h)jkKHtpf1#Cy6MbB?FeyFU=f2wu*#3PUMmX#UO{8~-h@d{gsi-|{DpQ(DetS!u^Te^LK*=8BA&f3{qWGfgv^bab*vMt)YnN#T7Z zzduZV${X1AX3D?NOpW|l?H4+hF%v|Lr`HK-eB#^OnCZY}cl7R?ivb#X*1v4!LoWnQ z$`Low4{EC7xBbwd$7>j6TCrvA>uN@?P7~w(A8#vIT-`8N((zW!y+xPxUYZ+Dn|Xxs zTSn3ujwhUl=KR02XxlFLF0V5;CZCi1C~bG-fMsu&9bc~8+-B_@*VUQaXFf;92it6@ z{(3l`<=2bTJ2U<=>~g;PfA-_UcXzG6zqj`F)tczoy{x+(ZR(9DZ@&Bbzzj*ro7YdA z(I|Uz>QRUE6ock?^`q0%7tT7EzCKO3J-g84ko~p;pWojvYqcuV%RO$9jMjH zn0Ncq=?6OI?~M43q$0mE`?;&^igs@_H#9M3|MYxkwo&YiZfAD3pecVY6yNFH*E-Sc z{(}^s-_BLZ%?y&AN@w#eUQ0z)_+_!JxO3_SVR{LUFx&^Y`yQrX7B7=c0vcO)pGL-!o@w z@7)EZj;pV`oq5z%f9qt2z0}Os>@2U@6W%HAH{8(Fap2R0+IaOg-p8NQ?pPHC8HsT& z_;KLfj#)1J^XF{c#C)zSL&9}decj~U=3fkpYrk8XFvad!^2~OxY`B?S|B~z1uN~A6 z*}N-U!q|HKt)x7+!i2BCR>?of>FJ8J4|88UZBPr}{G_4$TF@`K8=f94!e-pH2d%eU&KOh@gOtW#Ik81y+bXgbP-H%>gXa?%flvU-U>Wm6B-?ArQ~MXI~{ zGw*AL6+1N3J~{dFR?NIV^||NOsWZi!ALti=YBv;uy~y0W=6pW(T6+Fe|mDX zpytQ9)vwkaP=8%$Hm7y<$2qU$9{oQSwJP$)xwdupKQKMNHuYKO8sB>-*DQX?pLnD^ z*S=`Fvm>je%KN1g3MHDZm?|iRI&Kd)Ibr)m(&Ds^`HN_qqLz0r8U!;PyywjiH@EZl|JNnl>{NW^@D6d;n zz^*+bX>w(kB?zdLz%bH78c=4J!KYbt6%zyXOqOUz(YD+drm9EWhXkvPGCML`w z@@Ugh;UKQJ;X4n1vYh|$L;1c;hJ5Bb4$bLR2Lj46VwXrO94==5K5w~M=`rpV!EgJt zE6sj|m32=m*%9iog4eA%M)Tj=yRqDE-a_+ZvjsXmw{NK_{`<1sEBK)fN1@9}2Kfm( zGo>EA*v@z%fs3nb+tgO2kcE<~UOl<|t0Qs^ljWHz1D$tSe>6(I?Yy&9x$o7M`&Y9< z_n-OxF#pe!6SWE!D<;KLacW?4<&D{z4$6gp*f8yC}pDv@BpL|#M zSGsEF;R%x!W$%{!O5OH>siD)!CBxllRpp zGHO@PhEy_znoq3M)&wrx>HJIpGQ`Q`^-K4bsAUCHd-ce5PRV>-3ndE7du4EJo^ z9r++}9b=c{nim`9wJF{4JeN?O+U!ue-|xbzkiMgTO-mxUXO}xqz1yzkb7;_5Wx3xho}}~dXWo*uHQ>b7tjZrQuNAK@IXcB(WHLj$cKLtaz!%3ZY`V^J z&hp`fyvQ%d?g|BlPbt<|v|gAi@7A{7(rYnGZvJk4ytsg8|BbF|hYuPr+tqzFFDUq$ z*^<>Ocq6X0DxX(pu6Ewf`|riBu6g$yPNq9vF7+^SDs24xWcdW%C@ELw)my#!b$@PO zy=})z>B&o0?TXE;*|Pmc>QLs}3Am7bF>_8@FAz3EaAFtK<|5g*k7!fA23f>gb$!M*2aU@Y%($AN`A+{b#XF zNL7pbu|eyPttDd*E)!T5vVV}wmH4n7bZDeV&4O@4n z>HM?llEIFbznxvbNkyvh%+8SB*xU*It#OR$U#A{+%~B7({5A0U47CKkZN}?Z#WMNN zonnpLpk;C<=>M`A6AwoBC{*5k6QF(Pxk=J0xs)bJ<;U8eRQsY_p2>t%pg()E(8hAPv{s4&wgU5~4pU$?v|y|mOqO)y|Z8CUnB{E3qdw{xC7 zaot#^Ka*>IXwfPEU90AKb}jl^?s+lj_8jIt&%-P|R%H3Vu5o3Ny7(>MYuSRCbE69< zrHOA^u%@p);`jDX#w86!8Cpl(=C897Gx;2I=Wfknh1VY!JQNq2U{t<+$AVQ%g;ure zcj+?qo?K9}b6X;(o=~@W%7Qg+x%DfjEO=%0(`rZX^QF_nj=wmt?Y-rjDbwa%jz1p8 zlEU9IHD{5@`rOuL%V$B22PZneEezA?YHtn|~| z>yLb%xH#bQyE)Y%j5+ImY5sPavZLZ)!qTSAaydG)K3;p^ax|19W-43p#!Cz==Ss`N z)@-tGYD&=0KgPjQBFX)Do#`vFa)vYVv4!8woszEJdnBZdf5mY8LDZG$rWN0$mbUP{d!Dx=@0_l|PhPGDrCZCZ zzNhkjFIc*sZMUeG@Bdq0J{8(Mm&_`B>XLlDe$kHapWN0>eD-8p-;;fFXP5pHiDs3U ztrw_Rfs zekuKW_jZ2QAM$~GR-dnK=lgGb`lO!gt}FeY=59Om^|(Eo@4l|ofQ#B6-i3B0+0B0J zvuDKz(MFN^t&bV3Yc}iI8F5KLB zW_eSe*zR~OxqIQ=;$d(DeDnY*hK`y^w1>#ARNA7GAMBPO3Q zfAjj{7XoJkxK3SS*Zy!hxma%fC;ix+r#Z8HVvg!-ds_!RX~-_sTDrI*r~FKI`_}5P zZojZ~!Ip}@^)GPhdVN^E?UBB4qEnS;{`=F>@)P)sciVE8uLvvI?kH>Vwx3~5&jF*a zOw1vzOE#Dkx4K+*k^eK3rK?Qy*te}!PuT6F|Eycnt->{1Vb`nNf;Rq@Zm-X^Ur*6Z zIV&o#gwNfSA@{*=N*&(P|-mzm!>t~aaTUK-(`&v>iZX787XHy4w76V5p)rde)oT5;2~ z?y&8YrU^A`@BM7wp3W&F@LQ;E%bzRtJs@l4#vAu@s;`swE89sgqhhOz z?=Ajdah!2C%bG@SjcVsz1&zWVZVF1uXU5kZXp1!Rm$+lXE!%SS^P?9B#njG=Z8G59 zvTIiBtRBv%XP-N^mCodwZP@B?J%uH_;C*eT7#js#xt3A{n3WzUV5PPuM=b?5%bKe}i4+;HDmC9srTBAES7cC2oz*xXxOiTl1NZS(cH zSoY&{Y|dPpYZ^sq(d+Ic%cY!Z{8H0jdRB`~rMr&%<|4oCduKS@`Lo2VEScFv@#h?^ zNlKz^+6AFYId-j?QGar=)Rm^Xry7-URuMZFYwLej%y)s$&*etfmWvY>+jA9%Ewo#H-}O_-h9Ir)i-LDgvDg1La!a^65*ChnoPr2guF2&vUu&Lm_7up~H#++2T z@SRJ^?cGz&&%0%N-1Q3#tgg3fs;^WDZ@cx%TUA8HiJz1C&)*C4S|wTB|IOP`c+Zdh z&YHaqme;E|W1nrmWzf9;Axq18YlSn}C-1&iJhR?9ZuU~QhP&_9zUgQ<%2m8xXX5`Y z$Mn;rMQ&=XU47Q{+1k_%S#bqP_qM1u`?j~p%zAxzYFLINugB@Rx92F%F-*H~)N}5F z9}j-0*0zVYy_?jeQpGBv7I*JXQOKNDr# ze&pDh+js3Iy6w=(`EoD*R?D_;pKJ@v)Bazpzp(oEN8elD?#*0&ZRym>=R8+_J-6!I zshQ4x*EYKP&N^wwoAp-g>?NaRSJ&%QZeAG2n!r{1;={EP7mctd-*9x zl=HT`D@=DP+%5jApY%~8>cO4e+)9S!J5ElFc~uoYdBW_)_D2L9btJFmGi`J>k@uP< zIF(gp%DDr!=VnOiZ4Ev4Z4IYmc+75*s8i{d6Spxy`%(6u3LC5wV)rqBE{dm2ri zZ`sCc>FF!&(;TsKg~ZNF|0*)fQ|5h_j_$2nn5pqX@1()nmBK6dpP1Wj*fUX0F?q`s zKkJK3yLg$B`}ZNK5O_ua;^V?_9gn52wwN+>!JnpV|L!W5FV~ecM9r?AUU|-P9mN z^q|)23uSjV2k(`AMe;+qRv^0%guDQGVpP`9VWnWCB!QGUv-|=ll8ne zXGro^1^yMrFZo`typ!$usiGpaLxz*)GN?T?R&X}1 ziFsHR^i6!7&--feZ6_sPf9-yIYGc^d-ro8Qu4ZG0=N{Eltb`WZVGtml0hW8t=B>TCC7y-yns z#i{(Xc=>sf+S<1d{~RhVSQ5M|t4%Guj`=tjuTAHZSkcHu!W)>+uGuAJbVSc+<^z#k z4sWBR{d0H!&fnh^87IZ$5oe_9)-{qgt`aN%y>cGxqfhMy-*U^IYql zUY$_X4(4ZbOTUP2IXP25<4sn_YR^?q117$b-gc;K+k(_-8?qn1%dqB-2=z9;R@u?k zZ6O_ai;k%oA@wP5s zC;o_(Ug;CRKM8nOo?rbhe*L518-2TuMz;vP4Se|__-szPO@Zjnj)&dJZZD3!pIP)O z_(@UTyqhI+B3e=pX`6jb71cd7!PqENT0u@q&Cb5QLeL|r_+CeHbLhJ*Tm5$%Es&C{ zj50Rtd9iF~8RL`q&L7vjH5~jGo%|;vtZQ+#k@^= z>d2F*y6jz*90sE6!Z+(ig?_q}6`&hB>&S}cn=`mezaPyk$Xj%w_zr8yiFDVl4+kzv zm=<2Vvw;8Kf=j=}nRORT-#78_7G|%8uqV5f(QCHl|LVo8b1p_|rTtR# zy;PN%Ix%=wp>toj?5nQLGk(}Sd9kW6>ih!3FN?1gEZyP!U6#{nk>#AE<~57XICkpw ze|2+<^xV)EWuCK$mHDK~^B2*#cRkCPD9~O}SZ2kh)aZP~ueJ5qrc*jz{j+AMOnxL% zXL}(~yLVaG;#C3DrOADG2w(-y;*7P); z8?UVG*FTW`bDe9u{0UL!wjUN1*AmjBWix+Q>WV6F$!b<=-{`KPc=Ft&7d}%CM4y*R zkc?k{=>OyQp_j@78Lnx$KI!l84ZT!h}_A~ahRmXPl&yh@wYd&1Gt(z;>bWZ&9IgZ!a zZxm0G>aBdoqLEw5u{K7K;fKa{-NRPis zO8fOQ&c2-!b@WEOL;sw^=CA7%7hcWTA}+BiGQ#m}?!li-<#JbVm~E7BPMC0+Nj}Xo zoHb$HE|=FUE^N8!KXF;wzL4$Vzh}#x-8-Fq-x}5ntsZ|o>{_zsCA+<3e)+(Qi`V-L zzYCxLt5p-tudmHto66r?d*tT2McHd6&79$QuEbeoj!k+K=YP)?j|soPL? zEyCiHsp7Tk`64FvucEd#nOaXvO2}o`w{FsZ!Yf|Y>s<1o^UKSW4qlU#()B7|oRjx4 zynMLUqB!mT3)5VOT{UXX^DiGX{yg1p55rCAyFI6(w#S)jKNM$F6svprBtp1Z$MVbV zP~SHnEzK;yu6?T>zlTl9S$0N~w4q9Bc93y^hvl|>hMg&?b^H6b&eZHKvzA`qc{6r? z$blzEH>}$p<=gVm=GOu1Dxv=|v#YX~tk2uNZ2RXGy)y;Anb=)mVvC)T*T7r zQgwMVkLmb4Fna7QwpFsh&u!1t{!cQO3|I~{%1E7HND#SQ@pyTV^;DF4SKj<&R##}TN#@jxqB^%iCp^B`*SL@&@9dS*P_ucr zb4}RSZ(Y-@7vnrtPio4oj@Z&ox2Bm+F%#pnK6gTI`~C&dPo7WQlRZ0r!L+W0wZRuc zy=NGHT-bVxE4F&J>7A+lhvj{Es3m^cqG<}J(?5kNxAtFZKbgEsC*|9I zX)pQM-d$(EE?m&Uwa4pjYw?Gh#n<<&WA(A)*}LLs=&l2KoBnTTUwT$SGK_ck&*wHQ z#_b)Vvo37=SJ3x4bZ?`Xs`I2t$6woT-g)YjdZ$R#y3EyV+Ts@TqntZGPhPd}*XQM4 zml-Ub_dPhpIq7#{tIsmlSD&9}WTqFX9u#|GF=O`pXG_|SCWJSHbWC2eQHp6E^9fVK zzpH|F7Kwd{IBump{h^iRVzYaZ-PLcEZ>E2rb2aAlzw&^KOG6*ekYxS2xN>Ui-?Ge$ zngNE_t^Cr?H9az35&5;h$Jvf4_NVBNEMCb~%bI!}Cr5Jms|uR%$DaS=vsYl#p2a2? zR6?hpm0WRk$P(%AF6COo-McTtYg#5TA}mbST?qnM3%kZqc;E5i4-2s%Oc@yL5t(2PW`o` zB{AhfW&5NJhXY=JeG;(h%D$q1XLMNE+sj>_i>=!la9OAR)~PGY{39aQroOtqLGIW3 zPqQZIi%m7n+jjZjj82YkSElu5tY4+wE8f1tcZX|p-li2Vmso!-x^G!~@uJiJnd0}C zJvT@R`y6;uMO$W*%;q?$mF=qT*Ngcbe5GF;HApnk+kPQZ{IJN{MQ;AJTr*kq1r-)u zTvF7QR_zgX>t~Lw%TfQQ%^E-R|L=L4>3Mw3n&)N0&|Dz}GFaHP^`M1e#Zsa@t zqYf|6|2S!NC_Hq=Z~gspzb2@Zf7`REIREK($z_&5YE!n%WPckCN zutm+gbMw}+_ElNiW2N^Thss1ev zx0i(fo9VK=cCxlbNzVF+rJ+B*P2Hl?&ny@APIjyH$KX%BY;W`vU2n1bTk%(2Sl)m2 z>@T;#JHHv?b~|NXwbpK!`AbUM=7H|cF5M@4?RxC2Z?%RUE-UNo(Yv%GTlLK^=j9q} zCL604zx(?|=>kK*3adA&?|nK8rT7FpFW&lZ+8n_u`CsC+eT(#$J1*%WX)oTbJQvV> z%wyLj$@ZQ6TVfjTzAzMTc%Aiq-aFP=p(~PRg?!&CY33}M@WifRn*6UnO{>MzW^jLC zxiC?(T>HY@`}(I|1m&`f_n=78eWbr!perw1OI z`TB6aqV)0RJOlar|0io@J`zq}_4{5eo5+&@J0CGO>7BdJam6~ZUTAmDm}Z{w_}7!3 z1`C}tAIvQeBD=ye>TWPJalNbVfWg3o+Djq?GcCV$DerFXs=6qSAL*V zEy`f$ttX!)!^|IsuqRDk?h((kel_2D7PcsL+2xhyJ5=u|6{jxS`Mb_+^~*iC3YDj} za_tb=yVUqp1|R&G zRcHBn$>P$qgBm$&{m!b-edzHdZL1~s{8iVMimhQk_f^Jq^8v{%3VPXUZuKIQBo<0O z>{!c{#?SDe+bkvcQjDqC^nc>)Pn~-MEX<3a7uYvce|c;?@2mr#!_HaF>zWK4y#7z$ z^k^1iz;+JSciST~6~CTYa9698H8kaL$hJVgShaNVxhA3(-_|wAUl7Z z?~{O)hUfU&ZpBR1eRi@ZW0}>xU(9f|wjm4V@g$x|=de6-PcEKzEDqey>}!L1D| z+78V0^_sktGiy~^G_U%Lt!rN`ezjv()znq{R~>ZhN-oph_ie?dnx96pW+%VBYxmnY zZKX_1(TA48z^>n>p)!AE`<^(@eihx-O|h$ODm6BqcM7`hmiT+iqoY&53ip;?7nj-c zaMP>n6V!jHS&DO>DXF{f`uf?<`Tu(490RU9wh09KL^uv0hkw@wZard^z=%iVMF5Ew((tUB0y8@db-D)24KVF5hGu+Wz6ez4VaSk~?7~ ztnxk0)($5V&dfacc+x+&%1!UK-8K50V&$*JVs-G>^y_N#b(Wh-DBb;5dOhVBqrX|Q z&4CGithtSfGIpPHFEN#}`MpYf_pOVG{YrOX%7dNoez1w_}fd zYb(l@>1Uqc51JS!E2v#5erDPam-%laXYVkvkZk?^I_KKTZQg3f<}UZ;UhVlTnQh6& z=^u`+oR;(X+JUC_&O@P+K96GUHuzlLeBsMfpYwG^<#xMcuQsV%Ui5RrqqQ&Z*ezSX ze}~kcg}Q5&PhEdbN62|%Zmh*@$Jj}ec5QjN@7_%1k0&yO6P7-_n>ORCR%^!U3-cx! zJ0(i%28-EiulLM(|CQ~5Po?-JFT)Qv-FPplf4QF3%W8S%#q4)Kw_at))7R6j|G6vL zW5uF>`|DPjgny5$`#3$zPO>U@`8owB5w$HJ&uX-L^VChfoG|~_A)Wne_@jR6 zkI1_2pXbheJSkpRSM*GeVauD;e&OgXr%$%nm#+TzF>c$t#dfXh)}-Ve4Q84?|Mc&O z*t74=5~n)Wt-mJLz2)2YVhP6!U+lD%#Lo!TvAeA~ArwOivtMZ{*td$Lys-MYP|~n7QJ17VAzl+b=k}IpDl#Q}oYo2X=HXzInSa zHq0$up?&9#t&XpZpV!S-JzpHUcZKQ_Mq7{a#CImztn+*RE%Exqv*o9#LtzWgg@?;I zSnBV#>P5ePUpeEkuxE2=U449Jc-^G5W3xM+#MwIEPikJ`dt~1HXqV?YUF#Plv|oE% zqcM}SH}-+RmV^FzvRxPTS3Bp=5ZV1)($z9Zxbbf4)W(e=3tuLG;OX1EytTSO$jY;9 ziivgRy`%aL=MP2Pty+0H{#C`PZ8ptL$2_?sm^VIP?b^t=VvAYJY1LPCbIYFc%v|tP zII_;^@{u_a>!+PxbLOL@{QR#ePQr60WNq-P3(C_g*syAE>&N-)@{6tW+kCU#f86-e z+;^3O2|8CPk$(64sKJchte}AfX*jKB?uKjhq+*AK` z$NW2Q&wjR^Q)PdH>2`N#M|Yc#JR8~?JlWFb$0xKsFYaznnRI{G$G#7|h5IEK{}dRWw$mJ^SIE%HMfBApG#FrWb67uM6!d_@Db_ zz1{SOM%OkxkXoI`Y`&a@anFSRXLhb-tG-)md5Bngcp8a^eeQMnWK~Pera3~x)pKJcqgmq1ID>s`2lIWg{HhHm|pOZsPz2Gj=^H zJ8bsjRGz2*#jjm;JDYfa+WuLi`|sfSOEtS|CSAJ9ai??{_u>OuLK-s-3MHJr>t8F? zTU%)KQ)kOOoxBM_ANuw@o_>ChOVsQIJh~tDc+Rkj-zs8q{?_~*l_{@(sm)n)^4Hy| znrm&#S0Vv5*wmHDCcmy(L!Ev5y>T~1`bxi>wEC$l+aTK%*X z(fIH*6+Tz)liMzZNO>-ZDE#(k+4cETn|kyP{^{QNUH-q4z#E-w$}L-a$|r1beR?3k zRESTNS=Ijy?>EY~9W2dbinrZaC z_cT{j+IwNyRi_!hWp1syV;KH%^6Ap>g7C@JD*-Zy;lg`XPn}qCwM>?~JlpEh>`H~g zu%7tXw`VpcyL;bjQc>AG^>`m^WycPTP^Jny>rLk2j#McJ1xEU zgiUtk#hJU+MAZUs=!dV!jAYf)c3XCcPwK(EzsHv}`|C)3shZ=MQ_#D5>MFUba*B2; zwmy|o6K>pGvh~4hmyfNlSyFNn1>XkWin#DXrfB*6f2U7|TQ>i6j_lPxa%9qnq*tE` z?ZS;Yvo|DYaENN}46gLuU98pcVcF^#Iq6^jr8+F>O?=lbb!vs}>Ay8qF?W<_o#{Qf zfmbxmXbpo)2nR#3>#WW#Pjrd{oaSe9$=uixJAP<>782Q( zo)#H9kIi^l{HcucbvwTaTW&3WAaU1TasTFJYyLH_Tdg}?D@J>LciX4wCet>E1mwzu z?M<4e8q;IBRDDPNL}N|e=$ng{&3&-x>qC)4j$Jb?e|MizmyO%ZdT-s!kaam1#E)h; zH?Qt%Z(CYxv@yGSQk|#dTP^ zd1u$0&0X+DwX^$RqVKy5OD_+d3BvQO9L>_Mtml}_{Q7E^_=*#L|AJO8INh@^C3?%o zx_fEg(j(gImT(T2B+NacVIlqj}pZTA(@8B-E zVQc6sx60_Cp|S_>su$viMKX+hre1Myof_`>K;hrBC(id>rS&tiwr(qwUH)D3?hMy^ zN2fl^sw`v(H4A;c{>{(4rn0?OLN-g6o!@lhR`#xSUtRTnzKj2vKH=1!GF9nBo(uQZ zox7$cJLQ(*s;RbDuN%m&&2Nj}+L{ydGWFW2ol19tJ~h-EGyZ$1&*b~ucG;|+cjunQ zB(Bplf23uhkkFds`-DfB&AT-C{k_k3W@IZ{eOmJC|L(VI9n~3SWUY5PYo=bb%M{UW zpXzXd;elhek%#1>)}32Ee@?O};=eAk=&#YdnNv@_+q(MQTG^$yLU!kg?hBop_2}gt zvt`%#vSNR0)m#gH*d#vrQf;)L#?96Kzn`rUiFtZ9Epqj4`KO|H)MYM&nQ-tr<`=m) z#PWVlNjtspO#yc;>zoxv-ACsdbKg3#^^K}%@!E*Fja$QaJMMD0Y?Wd3W0mfNMU}T- zOpf1@8yokc(RUik|CmzT39Tx<<2i7c`CbopP; ziIYs0HSeZJa6gW`u;#LCR#m$Ev!_W%wHU2#U%umS`ZiNP{`m8(%!7X`ezy7tuX=i? zr6vB(MzPPClc&$*E;Dog$irMSyQuiSidXG{%Gdw2JlA;XTy-~o`F)l3)zDRv_dhv& z%GhjsDwH>FPy0VfZLi4vCN57hp7(r{OJ`ZOt#+A>SYFKgThd!S6zoIZ=3Pp7?tP?+ z_tl@BTfU#Wv3YvYKAX*AYsypPq|Yv`jIf(A_fG1PGa9y;%OiHK`DL0>V4|s*9If2b z*qyP?T=bP*=&5(tXMOqq>Z#qe5Azhwa};>{I@iG3~cB9+rYM)LP@BhiEyVai6ZNc1~ z){E5QQ__y8Z@Tz$_Sv&{6qf2v2NWmsl8{IGuUm(78FOW(Q0Va1vr zaGd=rReJqlJezATq^^o3 zb`(!gbXPY1>!j!I&Up67vkmk9g=9%<{1U(O_0_Chk`H)|gPID~t>k@?8^|S|;eI{F zc4j9cL2h>Y*Ef80%ug7Q~ZSG+RfY8E;%%R zEt{ZOY~7yh%aa~pH7_UsiI$Z(`;9Fe%=M=fx=&4PWXhC$!lL=xll7o%08jjd_16}- z?vqWlVtuwCU-0}sQ$G2C33J4ka6Z3KsBkdSXYI@DrIyZmS#0l*y)g7#xi)U6_vMVZ zxX-FdqS~{+#+;E_SJxR^<6^yU|I$nj$NgI)-dN4&xHTjF+bkmi=^v|8`oE^+|2Q(K zF+R(F)>SbxM)%EAnuC|}#9d+kqBOPlqH1Cmc;oReeSFOD*ipR*Q7fq z{rIU@(v9}vw{?Dh$hsM}rDMxEFWybvdw%ijGkyGD81N~Vaz19=yXUNUA@_&B zz?rrcGgr2$Ce7tt*(z1Ml6NAL;uQ7^CDRs9_`mk^yiyShugvH(Cr(b-neQR6i`oBA zsP(asrimAtSXN0JcU|=Rzp8t$#1)^gmghXBpCYxI-#p-b$$98nde7>_RR^y!e2><6 zTrz8AtOm33TG_8^Dy2uopX)1b5OmekKfC3~?>^_*USWnoLROV=C5?RRUZhACds$wR z7s=Xpw!bd-kIRsQx&D8wcp&OFjl2;ET z>_2?q?4106K1OUWZe6<8YQ1^>niDT>7Jp5>Avu@Tc1f(vZ_m~HCN{)4iWI8`&b>EV zx#DicuUghy>DLeJHM0L~D17!>@GZuxR_A6Op8n5^q2TH=nG7+{KV`p}PIDcMKABqo zedXIF#$k0Y&3qRfKELAl0iy>QrDl^^`@I=o#j~Xe1^!?Qy(iCo^i9~aWM9VE-51tx zYn$|E=3|FT%E}Hb|E|B4taRpLTNKLI#>DzkSZd>ziHCH5HwR@Xa=yrqDO?a6RQmTv zp_PXCF0&sZ?Gl@8R+>2F+p#v;xL12re&l?i{o+ylRgvN^MK2F+)Vw|Os?IvDpEY7@ z@60k{R22M~=r0_~{3#*1Z&8%NA*b8w0*dZUx(oDqo_%`GdM#07#$uVd+zpcw63&U< z5j)?{C3s`&YKzx>Y{l*_4{MZIhiOB&Wp9)eW|ka$~DC;N_+nt(-Burk8u_5Is3XX{N02Zp;sG%mRdi17$cbF zAGc~kHm{!D7h{%VeQ(;+Rk>@z8CeHqE)tDaWkEc&)6#?Tk-3{ z<|+4hwr+cS>dDiIQJVwWjhZbyo}ZZW@x+3(qO}I)6Z8g_g2t8}DyDhkR9arM&jPLIyg1h;Y zqgc0{)q7_2IjE>jGfigdq1oGS1+eto`Jd?YDDazqXe6)Owt0J)C!6HY@-J#rd-alM z%^UuoeO8{EEa%IVCWf(ic7#rSneMwlD)Z@E>5`yq=@}FE{k$*ekUnptVpZMUd6DVI zn>knBnqqTl%PqMpyKV*DR;x*BUbfhiC+!6n>r)vyKIW*L=Bo!KvOJvCt0JZtZ&UKJ z&#vL0wfn8&d`Il zmAs3K5DAQa@$##{WK*ZZC!K!WeJfaT)NJDH3f((fuCwtg+<3B2?p6|afZonorY9Ef zHMwlOeSyXGNj&VKvzFcbp>dAgZuR5O(q7*AN$n>V|BsLTmFOFBDChCc1V`Dp`Fyh{ zT3MbtWar(!ZOPLAE~muZ1Sg#jxvH_eXA;wwi8hge-urJHUnv?BF#X$vPpdD)N0=#H zkey(kX&|^rUa8Cd@{#+sQRk*q{GD&Vm*a>Wo68Ml{n-~KSc?@G*_AHg@>PiO{~h%I z&z{?dxMbd@XyvSvc+SZh6xx1|>&Wi68!jlc*jX5Ue_rl;%0Mz-)8X?5;rOXh@^jV0 z8V=~3b6lr={Nok%=YQU`PLkpBU4KlaJDAu1oe1lEoAohSnGU(T*Umq6>O#)^`O32v z9Q<=;uk7lseXGvYZiwW&Q29>T=fGuGPAR3*1^chM|4#jI*=|PiLOVju3eZU5!jXL*rRJGpDqa{osr&-kQr>TwE0P zNou}ac1HN*7mB@|yAIBov0(Kdv2*P1Tk12pZFY9}rZt-{y;i8HaQ2#g;-rmtR@S;M zoptAqe8}?~TJvx9hWhSyQ$6*z!pP~DgtJzjamlsEH=3^Q+A7Ky%rS3eQs{HfJ8eHb z8a$ix7#s|*SMphA8^2`U`Qwa4mzl=iB?mJC*dDpb7@d|ZRXWOY^E0z7ukH4!U4NI} zS4cJ4`TOo2RUsvAubJXHA2?^->X(07)ywTBwQPfvjo0rv(>!Niso<64JowXI;L5ji zT32(Vw|csn7zQr2p24d$v7%Y~V`_hp-GWH&8&4#ypO-&34t->q+4eeYf5`j3D)oy~ zzHaIN=X`;OY2Sm>KkvO(Uy&DcadcW9G^tx#WXIipE56W&FVvU)OXww zUL#}p?eNUz6>KM^H{U6J|Hve%sVafnSN=r;=e862FLR%UuUUBUC;O)*7Be#=-cNLz zm*p-emOkf!U+!0~Lnpe@stcyJocelCs>Iz#Tt#=aM5Hn2WzplG{8k)SOPAkc{i@q_ z-3A474PT?3&ThIV^2PlpYozKZPn5{kd6C>C78bJWvZdxEwHUkdiA|2ek9+cVgydf- z43by7AYIcIE9ExFP`k!7`q|ubhxcxF-M?0=aP6N350tqgk4A+`e_6-AbVO+MQEZ4LjN zM~+wjPTN+qarygpl_fKuES2B5#ptlhtv0zoXHKmV|GMi}^*hV!=@FL?Z)FRgS$S#t ztE-oqrm3+mxREud_PfpUmA3a3rAx|7j~}-RoB3yI_`D03+NDzSyt!)q!n)aAq|Tiw z{Z+eW!Q5|oO!oq7mU&-ZDSvO?UX>~QQ+|apO+Kh5aBStlF2~;A42J~1PhYmt=}HR! zJ+6rpWzROf5aP>G`oaIq+TPLNyFzr(fyuch3yy0!Xs7S;xSN0Mp+%|JPR46DVovVf z(Hi)WpRLVP&u+&)k+KC6UK6@bN#ttPX4p&)FA)w3Vo1`-7hiquq)nfUf4ITRHw|46 zi-kTY7xTA$tI*e-Su}aKb;^u|7mO{nE*$LqAoCzm=eDcRtF$II>A67%W!7Z9p0fK> zbaGqZ8ID@3a}(1o=G1om3tV}6^5@!D1tlS7iK|RxP8zf2I6GdI`TX+45ve);9zU5) z_)chvD1W@P?U0&f-n+R|a~L+&3Yup;$Ps=y$=UkWTlp<%@9M93`mi5 z_Z4TaIb_`ClX2R3f%6)X_@(m=U9NkHt$ove^lOk-?&TfFOyUAwW!`)Ksb=WLap&&uBxvPHItqu|%Bt2?i9ZSFbKFS2X@ zDtRHNmCoPWI{9C3R-E75V&TrT*49HlLohYd(mME3fZvAIM~tqveZF6KxH}{vM^{3q zK=UMO96>++skub+la$dP1jtxH<#t=ip7UM>@};b*WjOjqTtw^+j;-=HV34wUHn7DRN!{*!5{4tPqzrn z4sp<)lX&Ay-=WYyo`PpKJ1{P~x}h<;U0>fpe2S#p@y*qzr#=!}V|-^_8(V^oz7^{_ zK`q|s6@i}muNeOtT$(fazFlSCf!XtxJu%kUxqpJ`_q!M7FJR@>T08amnr&C71YHX4 zmGSaDkbkC~Q<3wS>(zPH;{FX`0?QRuPQ*%u+NqnbGdwntjVWtIdl%+N{CoR6=*JPZ zSN|RM_JwYr)2*^(Zv5p7?159;maEzb$4rz85ml*`_1zg&s`5OJ!I4ky{#%)i90M;aYGI7|G>)g!rPdrwVpnYj14!lK_b zhkP|F%Qi(Db$FJxM1T4oo;WqT;pQ2i-lPRr*Dp0$9(H5J z>NygJ8?_HJs@Xea$9cyF%BC@|%WKYfv>`~~g;{T@Zq`ATd_z^^^sxCenZuVvc9ocW zy|uj*x$>il_J`9m-)diToV1Q9$%naMnb(|K7Vp&OUkR0Px>)p=Z9_*j)7MuM8KV6c zK3i9{*?#fax@XS{*R&sE^qog zY1PK;B~~}TFWH;OpT+$%B;IP<^Q((?{qniz5p!qir!AbU)7D5XY;s<+m2m;PmZ|C9 zwWrn!ty};3w#oe`o91y$Rk2Dbu`AJBaPf%lLMgAwzqmcrEhjzcE?na=f0;;^R6>wv zZ{024q%zYL@-nxM-O~11;+J`$`~l~O^w-iUF zD9*cdX!rTE|Ne#^c>gxhz|3R6;z^zG^`Ez{Ja_qG#MLAA+fHb2+WA`Vg%~e4d-5M? z)BkO+vS$iSIe76%+rRyDpZX*DOi?6k`d_uDBciSXi@stHlPABYs z{nNcwyw;;~%KiGq`=fl;n9sW>G$Zov`f8V4c;9m5{M012#H{ghWM6vV)7U3F z3fWJ&oLqDI|CL_{T7A zpMBY5vAx=P+iPh$&lk)nU7P7FVRnAi@0Sh!e3Q*S?EVtN9@MIKZT8ZCT5W1>PK!TR zIEZRImAg{>RBL*R>568qa~qA`@YHs-+^=*xmOlO6(zN{<6O5lRH_jG&S`+hf)~&7Q zC0bcxYnv|~*f70b?0D5v8$Yr6TMx#|Eh-m$l<`dX{;Bh-GY`4DS3h`IcU^D#BBqTF zS)ckonTw{YX=b|^GDJ;y%+nfg&vI6V$5AEGXJxzsXKlQIW=5RyBawzvPYSZOG8d@G z_L{!8IV>1Zem&OZ*@eA^=Fb)_NPFgda_bg>-uRQP0;L_vUY6`tcO*VbxkxO3$iV;n zv}@kSZ4BXO*(d7n++_H8YiFv#lUaZ2p3E@xmyRt^@pqEzc)($(kfs-LFf{1&ojJWf zm-MkTL|!RZHsw8L>AmTAX>sp^mA~h**z-*(Em)Yf?1;AUk2AGX+{O5wH7&H>d`{!c z?9~reee_?zl6PMBl|iN1<)*ZbI}xeQ-KSPvUvq1+@Og#USyw%mN!Om;oOh@pgsWG@dDdvFaV%}G$GOrjuk}FbPHsKO~{nFa`NiU@) z9lP_UuefwBH}c)3{}aQ^pO%=Oxab>j=$6*^gN7!x zU8l8+OHVL&s?|BB>rIu7y3+oAYG&A?=`PR0*Owhg?8wo}m?*U*Ey(54!gsty_hzo? z{i3;i!Okfnbu0FFiC(VUBhta#d96GA9PibhW#&v=Jg#e;cL;MPv~FxEx-iAHtNG8; zy2r+O>8VRYrGGpSH+dF1<=T?af|TTL8Q+C>S1kO~DX~aaTbs9~t3r8N;d`#@zc{Y{ zm@@g}a$Dc2Y_H8MX7=c2g?^1z@Hc92+VOv-^r3^r{WdPkITeK8vCI^E$zrs>ZOv!P z0(~2IiKf(l8*lY=Ef0RN(~QkM^2TN(=Y#WWV~s1-9U7h1<*t%D{PfTLr`hss8m%#( zLvDC++I)z4aGvoq`+}d6NB`Ir9@DwBN0K8~I3pyuG=u&6BnyG+e!sr?R|`&DZ}|By zWc`OzqQ|~1^5>Otej8d_Ja@qk<=rKr3`{v}`(_$mw$#@#JtVT=%z~Ng%G|W7)3z@a z*=Kk4P{`wZ;sy)OE?}sAwa@gKfR&~6J4P+D=ViRdr|NtV35o37yf;DN=a=$efA{Fx z2x*-p*>z6@OiplKTY9ZharuHyQ3qRIzrxK9-F~h$0ecr!8!XLR^<##0N-^)rFS@gC zc#5XHm=d4+J%0$zTz@-5Yz6@Je2=;QF}O&2(xPcBuTGLL)lA;EdxygEO% zmzI26Rym#ZWm@G=PnC=U`?<;L#v2coER-$y{`9x~!WT>}q6Ke7Jk|FJuKamrjlWZp zQVyTKio~xQg6qCqwP*F+e@mpC@y7}N6(Wj%YTX~cZnHXEQu-x4y1uXA%GGa-OquH& zcF(fNa+|wB_rXGz9g8Z`@9$7AJ}4)?U_qe3m3Z524S!C~YVlNTQl5Cu|L#8P^mSd; z2H8T}!dQ1i-o-nY^x+Qv||7Bg!vNJEM*n9;W#aisn?>?22I>)$Z zd-R?!Q|;f%v%Xgi_ccBGYDb9WGtG1-vA$;)u9)1_>fadidzJUpCZR^xv!UEi8t&w; zx!5n9z19EXhnwL}o!V}P64^O6rOCP)o*Ab+`nm~z%XWQkG7EcYxYBkbN71F7+7ASy za;N{^xGmw_h37UF;c8{`pD5=T%s=UF-TwR?Q%PrZ*rC^{&GKIx-3kR}6~9{KeZKYi z!|f?itC(h8&=yVj{P4$JW7nwGrAL>mF`qtST%dkyaog+UPZzt3KAiMXt5dq@J6)%> zbaVWn-p`Mp{!DjR_m8drS8MCn%QcR|f4!XdsoiOQe~@!YM0KHK+yR_=bXXdA<=l-%yxi48Y>db+O0h-f&; zRQ5JL%F^`r`23Rpd+VxsM)U98_Br}~Bje>XjpxNz&uMy_Cf|yg_(5}jP&^y=)Sjfz zl3HcV48BPsyk}EqJFr&W`nuNc(3Z7^MN%`(RyHuitoe82Irp6tOkL)7g%4jo(yF;5 z`nj}ItEp(m=AFj-56BsN#OE08xb<)2yN$BffA6+P`<%)rtdte9w0q|=-n*8U?CMoT zw5O?aJ^sfb`|wDY#Pr}+>*b$U?`4~0Sn0@C#>9~v(CRp?B_!K;wXt}T-Pzx(HEX6l zIHeVEVAd5|kM4hMHWBeBlsB!bd@8(aMf}4T({Jp#R{GF$MR0ZQlmz=zQ*KIjPA&9U zdtyUmj6TQi)sGue-bP(1Y*4GxE|dC}CA&n#e>0;{-=--iALNE}%e}Sem~tsH`cJXZ z_O)ViWpmXtmwlS=#eZOx;zipa!zKAa-2Y$I@UOkSa<_L}9kt4@0=ZQfiEmi0hT*W>U9 z$(V`VCp`2*tgM13sc+#+fA#s+X%S(IgtY%6$F4R^{3FB5Wx;k`b?=KGs;gYOr}#bC zXnz2CQ_XVkIO;21){ye+2 z|NJKX`|}PzWlw*&{_4FSE|WfFJ$fqf>`Au7gDkPaMHb@SrcLJ=>-MI<<#ltFzVtp- z=i-w4FQiSj%nP;g|HTlqZQ3pk@ngoyj*%>Xo@Ev+Xl=R|uxUeIkU;(Pu+9EY{=Cy! z{jgiYZT}C;7tdDhW8;1K&Gj5BpM#!B7Wav}6K>9M{Ih4{!>`)g)`xq3U*a%_ck9xR zy)t$7L0hGspV|BB>!;p zQET1z%EINos>to5oVOE1S5@m@U(m_AVzKuv_RVv=r%$U1TE zFJaPnkFVM$?uuN_V}UH8?^+)W^*oKIUwd&)M%R&(&GPZgr3-czl|7uCJ@E|34H=16 z<4YAhD}qcNT8|y+II~KY;f!M9`m>EUN-u3*v~Py7*q%ixCqs8~zcduRb1OFJ=}zeb zIWwO2NI6NW`expnTl!%6td9@sKIlC^U2q|O)2iH;88HGv@k}S$9qw}{OG=1Xm0w>l z-#AljjjZkb#SbjEuJ1F5OIq{d>>xabV`R?*Xnb? zDU%hajjDnpwkD)5x@VE|THSujku!Z0gzheU!Fx33;7qNBhwP6h@H<&+J)BkSw)&*l zjw#E%Lz-l^KCHjpe*a)E+vUlZlyz*Q4Q6`e8&8^cNo6H#)tTjM<$vg&Y|<}vnmUKa z>mEX3qK)^xP(u)4+)1d&Ts12R`!u_ZOaS7om7i$z|92aMQ{<-3F_Z zzXHQp7CdZTp6EUK3%}g->1UR|%?jZbb-4EJFjvpIop%q)G`?A~yJzbrMV9MwimNy1 z9=C07+2CTZMJjl9$?3hhKGMnAJ3Vx{i^HBjpV6vnb5uQsp^4MMA@uc`xw>q|t7U^b z7kZtEnN|6%k5%)6#PgGpomYPqvTn{xXi1y!DrMf&oImScAB`1E@s<_a>TJi?8@jNd zLuDa<`kck@*4JK%ldAA?(MbNHe*Bv3qPz#{QpUC#yZv|X($6dut4oq$ThF-b^2Wp; zO70m4CB@x8i}WR2C|GH`up-ZN$M&!-MPJRegV!Y{_Uu^|nvpb*`_~3Xm8gq4^W|^J zC|;krUSH;RlJuNcAM>)vBXM$&h3BOrQ{xoYfd%$@=Iij(7nz4VXn(r#9zkq&hIh% zx_1UgjmWJHW#=lUU0s&3BkA_CM_w}mnB`A|TN|p}lxGainl#1aKTGMYNBPW+JJR@G zI$BSYiJI~Mp=YJl_8m)mCfs^?C28)%2`zdq@oRpq^p*8qc+kG4>(jnP{jn>Th`PN< z=GnaH*z-?UFVu$qGIpQsq!4Q_xHMZ+ZQ6wN84tI7U3{?l>-Amj5pys3&R&*dza>3# zn)_N-tF#a=wuKgFrl@{=v~6p0%T}Hh22Qdr(sOE^8js3lY2Jv*oqVI!-Bo<|M~(Qc zh4J6{eIB+jq$wIoLI$Tw)6JmFMb6- zoKfez-ceGH_2#OFXSXg{ytH}2$~pFj4jzekvh>`;l@pnd^%lF%o+Ik9Q1OhSq&Y`c zzwx2)=_Rq(-Gqcpe;y3+cUk6JzGV`}RsK6|5vKc|Z&@>wX9Bm?^}mtqk59RteZOK> z%*LsukIYhjf9}k1{5Qijef7FK8<{s--pST;*s~(LDeQ&ZrumvvlzfF3Ec845;IH>r z0Y`WL4c9}yyROr@C&F^7%~-d{Rxm=(Ox9H)`OW&4eP-4>&IyJskJEO3bau|aozJCW z4);Ev@lfyAGb_Cer7gzXAq#Zf`4d-cjEOwNaelexhX=pHb2)GKn_CAJ&F-uDrFCx0 ztXJE+87*|4{*7_}9r2`lxr@$>3EfwpoSVn8cn$wfx36L0LGSasIE1?8Bh>a!ws2ql zEBWi@y{cEEz89>$vmxhY$g_7n&CK1}CgBcO#UvKr_l~~)Bzjkp;P=nn4jlO+a@YTT ze$BDM?SIn!AeZ}RY!&!FKRN%RGUnI3?cMoBymjw?wHL?#;C1}T^Zr^?9-1NqUYk#YLZF>D(M&?OapIr4p_T=YUHIDmp zbfJ058&^~2$ku1JMbR{) zy?Wi}>kfwwPQ8DS^>Yl*nc3^7=+5Civxx0<*wIJIfxaPvTC6#XOhZM!! zm;8HQCEc=4S)X&fV?>%1||GWCL&HUWXMQ?;uU1w;_KGM+H zc(*~&c}mX|4QD|%qXR8788sC(yMo-@($v(_w&=LKyW8`>K3iVFHKmEOX4;KZBAM5E~w}th5tprPbSZVj4(#}6yukZ^U54|%X_>4emMr?l6;ZIr* zgr+{2z*GCBFG+H4P2IBMZ#(Z#npwR->6)#u{j7~~jzLSb=e?5McIy94J$3E&RtvL| zjBtI?wQo9~PgQ(-YyNo_-iJO{g^q~cob=>btrGHv91wDoMcqxM_H1lP1k zk!&$;rq66Fyxg{u_Ydyw;QyU39&jw_-h!L5i_f&$RW$F`YBDXY{d9fu>Xo0 zH^cvsbg9Pg6|?5v_MYJ`{YZGmAr;Y<#J!j53t3ii8sE64xJb~Y_>4hd+y^VcLDjOWO%>Hmgds4epTVeaE#ak;HSL?hEaAUXm zyqo7*{nP12PKL7XjIW(Fs^eu&@880?ZN|ara#P#pR7?x9t>C}GD1J-MGVR`ln$729 zrIO#>N&FM|bNX7R?vEyaSGasj;3|1pUOs1euBN%fRZsmChSp<-h371UpSJ0*trnQA z|Bfl#caH0d!egSJ-xP9P@~NA~elS(w@f#7gY@yGg+e)scSO!g7>Aa9V^|wsG#;OV{ zj+2ispZs7Id}QpI?avQLFr3gG8A03$ha{5-<(S6 zXMa_eS9T^GSgE@BYWOKJx2?)2>ZjTKh$)-!#4u8(;Fp@yc9%a<{Ot|exob|{zWm?% ztx^V)CBp-8Hs&=OmiX`+E-_L4XX6~YkfUE@NB6BW(#)BE7PZVUY!OOaW}=vx8}h89 zKXu-2cw6RQIw(6nv_gwS+O_DQ9*dJ;7 zx+-5+u$;e7S#4TQ)RBxQdzTn}{n)a~WXb$fHAzV>Gv}Xhd7u?^MXjez>#T;CN%>pN zzC&LG^7L1xp3e(^k~YcXW`g$qSrgZ3R&1C$%i`!(CdV$DP`PUym#6JfC~i`&GtLp( zVpx$Du&n!UW4}%7?g=HV5-%ky(ghX>1kI|pIkm!MaR?jdyfa20xqX)ZyjaV(^0VC9 zi&4qv9{4oL^s!2GM1S6vsvWw+Su{1p(fPoX)vM;d5D`_1UGL1J{YN(a+4^TYxsHDH zl0Kx7y!&ZfT%SXvxKHT&lPBid9o$*F*rZf>k;2q@5$p3JvZhLZS|a;;-NV<S1!MZySX4ZHZ`mH~Fr0-wX^^>mt50)pbJOBN>c+PdT zzNYZRsuttS^68a)^R-!)CxlgquDTWK->L&Mg#7MwmaeXjHR0~=mJ_=SUQb-!+7kABx59cQ`}40m zxHRrdeoIhvyt;g|^P2ZPmtHtHK2C2rYm;$QS%V|AIXfXrrY(#f@i7!NAzRsHP;=_&r17kB>rzV4EX>J%kGqgKWEAdM1! z6ANQKg|(7uqI_q3|C{&U3~{)(^3D^}Qz=GIPSmGL)xUhmUH;&Bt>P9obzvViCEgj! zwiw^^nf)ZT)t@ihNBl$c$~8Kna=lY7>@J!cT+!@p$^Ti!KX(aBn|*&`-23GgK8?Tb zgs16x8JFs>m-rRJ#NF!j^pU7?=sc?_2TyQi3bC%$Y5F{op)DtPR+7WavMDnX;_h7K zU|jxER?#oX?Uq?(+Qp?4*rl_?&T9sp&eaS|R%*T*QnBCU&Uu@&;g)X8rg8VVt7dFw zWSE$}J*(37MQCFtr7BFsWmO&BFYEDx(>~bG-K$HTh3>T*|)DKtiuO{uZy>w~j8Yk~ep!SSw!iI1&)EEVAdT^s>$?FBZOAe<&fQ-(J9dDeDKnBhM3G zUz3z*V*cE@G^}Cecjp|2rCC9b7Z=5)S z;;8hF{C@7KYPvU+Bu=rk?$=)L>E>cNHJW$-JI$PHd-hF!&X?$YWm&{=W6K%4Rxaq8 zwrE>q?|~g6&+h1&eVoESCx71@##1R8SssTcd@M3>y4v)2InN~Lwt%xYOM9*OSGsHt zmpsR@f9t0+3i@gu>p!gV-!Xm4(r|~H#dexB0H8+zmRW8*79=a1Z#_DU_*-YBh6HO;!B>gm~=X9Xo{pWSa( zzp?B1znBRUA_o?H+4S`Mn!K{!qT~?sNa5S+yHnM^&EK+-X|*(aqNn@U6cO|Nvurk{ z^0gYc9F|z|l(m#YGx>MNwSAwG)6O*f{PEB0z4mpU*~N3tBs_hoA9|0AFVpjP@zSTN z2cy5Lyk`oH{CITDfduPJryHU?ftO!Bf03VYRK(`})8$h(-?rIp_|Eyy4ov|A>jm~M z{a$Y_TUYL6{bFa@8RuB9srG5oGP`QOeD(K&K9Yy5mpZS_^ktU_*O?pi=2n)6`A;@M zZMOazGnV3i$DRKB8-CgJRsZ-lFMkP*Z|l2CjE9^u^iQr`ae4sdKS z6TfwCvHfix-g_r`THe)iY3T+(S@f>2?PAtn<5?T$d{4irDP+ z;qh~y(N%xxqFtxurhJM#^fP+>+R}AwS9Krd6{=}Rl*gZTQ*Y4*kjvI zg4ZWFxvK4J*&5{K^_$0e^@X2CS9tfY*0DG4%$udK{f>Z)YvqscQ_>pGwwYzcO|O6cYNseT7$9eR){cBY9Vo#RdF?=x~9 zHK9ivY8M{ZVS7YAqJ3?sj&%4z3x5vb`+sK@Jf7*#s#9^{Q;1Bj1VeGK!1_(vGcKL` zv&nYdO1o(8Em=I}Zh(dOh{=wT2lA<Gjhw#9wn+#H?O>MM79`us)Whb$d*|L-zQY)gDBDRALRgh<85GY0lNhb~M15l*>( zDdJhvhr=eXZZ1waAjs};Hc;Zr91qp?voEpidN^IZK_DVSr9Mu;Du;c2$LdQ_axa$& zDzh#1da#`1&L_qXr;Y!<;I4{%)mw4AXX}cJ2l9#h`%_OTS4OEZt|^wiJma;^<-5MY zyz&P8F&EFuiU$0cTD7CCQlU+ z^>gimpuQhZ^PlbO+F8j{Z?@g_M)sDQvqR=sZIN;Rn$lsuHfe(G@#n=>p4?cFpFR{?{j-&ezS`pngulgjp(mYO#*>gx=rJ z7(^#tNY5!LSP_-scr0`YXGm_K!{HB`QbOuxFZvYvj^nmv=@&iwhNNPXh(0Z68;*%y z7LJQFTUajnyUnOLvDoPS<#RvYJy<{C!sntK3Fa=9X0JW145EKD1Sc$;|MZ1tQn83$ z!55*-CC7c{m~UJ0N@};J#e7ZmgykP@ozYqGY)f{a`AjM4ziwqKwTd-o+O2=cJE^-j zMLU{dc1yLk$jJi>({rkAcN&R3&5J+ln0@o!hj7vR9Me{OF#dHbB#DFj++8V!%U;PI zx0Whx`QqFt_pkX;!{+<1PyYJB-nKqOmu)NeF7-RxjwqNMDP`W3%zH!7mE&E7FPE>y zvTO0J@7EsENvT~`V47{@{C9(byw&EOeRj7etcze+E7-pxQA>x8$v}D&N{=0yC^HT)m(d!`ZjgfSWgXU(2o4?dnz` zXLN6d7*t(!UT*)Bx8vE}r>F=`PW!Z zYrW5U!;4=zuU$m%o>6eBhk@l+pLKJcaz6)1Tc}-%IQu=Is!jLSPnpgWu5)&redJ#} zD|LxpxtYb*%lZeuZMyC4;32fmwO`|^u|n~LOU`F5sj1{%+QZPtyg%jJ^nG^%10KFQ zp!6iS!}nM~ z!uJoNjhc(%k0l?u#Hcm>CPQWB<02jHErnNRSH3A@77TNG;gRX2@AyJh&;GN9o||^g zf*sxxM&|eSEf9U1`Hs=FPCWGZRlkmi|GB$lpBQiSQ~0p$xp>8%nomahrJT~X)$@LR zd1LHze^IQ9cDUK?fDfm&FKC*u?G1@Pv)$8_!30FP_)g^4QO7`Jh&$ND#c|r1= zr9MkqlP~niNb!Dbdw#;>?55Le55Eka{^(V(l);)?HRnD0K4paN(_H_C*XF`GrFHvq zxSucKnx1{ZVw36lt7~@6`cyX%%q7|vVv+jogo;%CcS`aDi= z_jWz#epqhdfl5Z9cj`lK#Qj=5 zLwdpAoc5;e-|yS8&NS}#nkeI&eSF7-tBJQ`HXHm8IP+=x+x@cAA5J+}H=pdjX6W5A zXPdmh()bQxDaO+Q9jl$8kn9X`#kD^0HJJZUHGxi&`Ejkn#+x#s^f>2x{!CNt53$=VAHYww(j zUG%2>^HS3ld^;Fw(>3awxLeKLnmKv)ggrdt^Mo%U`JedF9PRooqFFtM^-E^mak}O* zIse^(kGknjFG9R{pH*c~yLR$ycy!pFmX}eo7KftpF5WKF+p<`)M(SF<>*v)0-@B4- zZ=E$`XWd%kbY0_L%(EYzQ2p@ODP<8`ZcMWDbdyfYWuIcbr!Tx#(!XZw1x=>av-I5m zJy^MOdGVE3M+0n4T&wSupKvB6y`(5FaIoC?d3RU%~I7ps|!BNl8bK@R0>IQ zaL5Xmw(Q$HdOEqFl4&HRiSon^P97naEJN;HhCp%(=x40k5hb?_aIE;>SI`WdipeIh*RL9lrbL z&&>1>Y_*pS=Dn#3d3;>bn2-D70oJnLGxlGN61~|obLP*svMqIQegDL5w2;WD=l!5| zWJ&DanYZRSo%@)X*t}oc%W&<4^*s#?vx2t26<`bft|I?ZAB_xlRq{b zHoA4j{NeVKl?D5YyVL~^%zOQK{UiCy9*=)h!=FLHSldPeHJl^ zZ}N^mtBeW@{%g;>wB`KD6%VvJBer2vum;s$U4V#Z9Qv#vUkNr=0hP~ ziadupZhpKVap}xXOO_%A2Xs|zTfoNAY~mN zoY66@Mt9rRvzLB<%B{zxud z?seJmp*r-SWq63!O6G%QE}M=9cb5FTv*=D}Qded|c1qI5_&d^~4?^w-q*^S`{kKh2 z@q^UXT|t%E|0dXP2{`vNC8BX5pok z@xgjK*L=U{!rxXG{_N>5`knJW$gRWn!3>k%J1_l7%=)IIQ``64_s${Lvy;d2d7M z<4e+)(;uEvc)B{^-Ub)$-^`rMbHg7d%{V$Sex>dU*K0umy{tb4ccsoZC=B^6_w6U2 zv2OKS>9()jS4H=P9*A;#ueVlqc(aHJ=bE7OPQQexe-go}>ysu+8&)=-3Y^>Q z7@sb&_vQ3Rndcd;)228}JFGR>@rT#!=)6F&a}Re^F=Xw{R&sO}lT7ZA>k?eBPjEik=hZRU*~>J`2Fz5v%d8+94?9i|8y=OZ759&1*HLvp*?9Er>+Sc>;^1lUp=C9@0B4V+3=d*`LrMLxx z{?`?j^4_f!>I&vc;i?SS|Kin@*_9l(p4#31+>z2E8-8scf0W~&d&dvcI>^jb4wq_mapGAee=F0jtj0XJFtGmzo{z=PhYuc zk+;wBQl!(_nR+Liv?Rg~UnyVd!?lS0X~*NDXH)<0xOp#N#k#jVakh&#gnyAcch+)$ z)W@YvE6?zMT*c1!E<{qo>)V?ZOE#XKcszgR_wy>fA$P9sj(k4-PPOCFqYD=09Dl{u zCDD6IaBtl6t&H=z-|S2(DEVKcHaThaECIW0uZd+-*Y@~yh&gXszI@Y*OBzqJ^aRo` zR!JVL;O=zLKa~GLP~vA;_yxO7do+)#bWU9EDV1?KDq>}@)wP7~q=R85jJBWs{2cZ~ z9ozoY>2k@oWnX@Ie$|P6Co`%2$U@h(79pu_OEhj~Nu@@;6}q9&WK@%w`DeL9k?q0> zs+zmF@5P3;N|ZhKxW&4I^}ExJt9%7d0)NdZ|6a>@=JWiuvP_JfZbuaUuWkAqnmMJB zhhxIO&yVA7ZCi2eN{^(&yat;s@$cTA;4>_EdhF`*+B=O>H=B&icdNBTe0W)%)}_OE z(7>+Ms5p4RyKP729K7ZBTC1W?H7=p-WYLP|ZR>4Anz@RPO}cNa8|l`xQ1r3qtxds8 z1rOEAm+my!rMtaw^E|WFZx&spV_ped!o1ZbPL-jp@~rz z;&sBy1N$RAeyIGaDopUmxFtX9=CtrEiBEoBqM~6XDsjt7Z+$;tQqafp#+ds+dE1}6 zkzUT{WPd8w^)sys|I^3x+;)ZYN`Dy8be~(zao~i�z@D3jQ}D+r3_QN!@BxteL)CWNmz>4NKv? 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 literal 0 HcmV?d00001 diff --git a/images/Ghostty.icon/Assets/Screen Effects.png b/images/Ghostty.icon/Assets/Screen Effects.png new file mode 100644 index 0000000000000000000000000000000000000000..0af7d33388b919891158609e35e4c54a1c915229 GIT binary patch literal 92547 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 literal 0 HcmV?d00001 diff --git a/images/Ghostty.icon/Assets/Screen.png b/images/Ghostty.icon/Assets/Screen.png new file mode 100644 index 0000000000000000000000000000000000000000..2023b6ffabbd53642e0cdf78ef4c0c7ae6b49378 GIT binary patch literal 143481 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

  2. 9r6J?Y#A1^8~esE#drnBjl)05bPmzk+9>6y3K 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