From 987a759f17066ccb5595e3b45b0ab3d7ea2c688c Mon Sep 17 00:00:00 2001 From: Lon Sagisawa Date: Mon, 1 Sep 2025 14:44:57 +0900 Subject: [PATCH 01/49] i18n: update ja_JP translations --- po/ja_JP.UTF-8.po | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index 3d655f774..bda85095d 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -9,7 +9,7 @@ msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-03-21 00:08+0900\n" +"PO-Revision-Date: 2025-09-01 14:43+0900\n" "Last-Translator: Lon Sagisawa \n" "Language-Team: Japanese\n" "Language: ja\n" @@ -88,7 +88,7 @@ msgstr "右に分割" #: src/apprt/gtk/ui/1.5/command-palette.blp:16 msgid "Execute a command…" -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 @@ -161,7 +161,7 @@ msgstr "設定ファイルを開く" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 msgid "Command Palette" -msgstr "" +msgstr "コマンドパレット" #: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 msgid "Terminal Inspector" @@ -209,12 +209,12 @@ msgstr "許可" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 msgid "Remember choice for this split" -msgstr "" +msgstr "この分割ウィンドウに対して設定を記憶する" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "" +msgstr "設定を再読み込みしてプロンプトを再び表示" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 @@ -279,15 +279,15 @@ msgstr "クリップボードにコピーしました" #: src/apprt/gtk/Surface.zig:1268 msgid "Cleared clipboard" -msgstr "" +msgstr "クリップボードを空にしました" #: src/apprt/gtk/Surface.zig:2525 msgid "Command succeeded" -msgstr "" +msgstr "コマンド実行成功" #: src/apprt/gtk/Surface.zig:2527 msgid "Command failed" -msgstr "" +msgstr "コマンド実行失敗" #: src/apprt/gtk/Window.zig:216 msgid "Main Menu" @@ -299,7 +299,7 @@ msgstr "開いているすべてのタブを表示" #: src/apprt/gtk/Window.zig:266 msgid "New Split" -msgstr "" +msgstr "新しい分割" #: src/apprt/gtk/Window.zig:329 msgid "" From ecf3e2ad7d29b5597953388148fb15036e82d814 Mon Sep 17 00:00:00 2001 From: Jesse Miller Date: Sat, 6 Sep 2025 11:26:23 -0600 Subject: [PATCH 02/49] Position-independent font shaper caching Use relative cluster positioning to allow identical texts runs in different row positions to share the same cache entry. --- src/font/shaper/run.zig | 10 +++++++++- src/renderer/generic.zig | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 90917f657..0b6ebeb4c 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -17,6 +17,10 @@ pub const TextRun = struct { /// lower the chance of hash collisions if they become a problem. If /// there are hash collisions, it would result in rendering issues but /// the core data would be correct. + /// + /// The hash is position-independent within the row by using relative + /// cluster positions. This allows identical runs in different positions + /// to share the same cache entry, improving cache efficiency. hash: u64, /// The offset in the row where this run started @@ -77,7 +81,11 @@ pub const RunIterator = struct { // Go through cell by cell and accumulate while we build our run. var j: usize = self.i; while (j < max) : (j += 1) { - const cluster = j; + // Use relative cluster positions (offset from run start) to make + // the shaping cache position-independent. This ensures that runs + // with identical content but different starting positions in the + // row produce the same hash, enabling cache reuse. + const cluster = j - self.i; const cell = &cells[j]; // If we have a selection and we're at a boundary point, then diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index 8726f2951..dd0ed43bc 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2530,7 +2530,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // Advance our index until we reach or pass // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x < x) { + while (shaper_cells.?[shaper_cells_i].x + run.offset < x) { shaper_cells_i += 1; } } @@ -2769,13 +2769,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // 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); + assert(cells[shaper_cells_i].x + run.offset >= 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) : ({ + while (shaper_cells_i < cells.len and cells[shaper_cells_i].x + run.offset == x) : ({ shaper_cells_i += 1; }) { self.addGlyph( From 31e54ff44abae6c410de933f5867ee7c1d537369 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 8 Sep 2025 10:40:56 -0600 Subject: [PATCH 03/49] comment + style changes --- src/font/shape.zig | 8 +++++--- src/font/shaper/run.zig | 4 +++- src/renderer/generic.zig | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/font/shape.zig b/src/font/shape.zig index 5e1e30eec..dd0f3dcc5 100644 --- a/src/font/shape.zig +++ b/src/font/shape.zig @@ -38,9 +38,11 @@ pub const Shaper = switch (options.backend) { /// for a shaping call. Note all terminal cells may be present; only /// cells that have a glyph that needs to be rendered. pub const Cell = struct { - /// The column that this cell occupies. Since a set of shaper cells is - /// always on the same line, only the X is stored. It is expected the - /// caller has access to the original screen cell. + /// The X position of this shaper cell relative to the offset of the + /// run. Because runs are always within a single row, it is expected + /// that the caller can reconstruct the full position of the cell by + /// using the known Y position of the cell and adding the X position + /// to the run offset. x: u16, /// An additional offset to apply to the rendering. diff --git a/src/font/shaper/run.zig b/src/font/shaper/run.zig index 0b6ebeb4c..7bd019fd7 100644 --- a/src/font/shaper/run.zig +++ b/src/font/shaper/run.zig @@ -23,7 +23,9 @@ pub const TextRun = struct { /// to share the same cache entry, improving cache efficiency. hash: u64, - /// The offset in the row where this run started + /// The offset in the row where this run started. This is added to the + /// X position of the final shaped cells to get the absolute position + /// in the row where they belong. offset: u16, /// The total number of cells produced by this run. diff --git a/src/renderer/generic.zig b/src/renderer/generic.zig index dd0ed43bc..a72acf5c2 100644 --- a/src/renderer/generic.zig +++ b/src/renderer/generic.zig @@ -2528,9 +2528,11 @@ pub fn Renderer(comptime GraphicsAPI: type) type { break :cache cells; }; + const cells = shaper_cells.?; + // Advance our index until we reach or pass // our current x position in the shaper cells. - while (shaper_cells.?[shaper_cells_i].x + run.offset < x) { + while (run.offset + cells[shaper_cells_i].x < x) { shaper_cells_i += 1; } } @@ -2769,13 +2771,13 @@ pub fn Renderer(comptime GraphicsAPI: type) type { // 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 + run.offset >= x); + assert(run.offset + 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 + run.offset == x) : ({ + while (shaper_cells_i < cells.len and run.offset + cells[shaper_cells_i].x == x) : ({ shaper_cells_i += 1; }) { self.addGlyph( From 85b149a2aa1c94e9b262d443b97036f67f3913f1 Mon Sep 17 00:00:00 2001 From: Lon Sagisawa Date: Wed, 10 Sep 2025 02:02:26 +0900 Subject: [PATCH 04/49] Update po/ja_JP.UTF-8.po Co-authored-by: Takayuki Nagatomi --- po/ja_JP.UTF-8.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ja_JP.UTF-8.po b/po/ja_JP.UTF-8.po index bda85095d..43dc76c73 100644 --- a/po/ja_JP.UTF-8.po +++ b/po/ja_JP.UTF-8.po @@ -214,7 +214,7 @@ msgstr "この分割ウィンドウに対して設定を記憶する" #: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 msgid "Reload configuration to show this prompt again" -msgstr "設定を再読み込みしてプロンプトを再び表示" +msgstr "このプロンプトを再び表示するには設定を再読み込みしてください" #: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 #: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 From 5aac5a764b7058587489e728191fcc36f09c33e7 Mon Sep 17 00:00:00 2001 From: Misairuzame <45609522+Misairuzame@users.noreply.github.com> Date: Wed, 10 Sep 2025 20:17:48 +0200 Subject: [PATCH 05/49] i18n: add Italian translation (#7074) --- CODEOWNERS | 1 + po/it_IT.UTF-8.po | 320 ++++++++++++++++++++++++++++++++++++++++++++++ src/os/i18n.zig | 1 + 3 files changed, 322 insertions(+) create mode 100644 po/it_IT.UTF-8.po diff --git a/CODEOWNERS b/CODEOWNERS index 2a93ce671..801066686 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -183,6 +183,7 @@ /po/ga_IE.UTF-8.po @ghostty-org/ga_IE /po/ko_KR.UTF-8.po @ghostty-org/ko_KR /po/he_IL.UTF-8.po @ghostty-org/he_IL +/po/it_IT.UTF-8.po @ghostty-org/it_IT # Packaging - Snap /snap/ @ghostty-org/snap diff --git a/po/it_IT.UTF-8.po b/po/it_IT.UTF-8.po new file mode 100644 index 000000000..d72eee95f --- /dev/null +++ b/po/it_IT.UTF-8.po @@ -0,0 +1,320 @@ +# Italian translations for com.mitchellh.ghostty package +# Traduzioni italiane per il pacchetto com.mitchellh.ghostty. +# Copyright (C) 2025 "Mitchell Hashimoto, Ghostty contributors" +# This file is distributed under the same license as the com.mitchellh.ghostty package. +# Giacomo Bettini , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: com.mitchellh.ghostty\n" +"Report-Msgid-Bugs-To: m@mitchellh.com\n" +"POT-Creation-Date: 2025-07-22 17:18+0000\n" +"PO-Revision-Date: 2025-09-06 19:40+0200\n" +"Last-Translator: Giacomo Bettini \n" +"Language-Team: Italian \n" +"Language: it\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 "Cambia il titolo del terminale" + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6 +msgid "Leave blank to restore the default title." +msgstr "Lasciare vuoto per ripristinare il titolo predefinito." + +#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 +#: src/apprt/gtk/CloseDialog.zig:44 +msgid "Cancel" +msgstr "Annulla" + +#: 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 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5 +msgid "Configuration Errors" +msgstr "Errori di configurazione" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6 +msgid "" +"One or more configuration errors were found. Please review the errors below, " +"and either reload your configuration or ignore these errors." +msgstr "" +"Sono stati trovati uno o più errori di configurazione. Controlla gli errori seguenti, " +"poi ricarica la tua configurazione o ignora quegli errori." + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9 +msgid "Ignore" +msgstr "Ignora" + +#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10 +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:100 +#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:10 +msgid "Reload Configuration" +msgstr "Ricarica configurazione" + +#: 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 "Dividi in alto" + +#: 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 "Dividi in basso" + +#: 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 "Dividi a sinistra" + +#: 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 "Dividi a destra" + +#: src/apprt/gtk/ui/1.5/command-palette.blp:16 +msgid "Execute a command…" +msgstr "Esegui un comando…" + +#: 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 src/apprt/gtk/ui/1.2/ccw-paste.blp:11 +msgid "Paste" +msgstr "Incolla" + +#: 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 "Pulisci" + +#: 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 "Reimposta" + +#: 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 "Divisione" + +#: 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 "Cambia titolo…" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59 +msgid "Tab" +msgstr "Scheda" + +#: 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:265 +msgid "New Tab" +msgstr "Nuova scheda" + +#: 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 "Chiudi scheda" + +#: 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 "Nuova 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 "Chiudi finestra" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89 +msgid "Config" +msgstr "Configurazione" + +#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92 +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95 +msgid "Open Configuration" +msgstr "Apri configurazione" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85 +msgid "Command Palette" +msgstr "Riquadro comandi" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90 +msgid "Terminal Inspector" +msgstr "Ispettore del terminale" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107 +#: src/apprt/gtk/Window.zig:1038 +msgid "About Ghostty" +msgstr "Informazioni su Ghostty" + +#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112 +msgid "Quit" +msgstr "Chiudi" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:6 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:6 +msgid "Authorize Clipboard Access" +msgstr "Consenti accesso agli Appunti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7 +msgid "" +"An application is attempting to read from the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Un'applicazione sta cercando di leggere dagli Appunti. Il contenuto " +"attuale degli Appunti è mostrato di seguito." + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10 +msgid "Deny" +msgstr "Non consentire" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:11 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:11 +msgid "Allow" +msgstr "Consenti" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77 +msgid "Remember choice for this split" +msgstr "Ricorda scelta per questa divisione" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82 +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78 +msgid "Reload configuration to show this prompt again" +msgstr "Ricarica la configurazione per visualizzare nuovamente questo messaggio" + +#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7 +#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7 +msgid "" +"An application is attempting to write to the clipboard. The current " +"clipboard contents are shown below." +msgstr "" +"Un'applicazione sta cercando di scrivere negli Appunti. Il contenuto " +"attuale degli Appunti è mostrato di seguito." + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6 +msgid "Warning: Potentially Unsafe Paste" +msgstr "Attenzione: Incolla potenzialmente pericoloso" + +#: src/apprt/gtk/ui/1.5/ccw-paste.blp:7 src/apprt/gtk/ui/1.2/ccw-paste.blp:7 +msgid "" +"Pasting this text into the terminal may be dangerous as it looks like some " +"commands may be executed." +msgstr "" +"Incollare questo testo nel terminale potrebbe essere pericoloso poiché " +"sembra contenere comandi che potrebbero venire eseguiti." + +#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531 +msgid "Close" +msgstr "Chiudi" + +#: src/apprt/gtk/CloseDialog.zig:87 +msgid "Quit Ghostty?" +msgstr "Chiudere Ghostty?" + +#: src/apprt/gtk/CloseDialog.zig:88 +msgid "Close Window?" +msgstr "Chiudere la finestra?" + +#: src/apprt/gtk/CloseDialog.zig:89 +msgid "Close Tab?" +msgstr "Chiudere la scheda?" + +#: src/apprt/gtk/CloseDialog.zig:90 +msgid "Close Split?" +msgstr "Chiudere la divisione?" + +#: src/apprt/gtk/CloseDialog.zig:96 +msgid "All terminal sessions will be terminated." +msgstr "Tutte le sessioni del terminale saranno terminate." + +#: src/apprt/gtk/CloseDialog.zig:97 +msgid "All terminal sessions in this window will be terminated." +msgstr "Tutte le sessioni del terminale in questa finestra saranno terminate." + +#: src/apprt/gtk/CloseDialog.zig:98 +msgid "All terminal sessions in this tab will be terminated." +msgstr "Tutte le sessioni del terminale in questa scheda saranno terminate." + +#: src/apprt/gtk/CloseDialog.zig:99 +msgid "The currently running process in this split will be terminated." +msgstr "Il processo attualmente in esecuzione in questa divisione sarà terminato." + +#: src/apprt/gtk/Surface.zig:1266 +msgid "Copied to clipboard" +msgstr "Copiato negli Appunti" + +#: src/apprt/gtk/Surface.zig:1268 +msgid "Cleared clipboard" +msgstr "Appunti svuotati" + +#: src/apprt/gtk/Surface.zig:2525 +msgid "Command succeeded" +msgstr "Comando riuscito" + +#: src/apprt/gtk/Surface.zig:2527 +msgid "Command failed" +msgstr "Comando fallito" + +#: src/apprt/gtk/Window.zig:216 +msgid "Main Menu" +msgstr "Menù principale" + +#: src/apprt/gtk/Window.zig:239 +msgid "View Open Tabs" +msgstr "Vedi schede aperte" + +#: src/apprt/gtk/Window.zig:266 +msgid "New Split" +msgstr "Nuova divisione" + +#: src/apprt/gtk/Window.zig:329 +msgid "" +"⚠️ You're running a debug build of Ghostty! Performance will be degraded." +msgstr "" +"⚠️ Stai usando una build di debug di Ghostty! Le prestazioni saranno ridotte." + +#: src/apprt/gtk/Window.zig:775 +msgid "Reloaded the configuration" +msgstr "Configurazione ricaricata" + +#: src/apprt/gtk/Window.zig:1019 +msgid "Ghostty Developers" +msgstr "Sviluppatori di Ghostty" + +#: src/apprt/gtk/inspector.zig:144 +msgid "Ghostty: Terminal Inspector" +msgstr "Ghostty: Ispettore del terminale" diff --git a/src/os/i18n.zig b/src/os/i18n.zig index d39976811..917c34b4a 100644 --- a/src/os/i18n.zig +++ b/src/os/i18n.zig @@ -47,6 +47,7 @@ pub const locales = [_][:0]const u8{ "es_AR.UTF-8", "pt_BR.UTF-8", "ca_ES.UTF-8", + "it_IT.UTF-8", "bg_BG.UTF-8", "ga_IE.UTF-8", "hu_HU.UTF-8", From 83024e08bc8a36a004655dd246707c5d0cfa2b68 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 10 Sep 2025 20:04:23 -0700 Subject: [PATCH 06/49] macos: update Sparkle to 2.7.3 Fixes some critical issues. --- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7ecedbc14..db3dd11a5 100644 --- a/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/macos/Ghostty.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { - "revision" : "df074165274afaa39539c05d57b0832620775b11", - "version" : "2.7.1" + "revision" : "06beff60e3b609a485290e4d5d2d1e2eedb1e55d", + "version" : "2.7.3" } } ], From b4ecadf2e9946f1b2032ea475a1b4d74abcf15d7 Mon Sep 17 00:00:00 2001 From: Yaacov Akiba Slama Date: Thu, 11 Sep 2025 12:17:49 +0300 Subject: [PATCH 07/49] Add default bindings for the "copy" and "paste" keyboard keys. These keys are present in some old unix keyboards, but more importantly, their keycodes can be mapped to physical keys in modern programmable keyboards. Using them in Linux is a way to be able to have the same keys for copy/pasting in GUI apps and in terminal apps instead of switching between ctrl-c/ctrl-v and ctrl-shift-c/ctrl-shift-v. --- src/config/Config.zig | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 3ef39d87c..e76885891 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -5564,6 +5564,17 @@ pub const Keybinds = struct { ); { + try self.set.put( + alloc, + .{ .key = .{ .physical = .copy } }, + .{ .copy_to_clipboard = {} }, + ); + try self.set.put( + alloc, + .{ .key = .{ .physical = .paste } }, + .{ .paste_from_clipboard = {} }, + ); + // On non-MacOS desktop envs (Windows, KDE, Gnome, Xfce), ctrl+insert is an // alt keybinding for Copy and shift+ins is an alt keybinding for Paste // From 4717f8d22b726c5374ffb684922a86d89414051c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 9 Sep 2025 14:44:30 -0700 Subject: [PATCH 08/49] terminal: special/dynamic color enums --- src/terminal/color.zig | 79 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/terminal/color.zig b/src/terminal/color.zig index 08f725d5c..d108e205b 100644 --- a/src/terminal/color.zig +++ b/src/terminal/color.zig @@ -94,6 +94,85 @@ pub const Name = enum(u8) { } }; +/// The "special colors" as denoted by xterm. These can be set via +/// OSC 5 or via OSC 4 by adding the palette length to it. +/// +/// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html +pub const Special = enum(u3) { + bold = 0, + underline = 1, + blink = 2, + reverse = 3, + italic = 4, + + pub fn osc4(self: Special) u16 { + // "The special colors can also be set by adding the maximum + // number of colors (e.g., 88 or 256) to these codes in an + // OSC 4 control" - xterm ctlseqs + const max = @typeInfo(Palette).array.len; + return @as(u16, @intCast(@intFromEnum(self))) + max; + } + + test "osc4" { + const testing = std.testing; + try testing.expectEqual(256, Special.bold.osc4()); + try testing.expectEqual(257, Special.underline.osc4()); + try testing.expectEqual(258, Special.blink.osc4()); + try testing.expectEqual(259, Special.reverse.osc4()); + try testing.expectEqual(260, Special.italic.osc4()); + } +}; + +test Special { + _ = Special; +} + +/// The "dynamic colors" as denoted by xterm. These can be set via +/// OSC 10 through 19. +pub const Dynamic = enum(u5) { + foreground = 10, + background = 11, + cursor = 12, + pointer_foreground = 13, + pointer_background = 14, + tektronix_foreground = 15, + tektronix_background = 16, + highlight_background = 17, + tektronix_cursor = 18, + highlight_foreground = 19, + + /// The next dynamic color sequentially. This is required because + /// specifying colors sequentially without their index will automatically + /// use the next dynamic color. + /// + /// "Each successive parameter changes the next color in the list. The + /// value of Ps tells the starting point in the list." + pub fn next(self: Dynamic) ?Dynamic { + return std.meta.intToEnum( + Dynamic, + @intFromEnum(self) + 1, + ) catch null; + } + + test "next" { + const testing = std.testing; + try testing.expectEqual(.background, Dynamic.foreground.next()); + try testing.expectEqual(.cursor, Dynamic.background.next()); + try testing.expectEqual(.pointer_foreground, Dynamic.cursor.next()); + try testing.expectEqual(.pointer_background, Dynamic.pointer_foreground.next()); + try testing.expectEqual(.tektronix_foreground, Dynamic.pointer_background.next()); + try testing.expectEqual(.tektronix_background, Dynamic.tektronix_foreground.next()); + try testing.expectEqual(.highlight_background, Dynamic.tektronix_background.next()); + try testing.expectEqual(.tektronix_cursor, Dynamic.highlight_background.next()); + try testing.expectEqual(.highlight_foreground, Dynamic.tektronix_cursor.next()); + try testing.expectEqual(null, Dynamic.highlight_foreground.next()); + } +}; + +test Dynamic { + _ = Dynamic; +} + /// RGB pub const RGB = packed struct(u24) { r: u8 = 0, From aba8981d28e6db4ea20ea30b2fd692246966b45f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 10 Sep 2025 20:02:27 -0700 Subject: [PATCH 09/49] terminal: make dedicated OSC parsing, do dynamic resets --- src/terminal/osc.zig | 5 ++ src/terminal/osc/color.zig | 144 +++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 src/terminal/osc/color.zig diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 7f4f32597..21f5b0dfd 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -12,6 +12,7 @@ const assert = std.debug.assert; const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); +const osc_color = @import("osc/color.zig"); const log = std.log.scoped(.osc); @@ -1710,6 +1711,10 @@ pub const Parser = struct { } }; +test { + _ = osc_color; +} + test "OSC: change_window_title" { const testing = std.testing; diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig new file mode 100644 index 000000000..56e450dd4 --- /dev/null +++ b/src/terminal/osc/color.zig @@ -0,0 +1,144 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const DynamicColor = @import("../color.zig").Dynamic; +const SpecialColor = @import("../color.zig").Special; +const RGB = @import("../color.zig").RGB; + +pub const ParseError = Allocator.Error || error{ + MissingOperation, +}; + +/// The possible operations we support for colors. +pub const Operation = enum { + osc_110, + osc_111, + osc_112, + osc_113, + osc_114, + osc_115, + osc_116, + osc_117, + osc_118, + osc_119, +}; + +/// Parse any color operation string. This should NOT include the operation +/// itself, but only the body of the operation. e.g. for "4;a;b;c" the body +/// should be "a;b;c" and the operation should be set accordingly. +/// +/// Color parsing is fairly complicated so we pull this out to a specialized +/// function rather than go through our OSC parsing state machine. This is +/// much slower and requires more memory (since we need to buffer the full +/// request) but grants us an easier to understand and testable implementation. +/// +/// If color changing ends up being a bottleneck we can optimize this later. +pub fn parse( + alloc: Allocator, + op: Operation, + buf: []const u8, +) ParseError!List { + var it = std.mem.tokenizeScalar(u8, buf, ';'); + return switch (op) { + .osc_110 => try parseResetDynamicColor(alloc, .foreground, &it), + .osc_111 => try parseResetDynamicColor(alloc, .background, &it), + .osc_112 => try parseResetDynamicColor(alloc, .cursor, &it), + .osc_113 => try parseResetDynamicColor(alloc, .pointer_foreground, &it), + .osc_114 => try parseResetDynamicColor(alloc, .pointer_background, &it), + .osc_115 => try parseResetDynamicColor(alloc, .tektronix_foreground, &it), + .osc_116 => try parseResetDynamicColor(alloc, .tektronix_background, &it), + .osc_117 => try parseResetDynamicColor(alloc, .highlight_background, &it), + .osc_118 => try parseResetDynamicColor(alloc, .tektronix_cursor, &it), + .osc_119 => try parseResetDynamicColor(alloc, .highlight_foreground, &it), + }; +} + +fn parseResetDynamicColor( + alloc: Allocator, + color: DynamicColor, + it: *std.mem.TokenIterator(u8, .scalar), +) Allocator.Error!List { + var result: List = .{}; + if (it.next() != null) return result; + const req = try result.addOne(alloc); + req.* = .{ .reset = .{ .dynamic = color } }; + return result; +} + +/// A segmented list is used to avoid copying when many operations +/// are given in a single OSC. In most cases, OSC 4/104/etc. send +/// very few so the prealloc is optimized for that. +/// +/// The exact prealloc value is chosen arbitrarily assuming most +/// color ops have very few. If we can get empirical data on more +/// typical values we can switch to that. +pub const List = std.SegmentedList( + Request, + 2, +); + +/// A single operation related to the terminal color palette. +pub const Request = union(enum) { + set: ColoredTarget, + query: Target, + reset: Target, + reset_palette, + + pub const Target = union(enum) { + palette: u8, + special: SpecialColor, + dynamic: DynamicColor, + }; + + pub const ColoredTarget = struct { + target: Target, + color: RGB, + }; +}; + +// OSC 110-119: Reset Dynamic Colors +test "reset dynamic" { + const testing = std.testing; + const alloc = testing.allocator; + + inline for (@typeInfo(DynamicColor).@"enum".fields) |field| { + const color = @field(DynamicColor, field.name); + const op = @field(Operation, std.fmt.comptimePrint( + "osc_1{d}", + .{field.value}, + )); + + // Example script: + // printf '\e]110\e\\' + { + var list = try parse(alloc, op, ""); + errdefer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset = .{ .dynamic = color } }, + list.at(0).*, + ); + } + + // xterm allows a trailing semicolon. script to verify: + // + // printf '\e]110;\e\\' + { + var list = try parse(alloc, op, ";"); + errdefer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset = .{ .dynamic = color } }, + list.at(0).*, + ); + } + + // xterm does NOT allow any whitespace + // + // printf '\e]110 \e\\' + { + var list = try parse(alloc, op, " "); + errdefer list.deinit(alloc); + try testing.expectEqual(0, list.count()); + } + } +} From 8b56f80e5656e1d5cd029336f6e3865dba86674e Mon Sep 17 00:00:00 2001 From: Chris Marchesi Date: Thu, 11 Sep 2025 10:16:52 -0700 Subject: [PATCH 10/49] deps: update z2d to v0.8.1 Release notes at: https://github.com/vancluever/z2d/blob/v0.8.1/CHANGELOG.md Small release again, just keeping up to date with releases in anticipation of the Ghostty 1.2.0 release. --- 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 d797fff09..78097a180 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -20,8 +20,8 @@ }, .z2d = .{ // vancluever/z2d - .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz", - .hash = "z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr", + .url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", + .hash = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP", .lazy = true, }, .zig_objc = .{ diff --git a/build.zig.zon.json b/build.zig.zon.json index 495622af6..32d243d9a 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -129,10 +129,10 @@ "url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz", "hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM=" }, - "z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr": { + "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP": { "name": "z2d", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz", - "hash": "sha256-0yR5Yc5MxOJBV1cv4LOWBwWkZYcGU53qFZd40TlZPcg=" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", + "hash": "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0=" }, "zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": { "name": "zf", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 8a34f4776..a4630688f 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -291,11 +291,11 @@ in }; } { - name = "z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr"; + name = "z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP"; path = fetchZigArtifact { name = "z2d"; - url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz"; - hash = "sha256-0yR5Yc5MxOJBV1cv4LOWBwWkZYcGU53qFZd40TlZPcg="; + url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz"; + hash = "sha256-0DbDKSYA1ejhVx/WbOkwTgD57PNRFcnRviqBh8xpPZ0="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index f2972853f..0b244e6c5 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -32,4 +32,4 @@ https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811 https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz -https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz +https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 438d5331c..51c28b2a9 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -157,9 +157,9 @@ }, { "type": "archive", - "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.0.tar.gz", - "dest": "vendor/p/z2d-0.8.0-j5P_HgW8DQBvCefcGWPZA1WC5Nco08WhKjG3XCW3thMr", - "sha256": "d3247961ce4cc4e24157572fe0b3960705a4658706539dea159778d139593dc8" + "url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.8.1.tar.gz", + "dest": "vendor/p/z2d-0.8.1-j5P_Hq8vDwB8ZaDA54-SzESDLF2zznG_zvTHiQNJImZP", + "sha256": "d036c3292600d5e8e1571fd66ce9304e00f9ecf35115c9d1be2a8187cc693d9d" }, { "type": "archive", From b4fdefc2c086af49af662acfb5af977d443eba5e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 10:55:38 -0700 Subject: [PATCH 11/49] terminal: OSC 4/5 matches xterm --- src/terminal/osc/color.zig | 282 ++++++++++++++++++++++++++++++++++++- 1 file changed, 279 insertions(+), 3 deletions(-) diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 56e450dd4..6aa379451 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -10,6 +10,8 @@ pub const ParseError = Allocator.Error || error{ /// The possible operations we support for colors. pub const Operation = enum { + osc_4, + osc_5, osc_110, osc_111, osc_112, @@ -39,6 +41,8 @@ pub fn parse( ) ParseError!List { var it = std.mem.tokenizeScalar(u8, buf, ';'); return switch (op) { + .osc_4 => try parseGetSetAnsiColor(alloc, .osc_4, &it), + .osc_5 => try parseGetSetAnsiColor(alloc, .osc_5, &it), .osc_110 => try parseResetDynamicColor(alloc, .foreground, &it), .osc_111 => try parseResetDynamicColor(alloc, .background, &it), .osc_112 => try parseResetDynamicColor(alloc, .cursor, &it), @@ -52,12 +56,74 @@ pub fn parse( }; } +/// OSC 4/5 +fn parseGetSetAnsiColor( + alloc: Allocator, + comptime op: Operation, + it: *std.mem.TokenIterator(u8, .scalar), +) Allocator.Error!List { + // Note: in ANY error scenario below we return the accumulated results. + // This matches the xterm behavior (see misc.c ChangeAnsiColorRequest) + + var result: List = .{}; + errdefer result.deinit(alloc); + while (true) { + // We expect a `c; spec` pair. If either doesn't exist then + // we return the results up to this point. + const color_str = it.next() orelse return result; + const spec_str = it.next() orelse return result; + + // Color must be numeric. u9 because that'll fit our palette + special + const color: u9 = std.fmt.parseInt( + u9, + color_str, + 10, + ) catch return result; + + // Parse the color. + const target: Request.Target = switch (op) { + // OSC5 maps directly to the Special enum. + .osc_5 => .{ .special = std.meta.intToEnum( + SpecialColor, + std.math.cast(u3, color) orelse return result, + ) catch return result }, + + // OSC4 maps 0-255 to palette, 256-259 to special offset + // by the palette count. + .osc_4 => if (std.math.cast(u8, color)) |idx| .{ + .palette = idx, + } else .{ .special = std.meta.intToEnum( + SpecialColor, + std.math.cast(u3, color - 256) orelse return result, + ) catch return result }, + + else => comptime unreachable, + }; + + // "?" always results in a query. + if (std.mem.eql(u8, spec_str, "?")) { + const req = try result.addOne(alloc); + req.* = .{ .query = target }; + continue; + } + + const rgb = RGB.parse(spec_str) catch return result; + const req = try result.addOne(alloc); + req.* = .{ .set = .{ + .target = target, + .color = rgb, + } }; + } +} + +/// OSC 110-119: Reset Dynamic Colors fn parseResetDynamicColor( alloc: Allocator, color: DynamicColor, it: *std.mem.TokenIterator(u8, .scalar), ) Allocator.Error!List { var result: List = .{}; + errdefer result.deinit(alloc); if (it.next() != null) return result; const req = try result.addOne(alloc); req.* = .{ .reset = .{ .dynamic = color } }; @@ -95,6 +161,216 @@ pub const Request = union(enum) { }; }; +test "osc4" { + const testing = std.testing; + const alloc = testing.allocator; + + // Test every palette index + for (0..std.math.maxInt(u8)) |idx| { + // Simple color set + // printf '\e]4;0;red\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d};red", + .{idx}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_4, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = @intCast(idx) }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + } + + // Simple color query + // printf '\e]4;0;?\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d};?", + .{idx}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_4, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .query = .{ .palette = @intCast(idx) } }, + list.at(0).*, + ); + } + + // Trailing invalid data produces results up to that point + // printf '\e]4;0;red;\e\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d};red;", + .{idx}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_4, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = @intCast(idx) }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + } + + // Whitespace doesn't produce a working value in xterm but we + // allow it because Kitty does and it seems harmless. + // + // printf '\e]4;0;red \e\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d};red ", + .{idx}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_4, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = @intCast(idx) }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + } + } + + // Test every special color + for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| { + const special = try std.meta.intToEnum(SpecialColor, i); + + // Simple color set + // printf '\e]4;256;red\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d};red", + .{256 + i}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_4, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .special = special }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + } + } +} + +test "osc5" { + const testing = std.testing; + const alloc = testing.allocator; + + // Test every special color + for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| { + const special = try std.meta.intToEnum(SpecialColor, i); + + // Simple color set + // printf '\e]4;256;red\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d};red", + .{i}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_5, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .special = special }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + } + } +} + +test "osc4: multiple requests" { + const testing = std.testing; + const alloc = testing.allocator; + + // printf '\e]4;0;red;1;blue\e\\' + { + var list = try parse( + alloc, + .osc_4, + "0;red;1;blue", + ); + defer list.deinit(alloc); + try testing.expectEqual(2, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = 0 }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = 1 }, + .color = RGB{ .r = 0, .g = 0, .b = 255 }, + } }, + list.at(1).*, + ); + } + + // Multiple requests with same index overwrite each other + // printf '\e]4;0;red;0;blue\e\\' + { + var list = try parse( + alloc, + .osc_4, + "0;red;0;blue", + ); + defer list.deinit(alloc); + try testing.expectEqual(2, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = 0 }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .palette = 0 }, + .color = RGB{ .r = 0, .g = 0, .b = 255 }, + } }, + list.at(1).*, + ); + } +} + // OSC 110-119: Reset Dynamic Colors test "reset dynamic" { const testing = std.testing; @@ -111,7 +387,7 @@ test "reset dynamic" { // printf '\e]110\e\\' { var list = try parse(alloc, op, ""); - errdefer list.deinit(alloc); + defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( Request{ .reset = .{ .dynamic = color } }, @@ -124,7 +400,7 @@ test "reset dynamic" { // printf '\e]110;\e\\' { var list = try parse(alloc, op, ";"); - errdefer list.deinit(alloc); + defer list.deinit(alloc); try testing.expectEqual(1, list.count()); try testing.expectEqual( Request{ .reset = .{ .dynamic = color } }, @@ -137,7 +413,7 @@ test "reset dynamic" { // printf '\e]110 \e\\' { var list = try parse(alloc, op, " "); - errdefer list.deinit(alloc); + defer list.deinit(alloc); try testing.expectEqual(0, list.count()); } } From d7523e3e37e144a05b596e56bbdc8be79b7d5a08 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 11:12:01 -0700 Subject: [PATCH 12/49] terminal: OSC 10-19 --- src/terminal/osc/color.zig | 114 +++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 6aa379451..8adcb105a 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -12,6 +12,16 @@ pub const ParseError = Allocator.Error || error{ pub const Operation = enum { osc_4, osc_5, + osc_10, + osc_11, + osc_12, + osc_13, + osc_14, + osc_15, + osc_16, + osc_17, + osc_18, + osc_19, osc_110, osc_111, osc_112, @@ -43,6 +53,16 @@ pub fn parse( return switch (op) { .osc_4 => try parseGetSetAnsiColor(alloc, .osc_4, &it), .osc_5 => try parseGetSetAnsiColor(alloc, .osc_5, &it), + .osc_10 => try parseGetSetDynamicColor(alloc, .foreground, &it), + .osc_11 => try parseGetSetDynamicColor(alloc, .background, &it), + .osc_12 => try parseGetSetDynamicColor(alloc, .cursor, &it), + .osc_13 => try parseGetSetDynamicColor(alloc, .pointer_foreground, &it), + .osc_14 => try parseGetSetDynamicColor(alloc, .pointer_background, &it), + .osc_15 => try parseGetSetDynamicColor(alloc, .tektronix_foreground, &it), + .osc_16 => try parseGetSetDynamicColor(alloc, .tektronix_background, &it), + .osc_17 => try parseGetSetDynamicColor(alloc, .highlight_background, &it), + .osc_18 => try parseGetSetDynamicColor(alloc, .tektronix_cursor, &it), + .osc_19 => try parseGetSetDynamicColor(alloc, .highlight_foreground, &it), .osc_110 => try parseResetDynamicColor(alloc, .foreground, &it), .osc_111 => try parseResetDynamicColor(alloc, .background, &it), .osc_112 => try parseResetDynamicColor(alloc, .cursor, &it), @@ -116,6 +136,40 @@ fn parseGetSetAnsiColor( } } +/// OSC 10-19: Get/Set Dynamic Colors +fn parseGetSetDynamicColor( + alloc: Allocator, + start: DynamicColor, + it: *std.mem.TokenIterator(u8, .scalar), +) Allocator.Error!List { + // Note: in ANY error scenario below we return the accumulated results. + // This matches the xterm behavior (see misc.c ChangeColorsRequest) + + var result: List = .{}; + var color: DynamicColor = start; + while (true) { + const spec_str = it.next() orelse return result; + + if (std.mem.eql(u8, spec_str, "?")) { + const req = try result.addOne(alloc); + req.* = .{ .query = .{ .dynamic = color } }; + } else { + const rgb = RGB.parse(spec_str) catch return result; + const req = try result.addOne(alloc); + req.* = .{ .set = .{ + .target = .{ .dynamic = color }, + .color = rgb, + } }; + } + + // Each successive value uses the next color so long as it exists. + color = std.meta.intToEnum( + DynamicColor, + @intFromEnum(color) + 1, + ) catch return result; + } +} + /// OSC 110-119: Reset Dynamic Colors fn parseResetDynamicColor( alloc: Allocator, @@ -371,6 +425,66 @@ test "osc4: multiple requests" { } } +// OSC 10-19: Get/Set Dynamic Colors +test "dynamic" { + const testing = std.testing; + const alloc = testing.allocator; + + inline for (@typeInfo(DynamicColor).@"enum".fields) |field| { + const color = @field(DynamicColor, field.name); + const op = @field(Operation, std.fmt.comptimePrint( + "osc_{d}", + .{field.value}, + )); + + // Example script: + // printf '\e]10;red\e\\' + { + var list = try parse(alloc, op, "red"); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .dynamic = color }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + } + } +} + +test "dynamic multiple" { + const testing = std.testing; + const alloc = testing.allocator; + + // Example script: + // printf '\e]11;red;blue\e\\' + { + var list = try parse( + alloc, + .osc_11, + "red;blue", + ); + defer list.deinit(alloc); + try testing.expectEqual(2, list.count()); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .dynamic = .background }, + .color = RGB{ .r = 255, .g = 0, .b = 0 }, + } }, + list.at(0).*, + ); + try testing.expectEqual( + Request{ .set = .{ + .target = .{ .dynamic = .cursor }, + .color = RGB{ .r = 0, .g = 0, .b = 255 }, + } }, + list.at(1).*, + ); + } +} + // OSC 110-119: Reset Dynamic Colors test "reset dynamic" { const testing = std.testing; From 83dd578cc5e608f402d7eaab9f3cc9fe2b3e1811 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 11:18:11 -0700 Subject: [PATCH 13/49] terminal: osc 104/105 --- src/terminal/osc/color.zig | 136 +++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 8adcb105a..0364b068e 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -22,6 +22,8 @@ pub const Operation = enum { osc_17, osc_18, osc_19, + osc_104, + osc_105, osc_110, osc_111, osc_112, @@ -53,6 +55,8 @@ pub fn parse( return switch (op) { .osc_4 => try parseGetSetAnsiColor(alloc, .osc_4, &it), .osc_5 => try parseGetSetAnsiColor(alloc, .osc_5, &it), + .osc_104 => try parseResetAnsiColor(alloc, .osc_104, &it), + .osc_105 => try parseResetAnsiColor(alloc, .osc_105, &it), .osc_10 => try parseGetSetDynamicColor(alloc, .foreground, &it), .osc_11 => try parseGetSetDynamicColor(alloc, .background, &it), .osc_12 => try parseGetSetDynamicColor(alloc, .cursor, &it), @@ -136,6 +140,60 @@ fn parseGetSetAnsiColor( } } +/// OSC 104/105: Reset ANSI Colors +fn parseResetAnsiColor( + alloc: Allocator, + comptime op: Operation, + it: *std.mem.TokenIterator(u8, .scalar), +) Allocator.Error!List { + var result: List = .{}; + errdefer result.deinit(alloc); + while (true) { + const color_str = it.next() orelse { + // If no parameters are given, we reset the full table. + if (result.count() == 0) { + const req = try result.addOne(alloc); + req.* = switch (op) { + .osc_104 => .reset_palette, + .osc_105 => .reset_special, + else => comptime unreachable, + }; + } + return result; + }; + + // Color must be numeric. u9 because that'll fit our palette + special + const color: u9 = std.fmt.parseInt( + u9, + color_str, + 10, + ) catch return result; + + // Parse the color. + const target: Request.Target = switch (op) { + // OSC105 maps directly to the Special enum. + .osc_105 => .{ .special = std.meta.intToEnum( + SpecialColor, + std.math.cast(u3, color) orelse return result, + ) catch return result }, + + // OSC104 maps 0-255 to palette, 256-259 to special offset + // by the palette count. + .osc_104 => if (std.math.cast(u8, color)) |idx| .{ + .palette = idx, + } else .{ .special = std.meta.intToEnum( + SpecialColor, + std.math.cast(u3, color - 256) orelse return result, + ) catch return result }, + + else => comptime unreachable, + }; + + const req = try result.addOne(alloc); + req.* = .{ .reset = target }; + } +} + /// OSC 10-19: Get/Set Dynamic Colors fn parseGetSetDynamicColor( alloc: Allocator, @@ -202,6 +260,7 @@ pub const Request = union(enum) { query: Target, reset: Target, reset_palette, + reset_special, pub const Target = union(enum) { palette: u8, @@ -425,6 +484,83 @@ test "osc4: multiple requests" { } } +test "osc104" { + const testing = std.testing; + const alloc = testing.allocator; + + // Test every palette index + for (0..std.math.maxInt(u8)) |idx| { + // Simple color set + // printf '\e]104;0\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d}", + .{idx}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_104, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset = .{ .palette = @intCast(idx) } }, + list.at(0).*, + ); + } + } + + // Test every special color + for (0..@typeInfo(SpecialColor).@"enum".fields.len) |i| { + const special = try std.meta.intToEnum(SpecialColor, i); + + // Simple color set + // printf '\e]104;256\\' + { + const body = try std.fmt.allocPrint( + alloc, + "{d}", + .{256 + i}, + ); + defer alloc.free(body); + + var list = try parse(alloc, .osc_104, body); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset = .{ .special = special } }, + list.at(0).*, + ); + } + } +} + +test "osc104 reset all" { + const testing = std.testing; + const alloc = testing.allocator; + + var list = try parse(alloc, .osc_104, ""); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset_palette = {} }, + list.at(0).*, + ); +} + +test "osc105 reset all" { + const testing = std.testing; + const alloc = testing.allocator; + + var list = try parse(alloc, .osc_105, ""); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset_special = {} }, + list.at(0).*, + ); +} + // OSC 10-19: Get/Set Dynamic Colors test "dynamic" { const testing = std.testing; From 4350804f0a57f8c223547a8e02f008e925152b95 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 11:27:25 -0700 Subject: [PATCH 14/49] terminal: osc 10-19 should use next function --- src/terminal/osc/color.zig | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 0364b068e..909406079 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -221,10 +221,7 @@ fn parseGetSetDynamicColor( } // Each successive value uses the next color so long as it exists. - color = std.meta.intToEnum( - DynamicColor, - @intFromEnum(color) + 1, - ) catch return result; + color = color.next() orelse return result; } } From 03e2a8049e5aa394888daefba671d1c403f896c2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 11:34:10 -0700 Subject: [PATCH 15/49] terminal: remove old color tests --- src/terminal/osc.zig | 742 ------------------------------------- src/terminal/osc/color.zig | 20 + 2 files changed, 20 insertions(+), 742 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 21f5b0dfd..997db0350 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -2129,748 +2129,6 @@ test "OSC: longer than buffer" { try testing.expect(p.complete == false); } -test "OSC: OSC10: report foreground color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .color_operation); - try testing.expectEqual(cmd.color_operation.terminator, .st); - 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.ColorOperation.Kind.foreground, - op.report, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC10: set foreground color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .color_operation); - try testing.expectEqual(cmd.color_operation.terminator, .bel); - 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.ColorOperation.Kind.foreground, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0x00, .g = 0x7f, .b = 0xff }, - op.set.color, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC11: report background color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "11;?"; - 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 == .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.ColorOperation.Kind.background, - op.report, - ); - } - try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(it.next() == null); -} - -test "OSC: OSC11: set background color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "11;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 == .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.ColorOperation.Kind.background, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0xff, .g = 0xff, .b = 0xff }, - op.set.color, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC12: report cursor color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind.cursor, - op.report, - ); - } - try testing.expectEqual(cmd.color_operation.terminator, .bel); - try testing.expect(it.next() == null); -} - -test "OSC: OSC12: set cursor color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind.cursor, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0xff, .g = 0xff, .b = 0xff }, - op.set.color, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC4: get palette color 1" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "4;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 == .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 = 1 }, - op.report, - ); - try testing.expectEqual(cmd.color_operation.terminator, .st); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC4: get palette color 2" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 1 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 2 }, - op.report, - ); - } - try testing.expectEqual(cmd.color_operation.terminator, .st); - try testing.expect(it.next() == null); -} - -test "OSC: OSC4: set palette color 1" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "4;17;rgb:aa/bb/cc"; - 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.* == .set); - try testing.expectEqual( - Command.ColorOperation.Kind{ .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" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 17 }, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, - op.set.color, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .set); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 1 }, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0x00, .g = 0x11, .b = 0x22 }, - op.set.color, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC4: get with invalid index 1" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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 = 1 }, - op.report, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC4: get with invalid index 2" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 5 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .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 p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 0 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 1 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 2 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 3 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 4 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 5 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 6 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .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 p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 8 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 9 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 10 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 11 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 12 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 13 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 14 }, - op.report, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .report); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 15 }, - op.report, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC4: set with invalid index" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 1 }, - 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: mix get/set palette color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind{ .palette = 17 }, - op.set.kind, - ); - try testing.expectEqual( - RGB{ .r = 0xaa, .g = 0xbb, .b = 0xcc }, - op.set.color, - ); - } - { - const op = it.next().?; - try testing.expect(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 = .initAlloc(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 = .initAlloc(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); -} - -test "OSC: OSC104: reset palette color 1" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .reset_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.ColorOperation.Kind{ .palette = 17 }, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC104: reset palette color 2" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .reset_palette); - try testing.expectEqual(2, cmd.color_operation.operations.count()); - var it = cmd.color_operation.operations.constIterator(0); - { - const op = it.next().?; - try testing.expect(op.* == .reset); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 17 }, - op.reset, - ); - } - { - const op = it.next().?; - try testing.expect(op.* == .reset); - try testing.expectEqual( - Command.ColorOperation.Kind{ .palette = 111 }, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC104: invalid palette index" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .reset_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.ColorOperation.Kind{ .palette = 111 }, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC104: empty palette index" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .reset_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.ColorOperation.Kind{ .palette = 111 }, - op.reset, - ); - } - try std.testing.expect(it.next() == null); -} - test "OSC: OSC 9;1 ConEmu sleep" { const testing = std.testing; diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 909406079..3c39addd0 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -162,6 +162,9 @@ fn parseResetAnsiColor( return result; }; + // Empty color strings are ignored, not treated as an error. + if (color_str.len == 0) continue; + // Color must be numeric. u9 because that'll fit our palette + special const color: u9 = std.fmt.parseInt( u9, @@ -532,6 +535,23 @@ test "osc104" { } } +test "osc104 empty index" { + const testing = std.testing; + const alloc = testing.allocator; + + var list = try parse(alloc, .osc_104, "0;;1"); + defer list.deinit(alloc); + try testing.expectEqual(2, list.count()); + try testing.expectEqual( + Request{ .reset = .{ .palette = 0 } }, + list.at(0).*, + ); + try testing.expectEqual( + Request{ .reset = .{ .palette = 1 } }, + list.at(1).*, + ); +} + test "osc104 reset all" { const testing = std.testing; const alloc = testing.allocator; From 67b7a5f2677c6ba89a20860fd1ab474b6eedb4d1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 11:38:40 -0700 Subject: [PATCH 16/49] terminal: osc 104/105 are more flexible on invalid index --- src/terminal/osc/color.zig | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 3c39addd0..4d6c9a0de 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -146,6 +146,11 @@ fn parseResetAnsiColor( comptime op: Operation, it: *std.mem.TokenIterator(u8, .scalar), ) Allocator.Error!List { + // Note: xterm stops parsing the reset list on any error, but we're + // more flexible and try the next value. This matches the behavior of + // Kitty and I don't see a downside to being more flexible here. Hopefully + // no one depends on the exact behavior of xterm. + var result: List = .{}; errdefer result.deinit(alloc); while (true) { @@ -170,15 +175,15 @@ fn parseResetAnsiColor( u9, color_str, 10, - ) catch return result; + ) catch continue; // Parse the color. const target: Request.Target = switch (op) { // OSC105 maps directly to the Special enum. .osc_105 => .{ .special = std.meta.intToEnum( SpecialColor, - std.math.cast(u3, color) orelse return result, - ) catch return result }, + std.math.cast(u3, color) orelse continue, + ) catch continue }, // OSC104 maps 0-255 to palette, 256-259 to special offset // by the palette count. @@ -186,8 +191,8 @@ fn parseResetAnsiColor( .palette = idx, } else .{ .special = std.meta.intToEnum( SpecialColor, - std.math.cast(u3, color - 256) orelse return result, - ) catch return result }, + std.math.cast(u3, color - 256) orelse continue, + ) catch continue }, else => comptime unreachable, }; @@ -552,6 +557,19 @@ test "osc104 empty index" { ); } +test "osc104 invalid index" { + const testing = std.testing; + const alloc = testing.allocator; + + var list = try parse(alloc, .osc_104, "ffff;1"); + defer list.deinit(alloc); + try testing.expectEqual(1, list.count()); + try testing.expectEqual( + Request{ .reset = .{ .palette = 1 } }, + list.at(0).*, + ); +} + test "osc104 reset all" { const testing = std.testing; const alloc = testing.allocator; From 3afc8019d596e0954074963887f535d8a516cb49 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 12:09:52 -0700 Subject: [PATCH 17/49] terminal: update parser to use new color parser and stream handler --- src/Surface.zig | 16 +- src/apprt/surface.zig | 5 +- src/terminal/Parser.zig | 10 +- src/terminal/osc.zig | 604 ++++++++++++---------------------- src/terminal/osc/color.zig | 22 +- src/terminal/stream.zig | 6 +- src/termio/stream_handler.zig | 196 +++++++---- 7 files changed, 367 insertions(+), 492 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index bfadb3be8..dd2babf9b 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -863,18 +863,24 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { }, .unlocked); }, - .color_change => |change| { + .color_change => |change| color_change: { // Notify our apprt, but don't send a mode 2031 DSR report // because VT sequences were used to change the color. _ = try self.rt_app.performAction( .{ .surface = self }, .color_change, .{ - .kind = switch (change.kind) { - .background => .background, - .foreground => .foreground, - .cursor => .cursor, + .kind = switch (change.target) { .palette => |v| @enumFromInt(v), + .dynamic => |dyn| switch (dyn) { + .foreground => .foreground, + .background => .background, + .cursor => .cursor, + // Unsupported dynamic color change notification type + else => break :color_change, + }, + // Special colors aren't supported for change notification + .special => break :color_change, }, .r = change.color.r, .g = change.color.g, diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index a4070c668..e4effe128 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -78,10 +78,7 @@ pub const Message = union(enum) { password_input: bool, /// A terminal color was changed using OSC sequences. - color_change: struct { - kind: terminal.osc.Command.ColorOperation.Kind, - color: terminal.color.RGB, - }, + color_change: terminal.osc.color.ColoredTarget, /// Notifies the surface that a tick of the timer that is timing /// out selection scrolling has occurred. "selection scrolling" diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 428274878..0c814ff68 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -915,15 +915,15 @@ 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 == .reset_cursor); - try testing.expect(cmd.color_operation.operations.count() == 1); - var it = cmd.color_operation.operations.constIterator(0); + try testing.expect(cmd.color_operation.op == .osc_112); + try testing.expect(cmd.color_operation.requests.count() == 1); + var it = cmd.color_operation.requests.constIterator(0); { const op = it.next().?; try testing.expect(op.* == .reset); try testing.expectEqual( - osc.Command.ColorOperation.Kind.cursor, - op.reset, + osc.color.Request{ .reset = .{ .dynamic = .cursor } }, + op.*, ); } try std.testing.expect(it.next() == null); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 997db0350..72efc949e 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -13,6 +13,7 @@ const Allocator = mem.Allocator; const RGB = @import("color.zig").RGB; const kitty = @import("kitty.zig"); const osc_color = @import("osc/color.zig"); +pub const color = osc_color; const log = std.log.scoped(.osc); @@ -122,10 +123,10 @@ pub const Command = union(enum) { /// /// Currently, these OSCs are handled by `color_operation`: /// - /// 4, 10, 11, 12, 104, 110, 111, 112 + /// 4, 5, 10-19, 104, 105, 110-119 color_operation: struct { - source: ColorOperation.Source, - operations: ColorOperation.List = .{}, + op: osc_color.Operation, + requests: osc_color.List = .{}, terminator: Terminator = .st, }, @@ -171,46 +172,6 @@ pub const Command = union(enum) { /// ConEmu GUI macro (OSC 9;6) conemu_guimacro: []const u8, - 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, 2); - - pub const Kind = union(enum) { - palette: u8, - foreground, - background, - cursor, - }; - - set: struct { - kind: Kind, - color: RGB, - }, - reset: Kind, - report: Kind, - }; - pub const ProgressReport = struct { pub const State = enum(c_int) { remove, @@ -346,6 +307,12 @@ pub const Parser = struct { @"12", @"13", @"133", + @"14", + @"15", + @"16", + @"17", + @"18", + @"19", @"2", @"21", @"22", @@ -371,21 +338,8 @@ pub const Parser = struct { clipboard_kind, clipboard_kind_end, - // Get/set color palette index - osc_4_index, - osc_4_color, - - // Get/set foreground color - osc_10, - - // Get/set background color - osc_11, - - // Get/set cursor color - osc_12, - - // Reset color palette index - osc_104, + // OSC color operation. + osc_color, // Hyperlinks hyperlink_param_key, @@ -492,7 +446,7 @@ pub const Parser = struct { // Some commands have their own memory management we need to clear. switch (self.command) { .kitty_color_protocol => |*v| v.list.deinit(), - .color_operation => |*v| v.operations.deinit(self.alloc.?), + .color_operation => |*v| v.requests.deinit(self.alloc.?), else => {}, } @@ -580,6 +534,12 @@ pub const Parser = struct { '1' => self.state = .@"11", '2' => self.state = .@"12", '3' => self.state = .@"13", + '4' => self.state = .@"14", + '5' => self.state = .@"15", + '6' => self.state = .@"16", + '7' => self.state = .@"17", + '8' => self.state = .@"18", + '9' => self.state = .@"19", else => self.state = .invalid, }, @@ -590,12 +550,10 @@ pub const Parser = struct { self.state = .invalid; break :osc_10; } - self.command = .{ - .color_operation = .{ - .source = .get_set_foreground, - }, - }; - self.state = .osc_10; + self.command = .{ .color_operation = .{ + .op = .osc_10, + } }; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, @@ -603,11 +561,6 @@ pub const Parser = struct { else => self.state = .invalid, }, - .osc_10, .osc_11, .osc_12 => switch (c) { - ';' => self.parseOSC101112(false), - else => {}, - }, - .@"104" => switch (c) { ';' => osc_104: { if (self.alloc == null) { @@ -617,21 +570,16 @@ pub const Parser = struct { } self.command = .{ .color_operation = .{ - .source = .reset_palette, + .op = .osc_104, }, }; - self.state = .osc_104; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, - .osc_104 => switch (c) { - ';' => self.parseOSC104(false), - else => {}, - }, - .@"11" => switch (c) { ';' => osc_11: { if (self.alloc == null) { @@ -639,47 +587,39 @@ pub const Parser = struct { self.state = .invalid; break :osc_11; } - self.command = .{ - .color_operation = .{ - .source = .get_set_background, - }, - }; - self.state = .osc_11; + self.command = .{ .color_operation = .{ + .op = .osc_11, + } }; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, - '0'...'2' => blk: { + '0'...'9' => blk: { if (self.alloc == null) { log.warn("OSC 11{c} requires an allocator, but none was provided", .{c}); self.state = .invalid; break :blk; } - const alloc = self.alloc orelse return; - self.command = .{ .color_operation = .{ - .source = switch (c) { - '0' => .reset_foreground, - '1' => .reset_background, - '2' => .reset_cursor, + .op = switch (c) { + '0' => .osc_110, + '1' => .osc_111, + '2' => .osc_112, + '3' => .osc_113, + '4' => .osc_114, + '5' => .osc_115, + '6' => .osc_116, + '7' => .osc_117, + '8' => .osc_118, + '9' => .osc_119, 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.state = .osc_color; + self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, @@ -692,12 +632,10 @@ pub const Parser = struct { self.state = .invalid; break :osc_12; } - self.command = .{ - .color_operation = .{ - .source = .get_set_cursor, - }, - }; - self.state = .osc_12; + self.command = .{ .color_operation = .{ + .op = .osc_12, + } }; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, @@ -705,6 +643,19 @@ pub const Parser = struct { }, .@"13" => switch (c) { + ';' => osc_13: { + if (self.alloc == null) { + log.warn("OSC 13 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_13; + } + self.command = .{ .color_operation = .{ + .op = .osc_13, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, '3' => self.state = .@"133", else => self.state = .invalid, }, @@ -714,6 +665,110 @@ pub const Parser = struct { else => self.state = .invalid, }, + .@"14" => switch (c) { + ';' => osc_14: { + if (self.alloc == null) { + log.warn("OSC 14 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_14; + } + self.command = .{ .color_operation = .{ + .op = .osc_14, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"15" => switch (c) { + ';' => osc_15: { + if (self.alloc == null) { + log.warn("OSC 15 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_15; + } + self.command = .{ .color_operation = .{ + .op = .osc_15, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"16" => switch (c) { + ';' => osc_16: { + if (self.alloc == null) { + log.warn("OSC 16 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_16; + } + self.command = .{ .color_operation = .{ + .op = .osc_16, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"17" => switch (c) { + ';' => osc_17: { + if (self.alloc == null) { + log.warn("OSC 17 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_17; + } + self.command = .{ .color_operation = .{ + .op = .osc_17, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"18" => switch (c) { + ';' => osc_18: { + if (self.alloc == null) { + log.warn("OSC 18 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_18; + } + self.command = .{ .color_operation = .{ + .op = .osc_18, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .@"19" => switch (c) { + ';' => osc_19: { + if (self.alloc == null) { + log.warn("OSC 19 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_19; + } + self.command = .{ .color_operation = .{ + .op = .osc_19, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, + else => self.state = .invalid, + }, + + .osc_color => {}, + .@"2" => switch (c) { '1' => self.state = .@"21", '2' => self.state = .@"22", @@ -793,30 +848,32 @@ pub const Parser = struct { } self.command = .{ .color_operation = .{ - .source = .get_set_palette, + .op = .osc_4, }, }; - self.state = .osc_4_index; + self.state = .osc_color; self.buf_start = self.buf_idx; self.complete = true; }, else => self.state = .invalid, }, - .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) { + ';' => osc_5: { + if (self.alloc == null) { + log.info("OSC 5 requires an allocator, but none was provided", .{}); + self.state = .invalid; + break :osc_5; + } + self.command = .{ + .color_operation = .{ + .op = .osc_5, + }, + }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.complete = true; + }, '2' => self.state = .@"52", else => self.state = .invalid, }, @@ -1480,178 +1537,28 @@ pub const Parser = struct { } } + fn endOscColor(self: *Parser) void { + const alloc = self.alloc.?; + assert(self.command == .color_operation); + const data = self.buf[self.buf_start..self.buf_idx]; + self.command.color_operation.requests = osc_color.parse( + alloc, + self.command.color_operation.op, + data, + ) catch |err| list: { + log.info( + "failed to parse OSC color request err={} data={s}", + .{ err, data }, + ); + break :list .{}; + }; + } + fn endAllocableString(self: *Parser) void { const list = self.buf_dynamic.?; self.temp_state.str.* = list.items; } - 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; - - 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; - }, - }; - 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, - }, - }; - } - } - - 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; - - 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 = switch (self.state) { - .osc_10 => .foreground, - .osc_11 => .background, - .osc_12 => .cursor, - else => unreachable, - }, - }; - } else { - 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 = 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, - }, - }; - } - } - - 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; - - 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| { - 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 @@ -1667,11 +1574,15 @@ pub const Parser = struct { // Other cleanup we may have to do depending on state. switch (self.state) { + .allocable_string => self.endAllocableString(), .semantic_exit_code => self.endSemanticExitCode(), .semantic_option_value => self.endSemanticOptionValue(), .hyperlink_uri => self.endHyperlink(), .string => self.endString(), .conemu_sleep_value => self.endConEmuSleepValue(), + .kitty_color_protocol_key => self.endKittyColorProtocolOption(.key_only, true), + .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), + .osc_color => self.endOscColor(), // We received OSC 9;X ST, but nothing else, finish off as a // desktop notification with "X" as the body. @@ -1692,12 +1603,6 @@ pub const Parser = struct { .conemu_progress_value, => {}, - .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_color => self.parseOSC4(true), - .osc_10, .osc_11, .osc_12 => self.parseOSC101112(true), - .osc_104 => self.parseOSC104(true), else => {}, } @@ -1916,111 +1821,6 @@ test "OSC: end_of_input" { try testing.expect(cmd == .end_of_input); } -test "OSC: OSC110: reset foreground color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "110"; - 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 == .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.ColorOperation.Kind.foreground, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC111: reset background color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - 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 == .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.ColorOperation.Kind.background, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC112: reset cursor color" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "112"; - 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 == .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.ColorOperation.Kind.cursor, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - -test "OSC: OSC112: reset cursor color with semicolon" { - const testing = std.testing; - - var p: Parser = .initAlloc(testing.allocator); - defer p.deinit(); - - const input = "112;"; - for (input) |ch| 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 == .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.ColorOperation.Kind.cursor, - op.reset, - ); - } - try testing.expect(it.next() == null); -} - test "OSC: get/set clipboard" { const testing = std.testing; diff --git a/src/terminal/osc/color.zig b/src/terminal/osc/color.zig index 4d6c9a0de..8a8e8b942 100644 --- a/src/terminal/osc/color.zig +++ b/src/terminal/osc/color.zig @@ -105,7 +105,7 @@ fn parseGetSetAnsiColor( ) catch return result; // Parse the color. - const target: Request.Target = switch (op) { + const target: Target = switch (op) { // OSC5 maps directly to the Special enum. .osc_5 => .{ .special = std.meta.intToEnum( SpecialColor, @@ -178,7 +178,7 @@ fn parseResetAnsiColor( ) catch continue; // Parse the color. - const target: Request.Target = switch (op) { + const target: Target = switch (op) { // OSC105 maps directly to the Special enum. .osc_105 => .{ .special = std.meta.intToEnum( SpecialColor, @@ -266,17 +266,17 @@ pub const Request = union(enum) { reset: Target, reset_palette, reset_special, +}; - pub const Target = union(enum) { - palette: u8, - special: SpecialColor, - dynamic: DynamicColor, - }; +pub const Target = union(enum) { + palette: u8, + special: SpecialColor, + dynamic: DynamicColor, +}; - pub const ColoredTarget = struct { - target: Target, - color: RGB, - }; +pub const ColoredTarget = struct { + target: Target, + color: RGB, }; test "osc4" { diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index 3009935ec..29d8c42d3 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -1565,7 +1565,11 @@ pub fn Stream(comptime Handler: type) type { .color_operation => |v| { if (@hasDecl(T, "handleColorOperation")) { - try self.handler.handleColorOperation(v.source, &v.operations, v.terminator); + try self.handler.handleColorOperation( + v.op, + &v.requests, + 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 002ccdb39..2d1f3293c 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1187,12 +1187,15 @@ pub const StreamHandler = struct { pub fn handleColorOperation( self: *StreamHandler, - source: terminal.osc.Command.ColorOperation.Source, - operations: *const terminal.osc.Command.ColorOperation.List, + op: terminal.osc.color.Operation, + requests: *const terminal.osc.color.List, terminator: terminal.osc.Terminator, ) !void { + // We'll need op one day if we ever implement reporting special colors. + _ = op; + // return early if there is nothing to do - if (operations.count() == 0) return; + if (requests.count() == 0) return; var buffer: [1024]u8 = undefined; var fba: std.heap.FixedBufferAllocator = .init(&buffer); @@ -1201,63 +1204,71 @@ pub const StreamHandler = struct { var response: std.ArrayListUnmanaged(u8) = .empty; const writer = response.writer(alloc); - var report: bool = false; - - try writer.print("\x1b]{}", .{source}); - - var it = operations.constIterator(0); - - while (it.next()) |op| { - switch (op.*) { + var it = requests.constIterator(0); + while (it.next()) |req| { + switch (req.*) { .set => |set| { - switch (set.kind) { + switch (set.target) { .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 = {} }); + .dynamic => |dynamic| switch (dynamic) { + .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 = {} }); + }, + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => log.info("setting dynamic color {s} not implemented", .{ + @tagName(dynamic), + }), }, + .special => log.info("setting special colors not implemented", .{}), } // Notify the surface of the color change self.surfaceMessageWriter(.{ .color_change = .{ - .kind = set.kind, + .target = set.target, .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); + .reset => |target| switch (target) { + .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], - }, - }); - }, + self.surfaceMessageWriter(.{ + .color_change = .{ + .target = target, + .color = self.terminal.color_palette.colors[i], + }, + }); + }, + .dynamic => |dynamic| switch (dynamic) { .foreground => { self.foreground_color = null; _ = self.renderer_mailbox.push(.{ @@ -1265,7 +1276,7 @@ pub const StreamHandler = struct { }, .{ .forever = {} }); self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .foreground, + .target = target, .color = self.default_foreground_color, } }); }, @@ -1276,7 +1287,7 @@ pub const StreamHandler = struct { }, .{ .forever = {} }); self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .background, + .target = target, .color = self.default_background_color, } }); }, @@ -1289,33 +1300,83 @@ pub const StreamHandler = struct { if (self.default_cursor_color) |color| { self.surfaceMessageWriter(.{ .color_change = .{ - .kind = .cursor, + .target = target, .color = color, } }); } }, - } + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => log.warn("resetting dynamic color {s} not implemented", .{ + @tagName(dynamic), + }), + }, + .special => log.info("resetting special colors not implemented", .{}), }, - .report => |kind| report: { - if (self.osc_color_report_format == .none) break :report; + .reset_palette => { + const mask = &self.terminal.color_palette.mask; + var mask_iterator = mask.iterator(.{}); + while (mask_iterator.next()) |i| { + self.terminal.flags.dirty.palette = true; + self.terminal.color_palette.colors[i] = self.terminal.default_palette[i]; + self.surfaceMessageWriter(.{ + .color_change = .{ + .target = .{ .palette = @intCast(i) }, + .color = self.terminal.color_palette.colors[i], + }, + }); + } + mask.* = .initEmpty(); + }, - report = true; + .reset_special => log.warn( + "resetting all special colors not implemented", + .{}, + ), + + .query => |kind| report: { + if (self.osc_color_report_format == .none) break :report; 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, + .dynamic => |dynamic| switch (dynamic) { + .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, + .pointer_foreground, + .pointer_background, + .tektronix_foreground, + .tektronix_background, + .highlight_background, + .tektronix_cursor, + .highlight_foreground, + => { + log.info( + "reporting dynamic color {s} not implemented", + .{@tagName(dynamic)}, + ); + break :report; + }, + }, + .special => { + log.info("reporting special colors not implemented", .{}); + break :report; + }, }; 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}", + "\x1b]4;{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", .{ i, @as(u16, color.r) * 257, @@ -1323,19 +1384,21 @@ pub const StreamHandler = struct { @as(u16, color.b) * 257, }, ), - else => try writer.print( - ";rgb:{x:0>4}/{x:0>4}/{x:0>4}", + .dynamic => |dynamic| try writer.print( + "\x1b]{d};rgb:{x:0>4}/{x:0>4}/{x:0>4}", .{ + @intFromEnum(dynamic), @as(u16, color.r) * 257, @as(u16, color.g) * 257, @as(u16, color.b) * 257, }, ), + .special => unreachable, }, .@"8-bit" => switch (kind) { .palette => |i| try writer.print( - ";{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", + "\x1b]4;{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", .{ i, @as(u16, color.r), @@ -1343,22 +1406,27 @@ pub const StreamHandler = struct { @as(u16, color.b), }, ), - else => try writer.print( - ";rgb:{x:0>2}/{x:0>2}/{x:0>2}", + .dynamic => |dynamic| try writer.print( + "\x1b]{d};rgb:{x:0>2}/{x:0>2}/{x:0>2}", .{ + @intFromEnum(dynamic), @as(u16, color.r), @as(u16, color.g), @as(u16, color.b), }, ), + .special => unreachable, }, .none => unreachable, } + + try writer.writeAll(terminator.string()); }, } } - if (report) { + + if (response.items.len > 0) { // If any of the operations were reports, finalize the report // string and send it to the terminal. try writer.writeAll(terminator.string()); From c9574ed0c79c4fff86d5f37ee0291ff3d635cd33 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 14:13:07 -0700 Subject: [PATCH 18/49] macOS: grab text field focus of command palette after tick Fixes #8497 This works on every other supported version of macOS but doesn't work on macOS tahoe. Putting it on the next event loop tick works at least on Sequoia and Tahoe so let's just do that. --- .../Sources/Features/Command Palette/CommandPalette.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Command Palette/CommandPalette.swift b/macos/Sources/Features/Command Palette/CommandPalette.swift index 3e5a3a36f..8d15cbf9a 100644 --- a/macos/Sources/Features/Command Palette/CommandPalette.swift +++ b/macos/Sources/Features/Command Palette/CommandPalette.swift @@ -165,7 +165,12 @@ fileprivate struct CommandPaletteQuery: View { .textFieldStyle(.plain) .focused($isTextFieldFocused) .onAppear { - isTextFieldFocused = true + // We want to grab focus on appearance. We have to do this after a tick + // on macOS Tahoe otherwise this doesn't work. See: + // https://github.com/ghostty-org/ghostty/issues/8497 + DispatchQueue.main.async { + isTextFieldFocused = true + } } .onChange(of: isTextFieldFocused) { focused in if !focused { From 22ec755e7519b8c5e26b798523b62786a6577da3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 14:39:04 -0700 Subject: [PATCH 19/49] macos: run change title dialog in a sheet modal This fixes a macOS 26 issue where the OK button would not be visible. This MUST be an AppKit bug, but I'm trying to find workarounds. --- .../Sources/Ghostty/SurfaceView_AppKit.swift | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index 1c5c8eb6a..03c113209 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -509,13 +509,14 @@ extension Ghostty { // Make the text field the first responder so it gets focus alert.window.initialFirstResponder = textField - let response = alert.runModal() - - // Check if the user clicked "OK" - if response == .alertFirstButtonReturn { + let completionHandler: (NSApplication.ModalResponse) -> Void = { [weak self] response in + guard let self else { return } + + // Check if the user clicked "OK" + guard response == .alertFirstButtonReturn else { return } + // Get the input text let newTitle = textField.stringValue - if newTitle.isEmpty { // Empty means that user wants the title to be set automatically // We also need to reload the config for the "title" property to be @@ -529,6 +530,16 @@ extension Ghostty { title = newTitle } } + + // We prefer to run our alert in a sheet modal if we have a window. + if let window { + alert.beginSheetModal(for: window, completionHandler: completionHandler) + } else { + // On macOS 26 RC, this codepath results in the "OK" button not being + // visible. The above codepath should be taken most times but I'm just + // noting this as something I noticed consistently. + completionHandler(alert.runModal()) + } } func setTitle(_ title: String) { From 09246780bb63ef0b2e1040d9fabf7093b774d451 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 11 Sep 2025 14:49:02 -0700 Subject: [PATCH 20/49] macos 26: fix visual glitches with moving tabs and titlebar tabs This is a hacky fix to fix some visual glitches when titlebar tabs is on and we're using the `move_tab` keybinding action (I test via the command palette). There is probably a more graceful way to fix this but this might be good enough for a 1.2 to fix a very obviously nasty UI render. --- .../Features/Terminal/TerminalController.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index bdf3abeb6..4bb642ea6 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -1219,6 +1219,23 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr // Get our target window let targetWindow = tabbedWindows[finalIndex] + + // Moving tabs on macOS 26 RC causes very nasty visual glitches in the titlebar tabs. + // I believe this is due to messed up constraints for our hacky tab bar. I'd like to + // find a better workaround. For now, this improves things dramatically. + // + // Reproduction: titlebar tabs, create two tabs, "move tab left" + if #available(macOS 26, *) { + if window is TitlebarTabsTahoeTerminalWindow { + tabGroup.removeWindow(selectedWindow) + targetWindow.addTabbedWindow(selectedWindow, ordered: action.amount < 0 ? .below : .above) + DispatchQueue.main.async { + selectedWindow.makeKey() + } + + return + } + } // Begin a group of window operations to minimize visual updates NSAnimationContext.beginGrouping() From dc7bf7e71f8c073908024393e310bd8f53eab9a3 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Sep 2025 07:29:41 -0700 Subject: [PATCH 21/49] ci: disable trigger-snap for PRs Fixes #8568 This will hide snap issues from PRs which is not ideal but we can address that in the future. We still run snap CI for main. --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9781e357d..3c3be31c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -273,6 +273,7 @@ jobs: ghostty-source.tar.gz trigger-snap: + if: github.event_name != 'pull_request' runs-on: namespace-profile-ghostty-xsm needs: build-dist steps: From ac07cf5270fd77cbcfe92dbc44454b2d0d139d32 Mon Sep 17 00:00:00 2001 From: azhn Date: Sat, 13 Sep 2025 06:52:44 +1000 Subject: [PATCH 22/49] Use ghostty-specific themes release --- build.zig.zon | 4 ++-- src/build/GhosttyResources.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 78097a180..21855790a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -112,8 +112,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz", - .hash = "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz", + .hash = "N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv", .lazy = true, }, }, diff --git a/src/build/GhosttyResources.zig b/src/build/GhosttyResources.zig index 10de2584b..266069f83 100644 --- a/src/build/GhosttyResources.zig +++ b/src/build/GhosttyResources.zig @@ -120,7 +120,7 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyResources { // Themes if (b.lazyDependency("iterm2_themes", .{})) |upstream| { const install_step = b.addInstallDirectory(.{ - .source_dir = upstream.path("ghostty"), + .source_dir = upstream.path(""), .install_dir = .{ .custom = "share" }, .install_subdir = b.pathJoin(&.{ "ghostty", "themes" }), .exclude_extensions = &.{".md"}, From e0350aa13f104ec19a423cc0041b58ca6dcb2153 Mon Sep 17 00:00:00 2001 From: azhn Date: Sat, 13 Sep 2025 06:53:18 +1000 Subject: [PATCH 23/49] nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json --- build.zig.zon.json | 6 +++--- build.zig.zon.nix | 6 +++--- build.zig.zon.txt | 2 +- flatpak/zig-packages.json | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/build.zig.zon.json b/build.zig.zon.json index 32d243d9a..0955f732a 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b": { + "N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz", - "hash": "sha256-3vPlDDjv6BCLyro1YytzPtF0FfBH20skYuA9laDWhac=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz", + "hash": "sha256-ZGDpBXk3zGbHa4UNldAphybKcWz6eSaewZaJVAdPIh0=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index a4630688f..586e41290 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b"; + name = "N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz"; - hash = "sha256-3vPlDDjv6BCLyro1YytzPtF0FfBH20skYuA9laDWhac="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz"; + hash = "sha256-ZGDpBXk3zGbHa4UNldAphybKcWz6eSaewZaJVAdPIh0="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 0b244e6c5..b8e7ffd81 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 51c28b2a9..acf0e69cf 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b314fc540434cc037c2811fc048d32854b5b78c3.tar.gz", - "dest": "vendor/p/N-V-__8AAGupuwFrRxb2dkqFqmEChLEa4J3e95GReqvomV1b", - "sha256": "def3e50c38efe8108bcaba35632b733ed17415f047db4b2462e03d95a0d685a7" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv", + "sha256": "6460e9057937cc66c76b850d95d0298726ca716cfa79269ec1968954074f221d" }, { "type": "archive", From 928f5492dc6cda23466f4f91f776fe5509c1b81c Mon Sep 17 00:00:00 2001 From: azhn Date: Sat, 13 Sep 2025 06:54:41 +1000 Subject: [PATCH 24/49] Fix name of `check-zig-cache.sh` in documentation for updating the zig cache hash --- HACKING.md | 2 +- nix/build-support/check-zig-cache.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/HACKING.md b/HACKING.md index 2d3640fca..905a244e8 100644 --- a/HACKING.md +++ b/HACKING.md @@ -150,7 +150,7 @@ hash in CI, and builds will fail if it drifts. To update it, you can run the following in the repository root: ``` -./nix/build-support/check-zig-cache-hash.sh --update +./nix/build-support/check-zig-cache.sh --update ``` This will write out the `nix/zigCacheHash.nix` file with the updated hash diff --git a/nix/build-support/check-zig-cache.sh b/nix/build-support/check-zig-cache.sh index 33e57e790..d6a9fdc30 100755 --- a/nix/build-support/check-zig-cache.sh +++ b/nix/build-support/check-zig-cache.sh @@ -34,7 +34,7 @@ help() { echo "To fix, please (manually) re-run the script from the repository root," echo "commit, and submit a PR with the update:" echo "" - echo " ./nix/build-support/check-zig-cache-hash.sh --update" + echo " ./nix/build-support/check-zig-cache.sh --update" echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json" echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json flatpak/zig-packages.json\"" echo "" From ffa54dceea635492ea7f3c25daf3c8a99f2f957e Mon Sep 17 00:00:00 2001 From: azhn Date: Sat, 13 Sep 2025 07:17:00 +1000 Subject: [PATCH 25/49] Update ` .github/workflows/update-colorschemes.yml` to track the latest release and update the ghostty-specific download --- .github/workflows/update-colorschemes.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update-colorschemes.yml b/.github/workflows/update-colorschemes.yml index 6c3a87659..569ad83a5 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -40,9 +40,11 @@ jobs: - name: Run zig fetch id: zig_fetch run: | - UPSTREAM_REV="$(curl "https://api.github.com/repos/mbadolato/iTerm2-Color-Schemes/commits/master" | jq -r '.sha')" - nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/$UPSTREAM_REV.tar.gz" - echo "upstream_rev=$UPSTREAM_REV" >> "$GITHUB_OUTPUT" + # Get the latest release from iTerm2-Color-Schemes + RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest) + TAG_NAME=$(echo "$RELEASE_INFO" | jq -r '.tag_name') + nix develop -c zig fetch --save="iterm2_themes" "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/${TAG_NAME}/ghostty-themes.tgz" + echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT - name: Update zig cache hash run: | @@ -71,5 +73,5 @@ jobs: 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 }} + Upstream release: https://github.com/mbadolato/iTerm2-Color-Schemes/releases/tag/${{ steps.zig_fetch.outputs.tag_name }} labels: dependencies From eaaf5aa8cf268105ffca99d4fc3435854657adc7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Sep 2025 20:14:26 -0700 Subject: [PATCH 26/49] macos: always reset titlebar tab constraints on frame change Fixes #8595 Whenever the titlebar frame changes, we should set up our constraints again to force it to re-render properly. --- .../Window Styles/TitlebarTabsTahoeTerminalWindow.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 9381f7329..260fac4cc 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -156,7 +156,7 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool accessoryView.needsLayout = true // 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 + // other events occur, the tab bar can resize and clear our constraints. 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 @@ -167,9 +167,6 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool ) { [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 From d813d82da1b8b018e17a2b0cc0882acf2e2b0280 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Sep 2025 20:40:45 -0700 Subject: [PATCH 27/49] macOS 26: Always set titlebarview background color for transparent title This fixes an issue where new tabs would not have the proper transparent background set whilst in native fullscreen. This is because in native fullscreen, the NSTitlebarView always is visible, so our guard was preventing us from setting it before. --- .../TransparentTitlebarTerminalWindow.swift | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift index f6ad6e56c..7ae628341 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TransparentTitlebarTerminalWindow.swift @@ -80,16 +80,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow { // 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 - } + // Even if we aren't transparent, we still set this because this becomes the + // color of the titlebar in native fullscreen view. + 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 From a19aa0a398f2dcd0494482981f604d8113cd7f27 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Sep 2025 21:08:52 -0700 Subject: [PATCH 28/49] terminal: OSC 104 with no semicolon should parse as reset palette https://github.com/ghostty-org/ghostty/pull/8590#issuecomment-3287418867 --- src/terminal/Parser.zig | 32 ++++++++++++++++++++++++++++++++ src/terminal/osc.zig | 16 +++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/terminal/Parser.zig b/src/terminal/Parser.zig index 0c814ff68..1f2e814f6 100644 --- a/src/terminal/Parser.zig +++ b/src/terminal/Parser.zig @@ -930,6 +930,38 @@ test "osc: 112 incomplete sequence" { } } +test "osc: 104 empty" { + var p: Parser = init(); + defer p.deinit(); + p.osc_parser.alloc = std.testing.allocator; + + _ = p.next(0x1B); + _ = p.next(']'); + _ = p.next('1'); + _ = p.next('0'); + _ = p.next('4'); + + { + const a = p.next(0x07); + try testing.expect(p.state == .ground); + try testing.expect(a[0].? == .osc_dispatch); + try testing.expect(a[1] == null); + try testing.expect(a[2] == null); + + 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.op == .osc_104); + try testing.expect(cmd.color_operation.requests.count() == 1); + var it = cmd.color_operation.requests.constIterator(0); + { + const op = it.next().?; + try testing.expect(op.* == .reset_palette); + } + try std.testing.expect(it.next() == null); + } +} + test "csi: too many params" { var p = init(); _ = p.next(0x1B); diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 72efc949e..028fcdf0a 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -557,7 +557,11 @@ pub const Parser = struct { self.buf_start = self.buf_idx; self.complete = true; }, - '4' => self.state = .@"104", + '4' => { + self.state = .@"104"; + // If we have an allocator, then we can complete the OSC104 + if (self.alloc != null) self.complete = true; + }, else => self.state = .invalid, }, @@ -1584,6 +1588,16 @@ pub const Parser = struct { .kitty_color_protocol_value => self.endKittyColorProtocolOption(.key_and_value, true), .osc_color => self.endOscColor(), + // 104 abruptly ended turns into a reset palette command. + .@"104" => { + self.command = .{ .color_operation = .{ + .op = .osc_104, + } }; + self.state = .osc_color; + self.buf_start = self.buf_idx; + self.endOscColor(); + }, + // We received OSC 9;X ST, but nothing else, finish off as a // desktop notification with "X" as the body. .conemu_sleep, From d7c2a829bbd31803b661a2b54e9e38d5b92c69db Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 12 Sep 2025 21:19:06 -0700 Subject: [PATCH 29/49] termio: don't send extra OSC terminator for color reports Fixes #8613 I reiterate my comment in my own PR that this needs to be extracted so we can unit test this. :) --- src/termio/stream_handler.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2d1f3293c..f9bc03500 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1429,7 +1429,6 @@ pub const StreamHandler = struct { if (response.items.len > 0) { // 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); self.messageWriter(msg); } From ab5cd0b7090dede5d76cf3c9914d6c53cafe9801 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Sep 2025 07:07:21 -0700 Subject: [PATCH 30/49] ci: update-colorschemes sets GH token for gh --- .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 569ad83a5..119f96563 100644 --- a/.github/workflows/update-colorschemes.yml +++ b/.github/workflows/update-colorschemes.yml @@ -39,6 +39,8 @@ jobs: - name: Run zig fetch id: zig_fetch + env: + GH_TOKEN: ${{ github.token }} run: | # Get the latest release from iTerm2-Color-Schemes RELEASE_INFO=$(gh api repos/mbadolato/iTerm2-Color-Schemes/releases/latest) From 431364cf16a7619de7b6d193d28eda9605ebff7b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 14 Sep 2025 13:04:55 -0700 Subject: [PATCH 31/49] macos: disable NSAutoFillHeuristicController on macOS 26 Fixes #8616 macOS 26 (as of RC1) has some pathological performance bug where the terminal becomes unusably slow after some period of time. We aren't 100% sure what triggers the slowdown, but it is app-wide (new tabs or windows don't resolve it) and Instruments traces point directly to NSAutoFillHeuristicController. Specifically, to the `debounceTextUpdate` selector. This is all not documented as far as I can find and also not open source, so I have no idea what's going on. The best I can tell is that the NSAutoFillHeuristicController has something to do with enabling heuristic-based autofill such as SMS auth codes in text input fields. I don't know what is causing it to go haywire. SMS autofill is not desirable in a terminal app, nor is any of the other automatic autofill in macOS I know of (contact info, passwords, etc.). So, we can just disable it. This default isn't documented but I found it via a strings dump of the AppKit binary blob and comparing it to the disassembly to see how it is used. In my limited testing, this seems to work around the problem. --- macos/Sources/App/macOS/AppDelegate.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 558658b89..df3a1f4f4 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -147,6 +147,16 @@ class AppDelegate: NSObject, // Disable the automatic full screen menu item because we handle // it manually. "NSFullScreenMenuItemEverywhere": false, + + // On macOS 26 RC1, the autofill heuristic controller causes unusable levels + // of slowdowns and CPU usage in the terminal window under certain [unknown] + // conditions. We don't know exactly why/how. This disables the full heuristic + // controller. + // + // Practically, this means things like SMS autofill don't work, but that is + // a desirable behavior to NOT have happen for a terminal, so this is a win. + // Manual autofill via the `Edit => AutoFill` menu item still work as expected. + "NSAutoFillHeuristicControllerEnabled": false, ]) } From 510f4e474cb68255b8831cad2aab1a83dc1bdd01 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 08:25:48 -0700 Subject: [PATCH 32/49] Set version to 1.2.0 --- build.zig.zon | 2 +- nix/package.nix | 2 +- src/build/Config.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 21855790a..fbfd2282d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ghostty, - .version = "1.1.4", + .version = "1.2.0", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, .dependencies = .{ diff --git a/nix/package.nix b/nix/package.nix index 08dfd710b..efe32c644 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -40,7 +40,7 @@ in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; - version = "1.1.4"; + version = "1.2.0"; # We limit source like this to try and reduce the amount of rebuilds as possible # thus we only provide the source that is needed for the build diff --git a/src/build/Config.zig b/src/build/Config.zig index b11e8850d..b4759b45c 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -20,7 +20,7 @@ const GitVersion = @import("GitVersion.zig"); /// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. /// Until then this MUST match build.zig.zon and should always be the /// _next_ version to release. -const app_version: std.SemanticVersion = .{ .major = 1, .minor = 1, .patch = 4 }; +const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 0 }; /// Standard build configuration options. optimize: std.builtin.OptimizeMode, From c195cb054d48ef66e9c951a505a6329725e48f8c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 08:32:24 -0700 Subject: [PATCH 33/49] ci: update tag release to Xcode 26, sparkle to 2.7.3 --- .github/workflows/release-tag.yml | 8 ++++---- .github/workflows/release-tip.yml | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index 3c5b8702b..eb6a6bbb5 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -39,7 +39,7 @@ jobs: echo "Version is valid: ${{ github.event.inputs.version }}" - - name: Exract the Version + - name: Extract the Version id: extract_version run: | if [[ "${{ github.event_name }}" == "push" ]]; then @@ -139,14 +139,14 @@ 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: Xcode Version run: xcodebuild -version - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.1 + SPARKLE_VERSION: 2.7.3 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -311,7 +311,7 @@ jobs: - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.1 + SPARKLE_VERSION: 2.7.3 run: | mkdir -p .action/sparkle cd .action/sparkle diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 051fe4ca2..015e42c89 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -243,7 +243,7 @@ jobs: # Setup Sparkle - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.1 + SPARKLE_VERSION: 2.7.3 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -481,7 +481,7 @@ jobs: # Setup Sparkle - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.1 + SPARKLE_VERSION: 2.7.3 run: | mkdir -p .action/sparkle cd .action/sparkle @@ -666,7 +666,7 @@ jobs: # Setup Sparkle - name: Setup Sparkle env: - SPARKLE_VERSION: 2.7.1 + SPARKLE_VERSION: 2.7.3 run: | mkdir -p .action/sparkle cd .action/sparkle From 895fb5f5dbb6dc41854143839a6cc1bd44ea449b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 08:49:45 -0700 Subject: [PATCH 34/49] ci: release tip should also staple the dmg --- .github/workflows/release-tip.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index 015e42c89..ec4ef4220 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -579,6 +579,7 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" + xcrun stapler staple "Ghostty.dmg" xcrun stapler staple "macos/build/Release/Ghostty.app" # Zip up the app From 5e52659d51b0bdcce158402c2d1e9efb2ac99872 Mon Sep 17 00:00:00 2001 From: mitchellh <1299+mitchellh@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:57:37 +0000 Subject: [PATCH 35/49] 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 fbfd2282d..9d0e21dca 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -112,8 +112,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz", - .hash = "N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv", + .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz", + .hash = "N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index 0955f732a..c3b71a9d2 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv": { + "N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz", - "hash": "sha256-ZGDpBXk3zGbHa4UNldAphybKcWz6eSaewZaJVAdPIh0=" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz", + "hash": "sha256-/6wSC8KIO1tJVxIXpIH1wiQKJazrx+b8RBt5tEYQPXU=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 586e41290..92db0e8f5 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv"; + name = "N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz"; - hash = "sha256-ZGDpBXk3zGbHa4UNldAphybKcWz6eSaewZaJVAdPIh0="; + url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz"; + hash = "sha256-/6wSC8KIO1tJVxIXpIH1wiQKJazrx+b8RBt5tEYQPXU="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index b8e7ffd81..2790019f8 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -28,7 +28,7 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz +https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index acf0e69cf..9a7a1e525 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250912-175234-6e04af3/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AALEfAwCyd2u6A3yOkdQx0rzl5druY97Lyl0bbTsv", - "sha256": "6460e9057937cc66c76b850d95d0298726ca716cfa79269ec1968954074f221d" + "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz", + "dest": "vendor/p/N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB", + "sha256": "ffac120bc2883b5b49571217a481f5c2240a25acebc7e6fc441b79b446103d75" }, { "type": "archive", From 6634afc3586561580fafbaf25762079ecf95570c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 09:12:30 -0700 Subject: [PATCH 36/49] ci: fix source tarball copy for release-tag --- .github/workflows/release-tag.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index eb6a6bbb5..d8d21c826 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -100,8 +100,10 @@ jobs: - name: Create Tarball run: | + rm -rf zig-out/dist nix develop -c zig build distcheck cp zig-out/dist/ghostty-${GHOSTTY_VERSION}.tar.gz . + cp zig-out/dist/ghostty-${GHOSTTY_VERSION}.tar.gz ghostty-source.tar.gz - name: Sign Tarball run: | From d4e62cb6d984262818bc4bc1660f3042dec60b85 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 09:24:46 -0700 Subject: [PATCH 37/49] ci: source tarball needs to be uploaded to staged artifacts --- .github/workflows/release-tag.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index d8d21c826..89b4c2f49 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -119,6 +119,8 @@ jobs: path: |- ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz.minisig + ghostty-source.tar.gz + ghostty-source.tar.gz.minisig build-macos: needs: [setup] From 693d64b5f83779de995cfda22049c84ae8d3b8e2 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 09:31:03 -0700 Subject: [PATCH 38/49] pin iTerm2 themes dependency There is an upstream bug where subsequent releases will nuke prior released packages, resulting in 404s. Not good! --- 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 9d0e21dca..105543e58 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -112,8 +112,8 @@ // Other .apple_sdk = .{ .path = "./pkg/apple-sdk" }, .iterm2_themes = .{ - .url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz", - .hash = "N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB", + .url = "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz", + .hash = "N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B", .lazy = true, }, }, diff --git a/build.zig.zon.json b/build.zig.zon.json index c3b71a9d2..bd4a9fe5e 100644 --- a/build.zig.zon.json +++ b/build.zig.zon.json @@ -49,10 +49,10 @@ "url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz", "hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA=" }, - "N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB": { + "N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B": { "name": "iterm2_themes", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz", - "hash": "sha256-/6wSC8KIO1tJVxIXpIH1wiQKJazrx+b8RBt5tEYQPXU=" + "url": "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz", + "hash": "sha256-6rKNFpaUvSbvNZ0/+u0h4I/RRaV5V7xIPQ9y7eNVbCA=" }, "N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": { "name": "jetbrains_mono", diff --git a/build.zig.zon.nix b/build.zig.zon.nix index 92db0e8f5..30e84f46f 100644 --- a/build.zig.zon.nix +++ b/build.zig.zon.nix @@ -163,11 +163,11 @@ in }; } { - name = "N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB"; + name = "N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B"; path = fetchZigArtifact { name = "iterm2_themes"; - url = "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz"; - hash = "sha256-/6wSC8KIO1tJVxIXpIH1wiQKJazrx+b8RBt5tEYQPXU="; + url = "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz"; + hash = "sha256-6rKNFpaUvSbvNZ0/+u0h4I/RRaV5V7xIPQ9y7eNVbCA="; }; } { diff --git a/build.zig.zon.txt b/build.zig.zon.txt index 2790019f8..3b3317ab1 100644 --- a/build.zig.zon.txt +++ b/build.zig.zon.txt @@ -8,6 +8,7 @@ https://deps.files.ghostty.org/breakpad-b99f444ba5f6b98cac261cbb391d8766b34a5918 https://deps.files.ghostty.org/fontconfig-2.14.2.tar.gz https://deps.files.ghostty.org/freetype-1220b81f6ecfb3fd222f76cf9106fecfa6554ab07ec7fdc4124b9bb063ae2adf969d.tar.gz https://deps.files.ghostty.org/gettext-0.24.tar.gz +https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz https://deps.files.ghostty.org/gtk4-layer-shell-1.1.0.tar.gz https://deps.files.ghostty.org/harfbuzz-11.0.0.tar.xz @@ -28,7 +29,6 @@ https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21a https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst -https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz diff --git a/flatpak/zig-packages.json b/flatpak/zig-packages.json index 9a7a1e525..0b5e95cce 100644 --- a/flatpak/zig-packages.json +++ b/flatpak/zig-packages.json @@ -61,9 +61,9 @@ }, { "type": "archive", - "url": "https://github.com/mbadolato/iTerm2-Color-Schemes/releases/download/release-20250915-154825-b4500fc/ghostty-themes.tgz", - "dest": "vendor/p/N-V-__8AANodAwCEvYCmi9reftwnr5UhMTCWm1aFAfhImHqB", - "sha256": "ffac120bc2883b5b49571217a481f5c2240a25acebc7e6fc441b79b446103d75" + "url": "https://deps.files.ghostty.org/ghostty-themes-20250915-162204-b1fe546.tgz", + "dest": "vendor/p/N-V-__8AANodAwDnyHwhlOv5cVRn2rx_dTvija-wy5YtTw1B", + "sha256": "eab28d169694bd26ef359d3ffaed21e08fd145a57957bc483d0f72ede3556c20" }, { "type": "archive", From 1201bc27d288db6ffae007ca5b0693f3cd77873e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 15 Sep 2025 11:01:07 -0700 Subject: [PATCH 39/49] bump the version for development --- build.zig.zon | 2 +- nix/package.nix | 2 +- src/build/Config.zig | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index 105543e58..efbe367d1 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,6 +1,6 @@ .{ .name = .ghostty, - .version = "1.2.0", + .version = "1.2.1", .paths = .{""}, .fingerprint = 0x64407a2a0b4147e5, .dependencies = .{ diff --git a/nix/package.nix b/nix/package.nix index efe32c644..fcc80b9dc 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -40,7 +40,7 @@ in stdenv.mkDerivation (finalAttrs: { pname = "ghostty"; - version = "1.2.0"; + version = "1.2.1"; # We limit source like this to try and reduce the amount of rebuilds as possible # thus we only provide the source that is needed for the build diff --git a/src/build/Config.zig b/src/build/Config.zig index b4759b45c..296a6a3b4 100644 --- a/src/build/Config.zig +++ b/src/build/Config.zig @@ -20,7 +20,7 @@ const GitVersion = @import("GitVersion.zig"); /// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly. /// Until then this MUST match build.zig.zon and should always be the /// _next_ version to release. -const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 0 }; +const app_version: std.SemanticVersion = .{ .major = 1, .minor = 2, .patch = 1 }; /// Standard build configuration options. optimize: std.builtin.OptimizeMode, From ac9f3b88aaea5afa398076e72282ad45aa9df456 Mon Sep 17 00:00:00 2001 From: Caleb Hearth Date: Mon, 15 Sep 2025 13:16:18 -0600 Subject: [PATCH 40/49] Pass config to splits in NewTerminalConfig Config contains the command, working directory, and environment variables intended to be passed to the new split, but it looks like we forgot to include it as an argument in this branch. Discussion: https://github.com/ghostty-org/ghostty/discussions/8637 --- macos/Sources/Features/App Intents/NewTerminalIntent.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/App Intents/NewTerminalIntent.swift b/macos/Sources/Features/App Intents/NewTerminalIntent.swift index 9b95208bb..f7242ee56 100644 --- a/macos/Sources/Features/App Intents/NewTerminalIntent.swift +++ b/macos/Sources/Features/App Intents/NewTerminalIntent.swift @@ -123,7 +123,8 @@ struct NewTerminalIntent: AppIntent { if let view = controller.newSplit( at: parent, - direction: location.splitDirection! + direction: location.splitDirection!, + baseConfig: config ) { return .result(value: TerminalEntity(view)) } From c6143a1539f48d8612e492ebeace62e4ea12d796 Mon Sep 17 00:00:00 2001 From: dmunozv04 <39565245+dmunozv04@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:55:01 +0200 Subject: [PATCH 41/49] Docs: add undo-timeout configuration setting name --- 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 02feeaa99..b0e4e918d 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -698,7 +698,7 @@ pub const Action = union(enum) { /// 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`. + /// amount is configured with the `undo-timeout` configuration settings. /// /// The undo/redo actions being limited ensures that there is /// bounded memory usage over time, closed surfaces don't continue running From 1397c762434ea9a0852a1bbcc046360658b79321 Mon Sep 17 00:00:00 2001 From: rhodes-b <59537185+rhodes-b@users.noreply.github.com> Date: Mon, 15 Sep 2025 21:49:48 -0500 Subject: [PATCH 42/49] mark ssh shell-integration wrapper as a function this matches other features + fixes a case where users alias to some other command --- src/shell-integration/bash/ghostty.bash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shell-integration/bash/ghostty.bash b/src/shell-integration/bash/ghostty.bash index ca5a012c6..2cf9d388f 100644 --- a/src/shell-integration/bash/ghostty.bash +++ b/src/shell-integration/bash/ghostty.bash @@ -103,7 +103,7 @@ fi # SSH Integration if [[ "$GHOSTTY_SHELL_FEATURES" == *ssh-* ]]; then - ssh() { + function ssh() { builtin local ssh_term ssh_opts ssh_term="xterm-256color" ssh_opts=() From 79f8ea07a173ae17aba1e59ff677eb8889560967 Mon Sep 17 00:00:00 2001 From: Nilton Perim Neto Date: Tue, 16 Sep 2025 01:13:12 -0300 Subject: [PATCH 43/49] Some portuguese translation updates (#8633) Added some prepositions not previously added and changed a word to be more accurate to the portuguese meaning --------- Signed-off-by: Nilton Perim Neto --- po/pt_BR.UTF-8.po | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/po/pt_BR.UTF-8.po b/po/pt_BR.UTF-8.po index 1b7162f90..ae2d80768 100644 --- a/po/pt_BR.UTF-8.po +++ b/po/pt_BR.UTF-8.po @@ -4,14 +4,15 @@ # This file is distributed under the same license as the com.mitchellh.ghostty package. # Gustavo Peres , 2025. # Guilherme Tiscoski , 2025. +# Nilton Perim Neto , 2025. # msgid "" msgstr "" "Project-Id-Version: com.mitchellh.ghostty\n" "Report-Msgid-Bugs-To: m@mitchellh.com\n" "POT-Creation-Date: 2025-07-22 17:18+0000\n" -"PO-Revision-Date: 2025-08-25 11:46-0500\n" -"Last-Translator: Guilherme Tiscoski \n" +"PO-Revision-Date: 2025-09-15 13:57-0300\n" +"Last-Translator: Nilton Perim Neto \n" "Language-Team: Brazilian Portuguese \n" "Language: pt_BR\n" @@ -26,7 +27,7 @@ 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." +msgstr "Deixe em branco para restaurar o título padrão." #: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9 #: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10 @@ -315,8 +316,8 @@ msgstr "Configuração recarregada" #: src/apprt/gtk/Window.zig:1019 msgid "Ghostty Developers" -msgstr "Desenvolvedores Ghostty" +msgstr "Desenvolvedores do Ghostty" #: src/apprt/gtk/inspector.zig:144 msgid "Ghostty: Terminal Inspector" -msgstr "Ghostty: Inspetor de terminal" +msgstr "Ghostty: Inspetor do terminal" From a92a237b8002a47524c6c3490b2f92adc54c79bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=AB=E3=83=AF=E3=83=AA=E3=83=9F=E4=BA=BA=E5=BD=A2?= Date: Tue, 16 Sep 2025 16:13:56 +0900 Subject: [PATCH 44/49] docs: add lacking version information `quick-terminal-size` option is available since 1.2.0 --- src/config/Config.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index e76885891..84c239321 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -2146,6 +2146,8 @@ keybind: Keybinds = .{}, /// from the first by a comma (`,`). Percentage and pixel sizes can be mixed /// together: for instance, a size of `50%,500px` for a top-positioned quick /// terminal would be half a screen tall, and 500 pixels wide. +/// +/// Available since: 1.2.0 @"quick-terminal-size": QuickTerminalSize = .{}, /// The layer of the quick terminal window. The higher the layer, From fd03a146ba0e57ec7a7f17388922a839d5a86882 Mon Sep 17 00:00:00 2001 From: Simon Olofsson Date: Tue, 16 Sep 2025 10:37:24 +0200 Subject: [PATCH 45/49] config: update theme names in docs They were renamed, see: https://github.com/mbadolato/iTerm2-Color-Schemes/commits/master/ghostty/Rose%20Pine --- 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 e76885891..ac7047a49 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -515,7 +515,7 @@ pub const compatibility = std.StaticStringMap( /// /// To specify a different theme for light and dark mode, use the following /// syntax: `light:theme-name,dark:theme-name`. For example: -/// `light:rose-pine-dawn,dark:rose-pine`. Whitespace around all values are +/// `light:Rose Pine Dawn,dark:Rose Pine`. Whitespace around all values are /// trimmed and order of light and dark does not matter. Both light and dark /// must be specified in this form. In this form, the theme used will be /// based on the current desktop environment theme. From b5b4b1be7261b3f44623a9b3fef40452148cab5a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Sep 2025 06:50:38 -0700 Subject: [PATCH 46/49] ci: no dmg to notarize for debug builds --- .github/workflows/release-tip.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/release-tip.yml b/.github/workflows/release-tip.yml index ec4ef4220..015e42c89 100644 --- a/.github/workflows/release-tip.yml +++ b/.github/workflows/release-tip.yml @@ -579,7 +579,6 @@ jobs: # Finally, we need to "attach the staple" to our executable, which will allow our app to be # validated by macOS even when an internet connection is not available. echo "Attach staple" - xcrun stapler staple "Ghostty.dmg" xcrun stapler staple "macos/build/Release/Ghostty.app" # Zip up the app From f7e622e8af105984f62a40016b9c8beeb122e244 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Sep 2025 13:30:19 -0700 Subject: [PATCH 47/49] config: fix binding parsing to allow values containing `=` Fixes #8667 The binding `a=text:=` didn't parse properly. This is a band-aid solution. It works and we have test coverage for it thankfully. Longer term we should move the parser to a fully state-machine based parser that parses the trigger first then the action, to avoid these kind of things. --- src/input/Binding.zig | 67 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/src/input/Binding.zig b/src/input/Binding.zig index b0e4e918d..54e7754f2 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -64,11 +64,35 @@ pub const Parser = struct { const flags, const start_idx = try parseFlags(raw_input); const input = raw_input[start_idx..]; - // Find the last = which splits are mapping into the trigger - // and action, respectively. - // 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; + // Find the equal sign. This is more complicated than it seems on + // the surface because we need to ignore equal signs that are + // part of the trigger. + const eql_idx: usize = eql: { + // TODO: We should change this parser into a real state machine + // based parser that parses the trigger fully, then yields the + // action after. The loop below is a total mess. + var offset: usize = 0; + while (std.mem.indexOfScalar( + u8, + input[offset..], + '=', + )) |offset_idx| { + // Find: '=+ctrl' or '==action' + const idx = offset + offset_idx; + if (idx < input.len - 1 and + (input[idx + 1] == '+' or + input[idx + 1] == '=')) + { + offset += offset_idx + 1; + continue; + } + + // Looks like the real equal sign. + break :eql idx; + } + + return Error.InvalidFormat; + }; // Sequence iterator goes up to the equal, action is after. We can // parse the action now. @@ -2298,6 +2322,39 @@ test "parse: equals sign" { try testing.expectError(Error.InvalidFormat, parseSingle("=ignore")); } +test "parse: text action equals sign" { + const testing = std.testing; + { + const binding = try parseSingle("==text:="); + try testing.expectEqual(Trigger{ .key = .{ .unicode = '=' } }, binding.trigger); + try testing.expectEqualStrings("=", binding.action.text); + } + + { + const binding = try parseSingle("==text:=hello"); + try testing.expectEqual(Trigger{ .key = .{ .unicode = '=' } }, binding.trigger); + try testing.expectEqualStrings("=hello", binding.action.text); + } + + { + const binding = try parseSingle("ctrl+==text:=hello"); + try testing.expectEqual(Trigger{ + .key = .{ .unicode = '=' }, + .mods = .{ .ctrl = true }, + }, binding.trigger); + try testing.expectEqualStrings("=hello", binding.action.text); + } + + { + const binding = try parseSingle("=+ctrl=text:=hello"); + try testing.expectEqual(Trigger{ + .key = .{ .unicode = '=' }, + .mods = .{ .ctrl = true }, + }, binding.trigger); + try testing.expectEqualStrings("=hello", binding.action.text); + } +} + // 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 67992fde915558b4d0345d8baa7e05a75c4d9209 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Sep 2025 15:28:51 -0700 Subject: [PATCH 48/49] font-size reloads at runtime if the font wasn't manually set This was a very common pitfall for users. The new logic will reload the font-size at runtime, but only if the font wasn't manually set by the user using actions such as `increase_font_size`, `decrease_font_size`, or `set_font_size`. The `reset_font_size` action will reset our state to assume the font-size wasn't manually set. I also updated a comment about `font-family` not reloading at runtime; this wasn't true even prior to this commit. --- src/Surface.zig | 37 +++++++++++++++++++++++++++++++++++-- src/config/Config.zig | 14 ++++++-------- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index dd2babf9b..ec6ec96a3 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -66,6 +66,12 @@ font_grid_key: font.SharedGridSet.Key, font_size: font.face.DesiredSize, font_metrics: font.Metrics, +/// This keeps track of if the font size was ever modified. If it wasn't, +/// then config reloading will change the font. If it was manually adjusted, +/// we don't change it on config reload since we assume the user wants +/// a specific size. +font_size_adjusted: bool, + /// The renderer for this surface. renderer: Renderer, @@ -514,6 +520,7 @@ pub fn init( .rt_surface = rt_surface, .font_grid_key = font_grid_key, .font_size = font_size, + .font_size_adjusted = false, .font_metrics = font_grid.metrics, .renderer = renderer_impl, .renderer_thread = render_thread, @@ -1446,7 +1453,21 @@ pub fn updateConfig( // but this is easier and pretty rare so it's not a performance concern. // // (Calling setFontSize builds and sends a new font grid to the renderer.) - try self.setFontSize(self.font_size); + try self.setFontSize(font_size: { + // If we have manually adjusted the font size, keep it that way. + if (self.font_size_adjusted) { + log.info("font size manually adjusted, preserving previous size on config reload", .{}); + break :font_size self.font_size; + } + + // If we haven't, then we update to the configured font size. + // This allows config changes to update the font size. We used to + // never do this but it was a common source of confusion and people + // assumed that Ghostty was broken! This logic makes more sense. + var size = self.font_size; + size.points = std.math.clamp(config.@"font-size", 1.0, 255.0); + break :font_size size; + }); // We need to store our configs in a heap-allocated pointer so that // our messages aren't huge. @@ -4637,10 +4658,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool log.debug("increase font size={}", .{clamped_delta}); - var size = self.font_size; // Max point size is somewhat arbitrary. + var size = self.font_size; size.points = @min(size.points + clamped_delta, 255); try self.setFontSize(size); + + // Mark that we manually adjusted the font size + self.font_size_adjusted = true; }, .decrease_font_size => |delta| { @@ -4652,6 +4676,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool var size = self.font_size; size.points = @max(1, size.points - clamped_delta); try self.setFontSize(size); + + // Mark that we manually adjusted the font size + self.font_size_adjusted = true; }, .reset_font_size => { @@ -4660,6 +4687,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool var size = self.font_size; size.points = self.config.original_font_size; try self.setFontSize(size); + + // Reset font size also resets the manual adjustment state + self.font_size_adjusted = false; }, .set_font_size => |points| { @@ -4668,6 +4698,9 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool var size = self.font_size; size.points = std.math.clamp(points, 1.0, 255.0); try self.setFontSize(size); + + // Mark that we manually adjusted the font size + self.font_size_adjusted = true; }, .prompt_surface_title => return try self.rt_app.performAction( diff --git a/src/config/Config.zig b/src/config/Config.zig index 2fd1b80b1..945e091f9 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -127,9 +127,6 @@ pub const compatibility = std.StaticStringMap( /// this within config files if you want to clear previously set values in /// configuration files or on the CLI if you want to clear values set on the /// CLI. -/// -/// Changing this configuration at runtime will only affect new terminals, i.e. -/// new windows, tabs, etc. @"font-family": RepeatableString = .{}, @"font-family-bold": RepeatableString = .{}, @"font-family-italic": RepeatableString = .{}, @@ -214,11 +211,12 @@ pub const compatibility = std.StaticStringMap( /// /// For example, 13.5pt @ 2px/pt = 27px /// -/// Changing this configuration at runtime will only affect new terminals, -/// i.e. new windows, tabs, etc. Note that you may still not see the change -/// depending on your `window-inherit-font-size` setting. If that setting is -/// true, only the first window will be affected by this change since all -/// subsequent windows will inherit the font size of the previous window. +/// Changing this configuration at runtime will only affect existing +/// terminals that have NOT manually adjusted their font size in some way +/// (e.g. increased or decreased the font size). Terminals that have manually +/// adjusted their font size will retain their manually adjusted size. +/// Otherwise, the font size of existing terminals will be updated on +/// reload. /// /// On Linux with GTK, font size is scaled according to both display-wide and /// text-specific scaling factors, which are often managed by your desktop From 4f47138ea35406348b446aec9ec5bdf8ea720815 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 16 Sep 2025 16:32:38 -0700 Subject: [PATCH 49/49] selection scrolling should only depend on y value Fixes #8683 The selection scrolling logic should only depend on the y value of the cursor position, not the x value. This presents unwanted scroll behaviors, such as reversing the scroll direction which was just a side effect of attempting to scroll tick to begin with. --- src/Surface.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index ec6ec96a3..8edeadf83 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -4012,7 +4012,7 @@ pub fn cursorPosCallback( // Stop selection scrolling when inside the viewport within a 1px buffer // for fullscreen windows, but only when selection scrolling is active. - if (pos.x >= 1 and pos.y >= 1 and self.selection_scroll_active) { + if (pos.y >= 1 and self.selection_scroll_active) { self.io.queueMessage( .{ .selection_scroll = false }, .locked,