183 Commits

Author SHA1 Message Date
Kat
7d5be8e960 i18n: update Russian translation (#8473)
Part of #8344
2025-09-03 04:12:21 +00:00
Mitchell Hashimoto
d05ec81b86 apprt/gtk-ng: must quit scenarios should quit immediately (#8500)
Fixes #8495

We were incorrectly calling graceful quit under must quit scenarios.
This would do things like confirm quit by inspecting for running
processes. However, must quit scenarios (namely when all windows are
destroyed) should quit immediately without any checks because the
dispose process takes more event loop ticks to fully finish.
2025-09-02 20:46:35 -07:00
Mitchell Hashimoto
f016b79f22 apprt/gtk-ng: must quit scenarios should quit immediately
Fixes #8495

We were incorrectly calling graceful quit under must quit scenarios.
This would do things like confirm quit by inspecting for running
processes. However, must quit scenarios (namely when all windows are
destroyed) should quit immediately without any checks because the
dispose process takes more event loop ticks to fully finish.
2025-09-02 20:42:01 -07:00
Ivan Bastrakov
52f5ab1a36 i18n: update Russian translation 2025-09-03 01:57:25 +03:00
Mitchell Hashimoto
8d11c08db3 feat: add selection-clear-on-copy configuration option (#8462)
Addresses issue: Add selection-clear-on-copy configuration #8407

Added configuration option `selection-clear-on-copy` that matches with
the `selection-clear-on-typing` option.
And `copy-on-select` is ignored when `selection-clear-on-copy` is true
regardless of whether `copy-on-select` is set to true or clipboard.
Also `.copy_to_clipboard` binding action was refactored to use
`copySelectionToClipboards` for consistent behavior.

> Consulted with Copilot (Claude Sonnet 4) to understand the control
flow of copy operations and help write the docs. Solution was authored
and implemented by me.
2025-09-02 14:55:46 -07:00
Toufiq Shishir
90c0fc2590 feat: add selection-clear-on-copy configuration 2025-09-02 14:48:47 -07:00
Mitchell Hashimoto
e909e28876 Compare fields directly instead of PackedStyle (#8489)
This is a small, but I think worthwhile micro optimization in style.zig,
I uncovered while investigating wider ranging optimizations in the
rendering section.

For me it results in ~4-5% increase in fps for DOOM-fire-zig benchmark,
which maximally stresses this code path.

Comparing the fields directly is actually faster than PackedStyle.

I wrote the code in style.zig, claude 4 wrote the benchmark, all PR
responses will be generated by jcm-slow-1


Style.eql Benchmark Comparison
==============================

Test: Small (1K pairs, 50% equal)
--------------------------------------------------
New implementation:
  Iterations: 49937
  Duration: 500.01 ms
  Throughput: 99872402 comparisons/sec
Old implementation:
  Iterations: 8508
  Duration: 500.06 ms
  Throughput: 17014026 comparisons/sec
Performance improvement:
  Speedup: 5.87x
  Improvement: +487.0%

Test: Medium (10K pairs, 50% equal)
--------------------------------------------------
New implementation:
  Iterations: 4435
  Duration: 500.09 ms
  Throughput: 88684746 comparisons/sec
Old implementation:
  Iterations: 850
  Duration: 500.50 ms
  Throughput: 16983017 comparisons/sec
Performance improvement:
  Speedup: 5.22x
  Improvement: +422.2%

Test: Large (50K pairs, 50% equal)
--------------------------------------------------
New implementation:
  Iterations: 861
  Duration: 500.41 ms
  Throughput: 86030144 comparisons/sec
Old implementation:
  Iterations: 171
  Duration: 501.70 ms
  Throughput: 17041989 comparisons/sec
Performance improvement:
  Speedup: 5.05x
  Improvement: +404.8%

Test: Mostly equal (10K pairs, 90% equal)
--------------------------------------------------
New implementation:
  Iterations: 4608
  Duration: 500.03 ms
  Throughput: 92154471 comparisons/sec
Old implementation:
  Iterations: 854
  Duration: 500.45 ms
  Throughput: 17064744 comparisons/sec
Performance improvement:
  Speedup: 5.40x
  Improvement: +440.0%

Test: Mostly different (10K pairs, 10% equal)
--------------------------------------------------
New implementation:
  Iterations: 4065
  Duration: 500.03 ms
  Throughput: 81294960 comparisons/sec
Old implementation:
  Iterations: 848
  Duration: 500.21 ms
  Throughput: 16952948 comparisons/sec
Performance improvement:
  Speedup: 4.80x
  Improvement: +379.5%

Test: Same flags (10K pairs, 50% equal)
--------------------------------------------------
New implementation:
  Iterations: 2799
  Duration: 500.00 ms
  Throughput: 55979776 comparisons/sec
Old implementation:
  Iterations: 859
  Duration: 500.13 ms
  Throughput: 17175672 comparisons/sec
Performance improvement:
  Speedup: 3.26x
  Improvement: +225.9%
2025-09-02 14:37:30 -07:00
Jesse Miller
cf0390bab5 Use comptime for eql() to ensure Style struct coverage. 2025-09-02 15:14:42 -06:00
Jesse Miller
4614e5fdad Zig 0.14+ can directly compare packed structs. 2025-09-02 14:58:21 -06:00
Mitchell Hashimoto
ce94bb9f6a macOS: firstRect should return full rect width/height (#8492)
Fixes #2473

This commit changes `ghostty_surface_ime_point` to return a full rect
with the width/height calculated for the preedit.

The `firstRect` function, which calls `ghostty_surface_ime_point` was
previously setting the width/height to zero. macOS didn't like this. We
then changed it to just hardcode it to width/height of one cell. This
worked but made it so the IME cursor didn't follow the preedit.

The result is shown in the video below. Notice the dictation icon
follows the text properly:



https://github.com/user-attachments/assets/81be8c63-9f0a-49b7-ac30-2db930beb238
2025-09-02 13:28:08 -07:00
Jesse Miller
ac104a3dfc zig fmt 2025-09-02 14:14:06 -06:00
Mitchell Hashimoto
16e47e7586 fix(font): detect and reject improper advance for icwidth (#8491)
Fixes #8481

Explained in code comments, basically the NF patcher can produce fonts
that have CJK characters with 1-cell advances, which screws up fallback
font scaling; fixed by not counting the ic width metric if the width of
the glyph is greater than the advance width.

> [!NOTE]
> As follow-on work to this it may be worth setting limits for scaling,
so you can't have one font scaled like twice as large as the primary
font, since that's almost always going to indicate something is very
wrong.
2025-09-02 13:09:07 -07:00
Mitchell Hashimoto
e8217aa007 macOS: firstRect should return full rect width/height
Fixes #2473

This commit changes `ghostty_surface_ime_point` to return a full rect
with the width/height calculated for the preedit.

The `firstRect` function, which calls `ghostty_surface_ime_point` was
previously setting the width/height to zero. macOS didn't like this. We
then changed it to just hardcode it to width/height of one cell. This
worked but made it so the IME cursor didn't follow the preedit.
2025-09-02 13:08:46 -07:00
Qwerasd
9aa1698e5a font: log warning when rejecting ic_width 2025-09-02 13:47:59 -06:00
Mitchell Hashimoto
3664ee9f87 macOS: Notify macOS of cell width/height for firstRect (#8490)
Related to #2473

This fixes an issue where the dictation icon didn't show the language
picker.
2025-09-02 12:38:50 -07:00
Qwerasd
a72995590b fix(font): detect and reject improper advance for icwidth 2025-09-02 13:33:33 -06:00
Mitchell Hashimoto
2bf0d3f4c7 macOS: Notify macOS of cell width/height for firstRect
Related to #2473

This fixes an issue where the dictation icon didn't show the language 
picker.
2025-09-02 12:26:52 -07:00
Mitchell Hashimoto
4af290d5f0 fix(renderer): kitty images should all be processed (#8488)
When processing kitty images in a loop in a few places we were returning
under certain conditions where we should instead have just continued the
loop. This caused serious problems for kitty images, especially for apps
that used multiple images on screen at once.

... I have no clue how I originally wrote this code and didn't see such
a trivial mistake, I think I was sleep deprived or something.

Should fix #8471
2025-09-02 12:14:11 -07:00
Qwerasd
ef7857f9be fix(renderer): kitty images should all be processed
When processing kitty images in a loop in a few places we were returning
under certain conditions where we should instead have just continued the
loop. This caused serious problems for kitty images, especially for apps
that used multiple images on screen at once.

... I have no clue how I originally wrote this code and didn't see such
a trivial mistake, I think I was sleep deprived or something.
2025-09-02 12:42:34 -06:00
Jesse Miller
7dcf2c9b62 Compare fields directly instead of PackedStyle
Comparing the fields directly is actually faster than PackedStyle
2025-09-02 12:05:30 -06:00
Mitchell Hashimoto
d316449ebf config: bind both physical digit plus unicode digit for goto_tab (#8486)
Fixes #8478

The comments explain this.
2025-09-02 09:12:08 -07:00
Mitchell Hashimoto
650028fa9f config: bind both physical digit plus unicode digit for goto_tab
Fixes #8478

The comments explain this.
2025-09-02 09:00:58 -07:00
Mitchell Hashimoto
5ef6412823 macOS: Progress bar for OSC9 progress reports (#8477)
#7975 but for macOS. The behavior and even the look is almost identical:


https://github.com/user-attachments/assets/b7e0b370-3a30-443d-89ae-08209d2c9b89

Similar to GTK, it'll remove the progress bar state after 15 seconds. 

AI disclaimer: Claude code produced most of this code. I reviewed all
the lines and understand them completely.
2025-08-31 20:51:54 -07:00
Mitchell Hashimoto
0b58830882 macOS: Progress bar for OSC9 progress reports 2025-08-31 20:42:34 -07:00
Mitchell Hashimoto
a41ec17b61 build(deps): bump actions/checkout from 4.3.0 to 5.0.0 (#8476)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.0
to 5.0.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/releases">actions/checkout's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
<li>Prepare v5.0.0 release by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2238">actions/checkout#2238</a></li>
</ul>
<h2>⚠️ Minimum Compatible Runner Version</h2>
<p><strong>v2.327.1</strong><br />
<a
href="https://github.com/actions/runner/releases/tag/v2.327.1">Release
Notes</a></p>
<p>Make sure your runner is updated to this version or newer to use this
release.</p>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/actions/checkout/compare/v4...v5.0.0">https://github.com/actions/checkout/compare/v4...v5.0.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>V5.0.0</h2>
<ul>
<li>Update actions checkout to use node 24 by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2226">actions/checkout#2226</a></li>
</ul>
<h2>V4.3.0</h2>
<ul>
<li>docs: update README.md by <a
href="https://github.com/motss"><code>@​motss</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1971">actions/checkout#1971</a></li>
<li>Add internal repos for checking out multiple repositories by <a
href="https://github.com/mouismail"><code>@​mouismail</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1977">actions/checkout#1977</a></li>
<li>Documentation update - add recommended permissions to Readme by <a
href="https://github.com/benwells"><code>@​benwells</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2043">actions/checkout#2043</a></li>
<li>Adjust positioning of user email note and permissions heading by <a
href="https://github.com/joshmgross"><code>@​joshmgross</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2044">actions/checkout#2044</a></li>
<li>Update README.md by <a
href="https://github.com/nebuk89"><code>@​nebuk89</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2194">actions/checkout#2194</a></li>
<li>Update CODEOWNERS for actions by <a
href="https://github.com/TingluoHuang"><code>@​TingluoHuang</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/2224">actions/checkout#2224</a></li>
<li>Update package dependencies by <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/2236">actions/checkout#2236</a></li>
</ul>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment
variables by <a href="https://github.com/jww3"><code>@​jww3</code></a>
in <a
href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<h2>v4.2.1</h2>
<ul>
<li>Check out other refs/* by commit if provided, fall back to ref by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1924">actions/checkout#1924</a></li>
</ul>
<h2>v4.2.0</h2>
<ul>
<li>Add Ref and Commit outputs by <a
href="https://github.com/lucacome"><code>@​lucacome</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1180">actions/checkout#1180</a></li>
<li>Dependency updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>- <a
href="https://redirect.github.com/actions/checkout/pull/1777">actions/checkout#1777</a>,
<a
href="https://redirect.github.com/actions/checkout/pull/1872">actions/checkout#1872</a></li>
</ul>
<h2>v4.1.7</h2>
<ul>
<li>Bump the minor-npm-dependencies group across 1 directory with 4
updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1739">actions/checkout#1739</a></li>
<li>Bump actions/checkout from 3 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1697">actions/checkout#1697</a></li>
<li>Check out other refs/* by commit by <a
href="https://github.com/orhantoy"><code>@​orhantoy</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1774">actions/checkout#1774</a></li>
<li>Pin actions/checkout's own workflows to a known, good, stable
version. by <a href="https://github.com/jww3"><code>@​jww3</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1776">actions/checkout#1776</a></li>
</ul>
<h2>v4.1.6</h2>
<ul>
<li>Check platform to set archive extension appropriately by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1732">actions/checkout#1732</a></li>
</ul>
<h2>v4.1.5</h2>
<ul>
<li>Update NPM dependencies by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1703">actions/checkout#1703</a></li>
<li>Bump github/codeql-action from 2 to 3 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1694">actions/checkout#1694</a></li>
<li>Bump actions/setup-node from 1 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1696">actions/checkout#1696</a></li>
<li>Bump actions/upload-artifact from 2 to 4 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1695">actions/checkout#1695</a></li>
<li>README: Suggest <code>user.email</code> to be
<code>41898282+github-actions[bot]@users.noreply.github.com</code> by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1707">actions/checkout#1707</a></li>
</ul>
<h2>v4.1.4</h2>
<ul>
<li>Disable <code>extensions.worktreeConfig</code> when disabling
<code>sparse-checkout</code> by <a
href="https://github.com/jww3"><code>@​jww3</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1692">actions/checkout#1692</a></li>
<li>Add dependabot config by <a
href="https://github.com/cory-miller"><code>@​cory-miller</code></a> in
<a
href="https://redirect.github.com/actions/checkout/pull/1688">actions/checkout#1688</a></li>
<li>Bump the minor-actions-dependencies group with 2 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1693">actions/checkout#1693</a></li>
<li>Bump word-wrap from 1.2.3 to 1.2.5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/actions/checkout/pull/1643">actions/checkout#1643</a></li>
</ul>
<h2>v4.1.3</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="08c6903cd8"><code>08c6903</code></a>
Prepare v5.0.0 release (<a
href="https://redirect.github.com/actions/checkout/issues/2238">#2238</a>)</li>
<li><a
href="9f265659d3"><code>9f26565</code></a>
Update actions checkout to use node 24 (<a
href="https://redirect.github.com/actions/checkout/issues/2226">#2226</a>)</li>
<li>See full diff in <a
href="https://github.com/actions/checkout/compare/v4.3.0...08c6903cd8c0fde910a37f88322edcfb5dd907a8">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=4.3.0&new-version=5.0.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-08-31 19:50:03 -07:00
dependabot[bot]
c535d0a664 build(deps): bump actions/checkout from 4.3.0 to 5.0.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.3.0 to 5.0.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.3.0...08c6903cd8c0fde910a37f88322edcfb5dd907a8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 5.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-01 00:47:33 +00:00
Mitchell Hashimoto
2009ea511d feat: added faint-opacity option (#8472)
This pull request adds the `--faint-opacity` option, as discussed in
#7637.

The default value of the option is also changed from `0.68` to `0.5` for
greater consistency with other popular terminal emulators.
2025-08-31 13:45:26 -07:00
Mitchell Hashimoto
d8578a9ee2 fix: correct the cursor Y position value exposed to shader uniforms (#8122)
Fix for discussion #8113

The cursor Y position value exposed to the shader uniforms was
incorrectly calculated. As per the doc in cell_text.v.glsl: In order to
get the top left of the glyph, we compute an offset based on the
bearings. The Y bearing is the distance from the bottom of the cell to
the top of the glyph, so we subtract it from the cell height to get the
y offset.

This calculation was mistakenly left out of the original code.

This will ensure that the custom shaders using
iCurrentCursor/iPreviousCursor get the correct Y coordinate representing
the top-left corner of the cursor rectangle, matching the documented
uniform behavior
2025-08-31 11:32:59 -07:00
Qwerasd
0d30f859bd renderer: clarify and correct custom shader cursor position math
This math was incorrect from the start, the previous fix helped OpenGL
but broke positioning under Metal; this commit fixes the math to be
correct under both backends and adds comments explaining exactly what's
going on.
2025-08-31 11:44:10 -06:00
Pavel Ivanov
650095e7e9 fix: changed default faint-opacity value to 0.5 2025-08-31 17:21:00 +02:00
Pavel Ivanov
6319464cfb refactor: move faint-opacity clamping to config finalization 2025-08-31 17:19:51 +02:00
Pavel Ivanov
fc6266133f feat: added faint-opacity option 2025-08-31 15:00:29 +02:00
Mitchell Hashimoto
a51a956bdb Update iTerm2 colorschemes (#8470)
Upstream revision:
6cdbc8501d
2025-08-30 21:01:10 -07:00
mitchellh
c94805f0aa deps: Update iTerm2 color schemes 2025-08-31 00:16:30 +00:00
Mitchell Hashimoto
937d17cc35 ci: add freebsd tests (#8466) 2025-08-30 12:59:01 -07:00
Jeffrey C. Ollie
0bc90b2a20 ci: build on freebsd 2025-08-30 13:58:25 -05:00
Jeffrey C. Ollie
75e3835a9e gtk-ng: ensure CSS works on both 4.14 and 4.16+ (#8459)
Ghostty 1.2 needs to support GTK 4.14 because that's the version that
ships with Ubuntu 24.04.

This PR ensures that any GTK 4.16 CSS features are not used in any
static CSS and that the runtime CSS loading handles both 4.14 and 4.16+
appropriately.
2025-08-30 10:41:59 -05:00
Jeffrey C. Ollie
d1e01ec5c3 gtk-ng: ensure CSS works on both 4.14 and 4.16+
Ghostty 1.2 needs to support GTK 4.14 because that's the version that
ships with Ubuntu 24.04.

This PR ensures that any GTK 4.16 CSS features are not used in any
static CSS and that the runtime CSS loading handles both 4.14 and 4.16+
appropriately.
2025-08-30 09:54:40 -05:00
Mitchell Hashimoto
b0d9b0dee0 build(deps): bump namespacelabs/nscloud-cache-action from 1.2.16 to 1.2.17 (#8449)
Bumps
[namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action)
from 1.2.16 to 1.2.17.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/namespacelabs/nscloud-cache-action/releases">namespacelabs/nscloud-cache-action's
releases</a>.</em></p>
<blockquote>
<h2>v1.2.17</h2>
<h2>What's Changed</h2>
<ul>
<li>Delete existing files at path before creating bind mount, this was
already handled correctly for existing directories but not for
files</li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/namespacelabs/nscloud-cache-action/compare/v1.2.16...v1.2.17">https://github.com/namespacelabs/nscloud-cache-action/compare/v1.2.16...v1.2.17</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a289cf5d2f"><code>a289cf5</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-cache-action/issues/33">#33</a>
from namespacelabs/delete-existing-files-at-path-befor...</li>
<li><a
href="3851f57081"><code>3851f57</code></a>
Delete existing files at path before creating bind mount</li>
<li><a
href="58efedf646"><code>58efedf</code></a>
Merge pull request <a
href="https://redirect.github.com/namespacelabs/nscloud-cache-action/issues/32">#32</a>
from namespacelabs/delete-existing-files-at-path-befor...</li>
<li><a
href="5e60691b8f"><code>5e60691</code></a>
Delete existing files at path before creating bind mount</li>
<li>See full diff in <a
href="305bfa7ea9...a289cf5d2f">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=namespacelabs/nscloud-cache-action&package-manager=github_actions&previous-version=1.2.16&new-version=1.2.17)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-08-29 07:08:26 -07:00
Mitchell Hashimoto
e6b019b197 build(deps): bump cachix/install-nix-action from 31.5.2 to 31.6.0 (#8450)
Bumps
[cachix/install-nix-action](https://github.com/cachix/install-nix-action)
from 31.5.2 to 31.6.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cachix/install-nix-action/releases">cachix/install-nix-action's
releases</a>.</em></p>
<blockquote>
<h2>v31.6.0</h2>
<h2>What's Changed</h2>
<ul>
<li>chore(deps): bump actions/checkout from 4 to 5 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/249">cachix/install-nix-action#249</a></li>
<li>docs: add example for <code>nix develop</code> by <a
href="https://github.com/jennydaman"><code>@​jennydaman</code></a> in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/248">cachix/install-nix-action#248</a></li>
<li>nix: 2.30.2 -&gt; 2.31.0 by <a
href="https://github.com/github-actions"><code>@​github-actions</code></a>[bot]
in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/250">cachix/install-nix-action#250</a>
Release notes: <a
href="https://discourse.nixos.org/t/nix-2-31-0-released/68465">https://discourse.nixos.org/t/nix-2-31-0-released/68465</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/jennydaman"><code>@​jennydaman</code></a> made
their first contribution in <a
href="https://redirect.github.com/cachix/install-nix-action/pull/248">cachix/install-nix-action#248</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cachix/install-nix-action/compare/v31.5.2...v31.6.0">https://github.com/cachix/install-nix-action/compare/v31.5.2...v31.6.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="56a7bb7b56"><code>56a7bb7</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/250">#250</a>
from cachix/create-pull-request/patch</li>
<li><a
href="c04e864467"><code>c04e864</code></a>
nix: 2.30.2 -&gt; 2.31.0</li>
<li><a
href="9aaadd8b85"><code>9aaadd8</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/248">#248</a>
from jennydaman/patch-1</li>
<li><a
href="a23271bac0"><code>a23271b</code></a>
Reword README.md section on <code>nix develop</code></li>
<li><a
href="f02d365678"><code>f02d365</code></a>
Merge pull request <a
href="https://redirect.github.com/cachix/install-nix-action/issues/249">#249</a>
from cachix/dependabot/github_actions/actions/checkout-5</li>
<li><a
href="b4dc112147"><code>b4dc112</code></a>
chore(deps): bump actions/checkout from 4 to 5</li>
<li><a
href="ca6a0fa535"><code>ca6a0fa</code></a>
Add example for <code>nix develop</code></li>
<li><a
href="96bd9f39e4"><code>96bd9f3</code></a>
ci: update nixpkgs channel used in tests</li>
<li><a
href="92ffed7f0d"><code>92ffed7</code></a>
ci: make test workflow dispatchable</li>
<li>See full diff in <a
href="fc6e360bed...56a7bb7b56">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cachix/install-nix-action&package-manager=github_actions&previous-version=31.5.2&new-version=31.6.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>
2025-08-29 07:08:09 -07:00
Guilherme Nandi Tiscoski
5761f66f35 i18n: update pt_BR translations (#8391)
relative to https://github.com/ghostty-org/ghostty/issues/8344
2025-08-29 13:26:37 +00:00
trag1c
a5eef1d227 i18n: Updating Irish translation for Ghostty 1.2 (#8349)
This Pull Request updates 5 translation strings for Ghostty 1.2 as per
#8344
2025-08-29 11:01:00 +02:00
dependabot[bot]
85e642097a build(deps): bump cachix/install-nix-action from 31.5.2 to 31.6.0
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 31.5.2 to 31.6.0.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Changelog](https://github.com/cachix/install-nix-action/blob/master/RELEASE.md)
- [Commits](fc6e360bed...56a7bb7b56)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-version: 31.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-29 00:08:00 +00:00
dependabot[bot]
bed350f0be build(deps): bump namespacelabs/nscloud-cache-action
Bumps [namespacelabs/nscloud-cache-action](https://github.com/namespacelabs/nscloud-cache-action) from 1.2.16 to 1.2.17.
- [Release notes](https://github.com/namespacelabs/nscloud-cache-action/releases)
- [Commits](305bfa7ea9...a289cf5d2f)

---
updated-dependencies:
- dependency-name: namespacelabs/nscloud-cache-action
  dependency-version: 1.2.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-29 00:07:51 +00:00
Mitchell Hashimoto
460fcc1344 docs: reorganize and separate out HACKING.md for technical instructions (#8445)
Also did some copy-editing and removed some things that haven't been
present in the codebase for a while (I thought we nuked conformance
stuff in 1.1 already...?)
2025-08-28 15:20:23 -07:00
Leah Amelia Chen
f91e6f1764 docs: better integrate CONTRIBUTING into the README 2025-08-29 06:14:41 +08:00
Leah Amelia Chen
f802d33652 docs: divide content more evenly between CONTRIBUTING and HACKING
CONTRIBUTING should now solely be about the contribution *process*
while HACKING goes into the technical details
2025-08-29 06:14:41 +08:00
Leah Amelia Chen
2701932475 docs: separate out HACKING.md from README.md
Also did some copy-editing and removed some things that haven't been
present in the codebase for a while (I thought we nuked conformance
stuff in 1.1 already...?)
2025-08-29 06:14:36 +08:00
Mitchell Hashimoto
6cfd89e248 macOS: Always require confirmation when executing a script via open (Finder, etc.) (#8442)
Right now, passing a file path to Ghostty will always execute it
unconditionally. This has various risks associated with it. I think we
can mitigate a lot of risks in the future by inspecting what is being
executed, but to be safe now we should always ask for confirmation.
2025-08-28 13:12:48 -07:00
Mitchell Hashimoto
9962e523a8 some typos 2025-08-28 12:58:07 -07:00
Mitchell Hashimoto
04956f3dc1 macos: require confirmation to run any script 2025-08-28 12:34:04 -07:00
Mitchell Hashimoto
f1ea30dcf1 macos: when executing a script directly, always wait after command 2025-08-28 11:26:33 -07:00
trag1c
d3cadf2495 i18n: update bg_BG translations (#8345)
Part of https://github.com/ghostty-org/ghostty/issues/8344
2025-08-27 23:16:15 +02:00
Mitchell Hashimoto
7106d71a42 apprt/gtk-ng: "cancel" should be default close response for dialog (#8427)
Fixes #8424

This is the response that will be sent when "escape" is pressed.

This also fixes a null ptr deref that was possible when this fix wasn't
in.
2025-08-27 09:48:19 -07:00
Mitchell Hashimoto
f54f2dc7f3 apprt/gtk-ng: "cancel" should be default close response for dialog
Fixes #8424

This is the response that will be sent when "escape" is pressed.

This also fixes a null ptr deref that was possible when this fix wasn't
in.
2025-08-27 09:42:35 -07:00
Mitchell Hashimoto
5013d028a3 terminal: fix csi parsing (#8417)
Make `MAX_PARAMS` public and increase CSI parameter limit from 16 to 24.
Fix potential out-of-bounds read in SGR partial sequence extraction.

Related discussion:
https://github.com/ghostty-org/ghostty/discussions/5198

DISCLAIMER: the tests were written with Claude Code's help.
2025-08-27 07:20:16 -07:00
Mitchell Hashimoto
adfc93047c terminal: fix up some tests to be more robust 2025-08-27 07:15:42 -07:00
Adrià Arrufat
a3f4997fbc fix(terminal): handle CSI/SGR with many parameters
Adds tests to ensure CSI and SGR sequences with 17 or more parameters are correctly parsed, fixing a bug where later parameters were previously dropped.
2025-08-27 07:10:17 -07:00
Adrià Arrufat
56d3fd872e fix(terminal): improve CSI parameter parsing
Make `MAX_PARAMS` public and increase CSI parameter limit from 16 to 24.
Fix potential out-of-bounds read in SGR partial sequence extraction.
2025-08-27 07:10:17 -07:00
Mitchell Hashimoto
58e85bf133 macos: use visible frame for quick terminal sizing calculation (#8423)
Fixes #8418

This fixes issues where left/right positions would be cut off from the
menu bar. And makes it so that size 100%,100% doesn't overflow into the
non-visible space of the edge of the screen.

I didn't just copy and paste, I tested each of these code paths.
2025-08-27 07:06:55 -07:00
Mitchell Hashimoto
19a27383f8 macos: use visible frame for quick terminal sizing calculation
Fixes #8418

This fixes issues where left/right positions would be cut off from the
menu bar. And makes it so that size 100%,100% doesn't overflow into the
non-visible space of the edge of the screen.
2025-08-27 06:59:02 -07:00
reo101
8fa065512f i18n: update bg_BG translations 2025-08-27 16:39:58 +03:00
Jeffrey C. Ollie
6530107e3b config: add entry for scroll-to-bottom (#8412)
Related #8408

(EDIT @mitchellh: Removed "Fixes" to avoid closing)
2025-08-26 22:26:03 -05:00
Jeffrey C. Ollie
87056a2600 surface: store entire scroll-to-bottom struct 2025-08-26 22:04:23 -05:00
Jeffrey C. Ollie
6a128189e3 osc: conemu cleanup (#8413)
- Add more comments, and make existing ones more consistent.
- Rename commands so they consitently have a `conemu_` prefix.
- Ensure that OSC 9 desktop notifications can be sent in the maximum
number of circumstances. There are still many notifications that can't
be sent because of our support for the ConEmu OSCs but that's the
tradeoff we have chosen. We recommend that you switch to OSC 777 to
ensure desktop notifications can be sent in all circumstances.
- Make sure that the tests that exercise the ConEmu OSCs have a
consistent naming structure. That will make them easier to find through
searching as well as make it easier to filter only the ConEmu OSC tests.
- Add more tests to make sure that desktop notifications are sent
properly.
2025-08-26 22:03:42 -05:00
Jeffrey C. Ollie
2490171304 surface: implement scroll-to-bottom=keystroke 2025-08-26 21:48:18 -05:00
Jeffrey C. Ollie
31c96d906a config: add entry for scroll-to-bottom
Fixes #8408
2025-08-26 21:48:18 -05:00
Jeffrey C. Ollie
64d8492836 osc: conemu cleanup
- Add more comments, and make existing ones more consistent.
- Rename commands so they consitently have a `conemu_` prefix.
- Ensure that OSC 9 desktop notifications can be sent in the maximum
  number of circumstances. There are still many notifications that can't
  be sent because of our support for the ConEmu OSCs but that's the
  tradeoff we have chosen. We recommend that you switch to OSC 777 to
  ensure desktop notifications can be sent in all circumstances.
- Make sure that the tests that exercise the ConEmu OSCs have a
  consistent naming structure. That will make them easier to find
  through searching as well as make it easier to filter only the ConEmu
  OSC tests.
- Add more tests to make sure that desktop notifications are sent
  properly.
2025-08-26 21:28:50 -05:00
Mitchell Hashimoto
c1ab41afac osc: parse OSC 9;6 gui macros (#8410) 2025-08-26 14:56:42 -07:00
Jeffrey C. Ollie
f047db6a3b osc: parse OSC 9;6 gui macros 2025-08-26 16:28:25 -05:00
Mitchell Hashimoto
cd8455c24b apprt/gtk-ng: show error widget if GLArea fails to initialize (#8390)
If GTK can't acquire an OpenGL context, this shows a message.
Previously, we would only log a warning which was difficult to find. The
GUI previously was the default GTK view which showed "Failed to acquire
EGL display" which was equally confusing.

This is a draft. There are TODOs (listed below).

## TODO

- [x] Disable context menu in error state
- [x] Use property to bind to unhealthy state instead of directly
setting stack child
- [x] Create a web page and put that in the error description
- [x] Set non-transparent background in error state
- [x] Bug where closing the window isn't exiting Ghostty
2025-08-26 12:07:42 -07:00
Mitchell Hashimoto
4d6269a859 apprt/gtk-ng: show error widget if GLArea fails to initialize
If GTK can't acquire an OpenGL context, this shows a message.
Previously, we would only log a warning which was difficult to find. The
GUI previously was the default GTK view which showed "Failed to acquire
EGL display" which was equally confusing.
2025-08-26 12:03:02 -07:00
Mitchell Hashimoto
3fb17dc802 scroll: round up fractional mouse scroll ticks (#7185)
Scrolling with a mouse on macos doesn't work very well when doing small,
single tick scrolls. macos attempts to mimic precision scrolling by
changing the magnitude of the scroll deltas based on scrolling speed.
Slow scrolls only send deltas with a magnitude of 0.1, which isn't
enough to send a single scroll event with the default scroll multiplier
of 3. Changing the scroll multiplier to 10 as a workaround (so even
single small scroll ticks are enough to register a scroll event) cause
scrolling to be way too fast if the scroll speed is ramped up.

This commit causes the yoffset delta to be rounded out to at least a
magnitude of 1 in the appropriate direction. Single scroll ticks now
register as a single vertical cell scroll event, but as scroll speed is
ramped up, the true delta reported to the surface is used again. Setting
a scroll multiplier of 1 with the changes here makes mouse scrolling
feel just as good as trackpad precision scrolling.
2025-08-26 11:07:12 -07:00
John Drouhard
6cf636b1ad scroll: round up fractional mouse scroll ticks
Scrolling with a mouse on macos doesn't work very well when doing small,
single tick scrolls. macos attempts to mimic precision scrolling by
changing the magnitude of the scroll deltas based on scrolling speed.
Slow scrolls only send deltas with a magnitude of 0.1, which isn't
enough to send a single scroll event with the default scroll multiplier
of 3. Changing the scroll multiplier to 10 as a workaround (so even
single small scroll ticks are enough to register a scroll event) cause
scrolling to be way too fast if the scroll speed is ramped up.

This commit causes the yoffset delta to be rounded out to at least a
magnitude of 1 in the appropriate direction. For small single scroll
ticks, it's enough to register a scroll event, but as scroll speed is
ramped up, the true delta reported to the surface is used again. Setting
a scroll multiplier of 1 with the changes here makes mouse scrolling
feel just as good as trackpad precision scrolling.
2025-08-26 11:02:36 -07:00
Mitchell Hashimoto
673afd193b macos: fix quick terminal fullscreen crash bug (#8093)
Fullscreen on quick terminal was failing with a crash, when it tried
to save the state of a non-existent toolbar and its accessory view
controllers.

See #7980
2025-08-26 10:51:31 -07:00
Mitchell Hashimoto
ff61cad1e2 gtk-ng: implement close_tab:other keybind (#8403) 2025-08-26 10:48:55 -07:00
Mitchell Hashimoto
520eaec61c macos: fix quick terminal issue where closing while fullscreen 2025-08-26 10:40:41 -07:00
Alexander Lais
e676eae640 macos: fix quick terminal fullscreen
Fullscreen on quick terminal was failing with a crash, when it tried
to save the state of a non-existent toolbar and its accessory view
controllers.
2025-08-26 10:36:06 -07:00
Mitchell Hashimoto
830194d436 Quick Terminal Sizing on macOS (#8402)
Fixes #8398
Fixes #2384 

This is just #7576 rebased with some style changes. For some reason I
couldn't update that PR.
2025-08-26 10:31:02 -07:00
Jeffrey C. Ollie
6f630a27be gtk-ng: implement close_tab:other keybind 2025-08-26 12:22:45 -05:00
Mitchell Hashimoto
ae48f323d7 macos: style changes for quick terminal sizing 2025-08-26 10:20:16 -07:00
Mitchell Hashimoto
a90bf58080 config: change quick terminal size C layout to tagged union 2025-08-26 09:52:26 -07:00
Friedrich Stoltzfus
6a78f9c0c0 Merge branch 'ghostty-org:main' into quick-term-initial-size 2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
466fdfffe6 macOS: rename c struct, relocate QuickTerminalSize file
Renamed the ghostty_quick_terminal_size_u to
ghostty_quick_terminal_size_s and moved the QuickTerminalSize file to
the Ghostty folder as requested.
2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
58e7400ea5 macOS: Round quick terminal window position coordinates
This resolves an issue where the right side of the quick terminal would
not resize equally to the left side if adjusting the width from the left
side.
2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
0afadeea5f use decl literals as suggested
Applied from the code review

Co-authored-by: Leah Amelia Chen <github@acc.pluie.me>
2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
e5ad6603f4 Merge branch 'main' into quick-term-initial-size 2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
7cc0728fe5 macOS: enable quick terminal manual resizing
You can now resize the quick terminal both vertically and horizontally. To incorporate adjusting the custom secondary size on the quick terminal we needed to have the ability to resize the width (if from top, bottom, or center), and height (if from right, left, or center). The quick terminal will retain the user's manually adjusted size while the app is open. A new feature with this is that when the secondary size is adjusted (or primary if the quick terminal is center), the size will increase or decrease on both sides of the terminal.
2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
17f7f204e1 macOS: update zig and c structs for quick terminal size
Applying the feedback given by @pluiedev to use an enum to specify the
type of quick terminal size configuration given (pixels or percentage).
Updated the Swift code to work with the enum as well.
2025-08-26 09:47:31 -07:00
Friedrich Stoltzfus
63cd424678 macOS: Add support for quick terminal sizing configuration
Added C bindings for the already existing quick-terminal-size
configuration. Created a new QuickTerminalSize struct to hold these
values in Swift. Updated the QuickTerminal implementation to use the
user's configuration if supplied. Retains defaults. Also adds support to
customize the width of the quick terminal (height if quick terminal is
set to right or left).
2025-08-26 09:47:31 -07:00
Mitchell Hashimoto
5c464e855d parameterize close_tab (#8389)
- Add mode (`this`/`other`) parameter to `close_tab` keybind/apprt
action.
- Keybinds will default to `this` if not specified, eliminating backward
compatibility issues (`keybind=x=close_tab` ===
`keybind=x=close_tab:this`).
- Remove `close_other_tabs` keybind and apprt action.

Replaces #8380
2025-08-26 09:28:16 -07:00
Aindriú Mac Giolla Eoin
9c725187e1 Updating two strings 2025-08-26 15:47:16 +01:00
trag1c
4630369f87 i18n: update Ukrainian translation (#8379) 2025-08-26 10:44:58 +02:00
Mitchell Hashimoto
5b0801cbc9 osc 9: allow single character notifications (#8396) 2025-08-25 19:35:51 -07:00
Jeffrey C. Ollie
3320a081b4 osc 9: allow single character notifications 2025-08-25 19:27:27 -05:00
trag1c
9a56e77937 i18n: add new translations in fr_FR (#8378)
relative to #8344
2025-08-26 00:32:56 +02:00
Marija Gjorgjieva Gjondeva
a471bac782 Update macedonian translation strings (#8392)
Updated new translation strings for version 1.2.

#8344
2025-08-25 21:03:43 +00:00
Jeffrey C. Ollie
14a3765916 cli: show colors in +list-colors if possible (#8393)
Fixes #8386

This is a fairly simple implementaion, there's no interactivity or
searching. It will adapt the number of columns to the available width of
the display though.

Will fallback to a plain text dump if there's no tty or the `--plain`
argument is specified on the CLI.

<img width="2112" height="1278" alt="image"
src="https://github.com/user-attachments/assets/0dbeec72-2092-4ed5-b1ed-0df43e5c64a3"
/>
2025-08-25 15:51:03 -05:00
Jeffrey C. Ollie
ca06b95f65 cli: show colors in +list-colors if possible
Fixes #8386

This is a fairly simple implementaion, there's no interactivity or
searching. It will adapt the number of columns to the available width of
the display though.

Will fallback to a plain text dump if there's no tty or the `--plain`
argument is specified on the CLI.
2025-08-25 15:00:32 -05:00
trag1c
d659bdcfdd i18n: add missing de_DE translations (#8351)
Part of #8344.
2025-08-25 21:37:46 +02:00
Volodymyr Chernetskyi
754bb4011a i18n: add missing coma 2025-08-25 19:59:46 +02:00
Volodymyr Chernetskyi
11d845ce17 i18n: shorten Ukrainian translations 2025-08-25 19:57:08 +02:00
Robin
c629ea674c i18n: add missing de_DE translations 2025-08-25 19:38:54 +02:00
Volodymyr Chernetskyi
f8d69e5baf i18n: use native Ukrainian word for "config" 2025-08-25 19:33:19 +02:00
Volodymyr Chernetskyi
c396c25898 i18n: shorten Ukrainian translation for "split" 2025-08-25 19:23:26 +02:00
Jeffrey C. Ollie
e98e868265 close-tab: style-fixes 2025-08-25 11:56:17 -05:00
Jeffrey C. Ollie
52a25e9c69 parameterize close_tab
- Add mode (`this`/`other`) parameter to `close_tab` keybind/apprt action.
- Keybinds will default to `this` if not specified, eliminating backward
  compatibility issues (`keybind=x=close_tab` === `keybind=x=close_tab:this`).
- Remove `close_other_tabs` keybind and apprt action.
2025-08-25 11:00:26 -05:00
Mitchell Hashimoto
8aa0b4c92a gtk-ng: fix setting/unsetting of urgency (#8376)
- Don't set urgency on windows that are the topmost window.
- Turn off urgency on windows that become the topmost window.

Fixes #8373
2025-08-25 07:20:15 -07:00
Jeffrey C. Ollie
8a14f21325 gtk-ng: fix setting/unsetting of urgency
- Don't set urgency on windows that are the topmost window.
- Turn off urgency on windows that become the topmost window.

Fixes #8373
2025-08-25 09:06:29 -05:00
Volodymyr Chernetskyi
5c03ff8165 i18n: update Ukrainian translation 2025-08-24 22:58:10 +02:00
trag1c
400576f0b0 i18n: Add missing ca_ES translations (#8371)
Part of #8344
2025-08-24 19:52:08 +02:00
KristoferSoler
c9199f2ba2 i18n: Add missing ca_ES translations
Part of #8344
2025-08-24 19:32:59 +02:00
trag1c
48120f8b6c fix: update Spanish translations and revision date in es_BO.UTF-8.po (#8374)
8 new strings added
2025-08-24 17:05:13 +02:00
Mitchell Hashimoto
27ed58252d Close other tabs feature on Mac. (#8363)
Supporting command line, file menu and keybindings. Default mac shortcut
of `super + alt + o` (other)

Not able to test on Linux so excluding `close_other_tabs` from `gtk` for
now
2025-08-24 08:00:11 -07:00
jamylak
c26323d697 Close other tabs feature on Mac.
Supporting command line, file menu and keybindings.
Default mac shortcut of `super + alt + o` (other)

Not able to test on Linux so excluding `close_other_tabs` from `gtk` for now
make a default short cut for close other tabs
2025-08-24 07:55:08 -07:00
Mitchell Hashimoto
13425b4881 Update iTerm2 colorschemes (#8368)
Upstream revision:
e4c0090a65
2025-08-24 07:02:50 -07:00
Mitchell Hashimoto
9ff716642e nix: update zon2nix (#8370)
- Builds with Zig 0.15 now (but still works just fine with Zig 0.14
projects).
- Fixes a double-free if nix-prefetch-git can't be found or errors out
- Adds support for generating Flatpak package metadata natively.
2025-08-24 07:02:38 -07:00
MiguelElGallo
c57a84a6de fix: update Spanish translations for window split terminology 2025-08-24 16:26:41 +03:00
MiguelElGallo
42b1ff70d1 fix: update Spanish translations and revision date in es_BO.UTF-8.po 2025-08-24 15:52:13 +03:00
Balázs Szücs
95bc181c98 Add hu_HU for Hungarian locale (#7560)
## Description of changes

Added Hungarian locale files, and corresponding translation

For the translation I mainly relied on my native skills, double checked
my work using LLMs.

Copilot generated summary:

This pull request introduces Hungarian language support to the
application by adding translations and updating the locale
configurations. The most important changes include the addition of
Hungarian translations in the `.po` file and registering the new locale
in the application's supported locales.

### Hungarian Language Support:

* Added Hungarian translations for various UI elements and messages in
the `po/hu_HU.UTF-8.po` file. This includes translations for prompts,
dialogs, menus, and other interface components.
* Updated the supported locales list in `src/os/i18n.zig` to include
`hu_HU.UTF-8`, enabling Hungarian as an available language option.

## Picture(s) of the translation


![image](https://github.com/user-attachments/assets/60f47f11-d55e-4408-889b-5b44ecaffc23)
2025-08-24 09:10:21 +00:00
Jeffrey C. Ollie
a18332828a nix: update zon2nix
- Builds with Zig 0.15 now (but still works just fine with Zig
  0.14 projects).
- Fixes a double-free if nix-prefetch-git can't be found or errors out
- Adds support for generating Flatpak package metadata natively.
2025-08-23 21:31:00 -05:00
mitchellh
00e4a90699 deps: Update iTerm2 color schemes 2025-08-24 00:14:50 +00:00
Mitchell Hashimoto
7622d2662d feat: add option to disable the "Reloaded the configuration" notification (#8366)
Redo of #8085 since I can't push that branch.
2025-08-23 12:59:09 -07:00
Mitchell Hashimoto
e1d4c37996 apprt/gtk-ng: some style changes for toast 2025-08-23 12:51:52 -07:00
dy0gu
1b8dd234b0 Merge branch 'main' of https://github.com/Elyptica/ghostty 2025-08-23 12:50:53 -07:00
dy0gu
43e010bf47 feat: add option to disable the "Reloaded the configuration" notification 2025-08-23 12:50:53 -07:00
Mitchell Hashimoto
062d596c0a terminal: fix use-after-free in exec (#8358)
This was only an issue on Linux, as MacOS' command is reallocated and
rewritten. We hit this using embedded Ghostty w/o a login shell :p
2025-08-23 12:45:44 -07:00
Kirwiisp
a3be474d28 add new translations 2025-08-23 21:03:25 +02:00
trag1c
b347585e27 i18n: update norwegian translations (#8365)
Updated the norwegian translation file to include the new translations
(cc @Uzaaft).
2025-08-23 19:37:15 +02:00
hanna
1aa59cf63d i18n: update choice selection prompt text 2025-08-23 13:19:13 -04:00
hanna
bd4e9b96bf i18n: update translation metadata 2025-08-23 12:53:05 -04:00
trag1c
59fd366264 i18n: Update Turkish translations (#8350)
Part of #8344
2025-08-23 18:48:35 +02:00
hanna
78f05ec96c i18n: adjust wording in translation 2025-08-23 12:48:32 -04:00
trag1c
0d536d447c i18n: Update Hebrew translation (#8362)
issue: #8344
2025-08-23 18:47:56 +02:00
hanna
4f4c06967a i18n: update norwegian translations 2025-08-23 12:43:02 -04:00
Emir SARI
f6f2a85256 i18n: Update Turkish translations
Signed-off-by: Emir SARI <emir_sari@icloud.com>
2025-08-23 19:27:19 +03:00
Gal
c181fc4fbf i18n: Update Hebrew translation
issue: #8344
2025-08-23 13:53:29 +03:00
Cheru Berhanu
d854ecd374 terminal: test execCommand w/ freed config 2025-08-22 15:55:08 -07:00
Cheru Berhanu
652f6f1deb terminal: fix use-after-free in exec
This was only an issue on Linux, as MacOS' command is reallocated and
rewritten. We hit this using embedded Ghostty w/o a login shell :p
2025-08-22 15:22:54 -07:00
Mitchell Hashimoto
c014dd79f6 terminal: fix build with -Di18n=false (#8359)
canonicalizeLocale should return a null-terminated string, and didn't
previously.

Compiler output:
```
src/os/i18n.zig:139:45: error: expected type 'error{NoSpaceLeft}![:0]const u8', found '[]const u8'
    if (comptime !build_config.i18n) return locale;
                                            ^~~~~~
src/os/i18n.zig:139:45: note: destination pointer requires '0' sentinel
src/os/i18n.zig:138:21: note: function return type declared here
) error{NoSpaceLeft}![:0]const u8 {
  ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
```
2025-08-22 14:54:11 -07:00
Cheru Berhanu
292efec669 terminal: fix build with -Di18n=false
canonicalizeLocale should return a null-terminated string, and didn't previously.

Compiler output:
```
src/os/i18n.zig:139:45: error: expected type 'error{NoSpaceLeft}![:0]const u8', found '[]const u8'
    if (comptime !build_config.i18n) return locale;
                                            ^~~~~~
src/os/i18n.zig:139:45: note: destination pointer requires '0' sentinel
src/os/i18n.zig:138:21: note: function return type declared here
) error{NoSpaceLeft}![:0]const u8 {
  ~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~
```
2025-08-22 14:47:04 -07:00
Mitchell Hashimoto
0c722b0e3d macos: if parent window is fullscreen, new window is fullscreen too (#8355)
Fixes #8229

This was a regression. 

The discussion noted in #8229 requests we create a new window on the
non-fullscreen desktop but that isn't how Ghostty has behaved
historically. I bisected back and tried 1.1.3 as well and we always
created a new fullscreen window when the parent was fullscreen.

This behavior matches iTerm2. Its noteworthy that native tabbing and
Apple apps such as Terminal.app and Safari do NOT do this. For both of
these, new window creates a _tab_ when in fullscreen. I don't think
that's particularly desirable, though.
2025-08-22 14:41:00 -07:00
Mitchell Hashimoto
54f8dff308 macos: if parent window is fullscreen, new window is fullscreen too
Fixes #8229

This was a regression. 

The discussion noted in #8229 requests we create a new window on the 
non-fullscreen desktop but that isn't how Ghostty has behaved
historically. I bisected back and tried 1.1.3 as well and we always
created a new fullscreen window when the parent was fullscreen.

This behavior matches iTerm2. Its noteworthy that native tabbing and
Apple apps such as Terminal.app and Safari do NOT do this. For both of
these, new window creates a _tab_ when in fullscreen. I don't think
that's particularly desirable, though.
2025-08-22 14:31:50 -07:00
Mitchell Hashimoto
60e077b651 deps: update z2d to v0.7.2 (#8354)
Release notes at:
  https://github.com/vancluever/z2d/blob/v0.7.2/CHANGELOG.md

This is mostly a bugfix release for text rendering, including a fix for
an invalid free flagged as:

https://github.com/vancluever/z2d/security/advisories/GHSA-v7f4-f3hm-282w

Note that the invalid free affects the new in-library text rendering
only and as such is likely not in use in Ghostty.

I'm anticipating *maybe* one more release after this ahead of Ghostty
1.2.0, depending on if I find any more bugs, but close to around the
release I am planning a freeze to ensure a clean versioned release
continues to be set.
2025-08-22 10:51:26 -07:00
Chris Marchesi
50fe12e85c deps: update z2d to v0.7.2
Release notes at:
  https://github.com/vancluever/z2d/blob/v0.7.2/CHANGELOG.md

This is mostly a bugfix release for text rendering, including a fix for
an invalid free flagged as:
  https://github.com/vancluever/z2d/security/advisories/GHSA-v7f4-f3hm-282w

Note that the invalid free affects the new in-library text rendering
only and as such is likely not in use in Ghostty.

I'm anticipating *maybe* one more release after this ahead of Ghostty
1.2.0, depending on if I find any more bugs, but close to around the
release I am planning a freeze to ensure a clean versioned release
continues to be set.
2025-08-22 10:30:01 -07:00
Mitchell Hashimoto
f4abecefe4 core: avoid possible deadlock in right-click-action paste actions (#8352)
Fixes #8313

The clipboard request flow can result in the apprt immediately
completing the request which itself grabs a lock. For pastes, we should
yield the lock during the clipboard request.

GTK is always async so this worked there, but we want to be resilient to
any apprt behavior here.
2025-08-22 09:52:51 -07:00
Mitchell Hashimoto
db60e981d1 core: avoid possible deadlock in right-click-action paste actions
Fixes #8313

The clipboard request flow can result in the apprt immediately
completing the request which itself grabs a lock. For pastes, we should
yield the lock during the clipboard request.

GTK is always async so this worked there, but we want to be resilient to
any apprt behavior here.
2025-08-22 09:49:18 -07:00
Mitchell Hashimoto
3859f50b88 Work around strange SwiftUI behavior in "older" macOSen. (might fix #7690) (#8026)
The Quick Terminal would not appear anymore, as somewhere in the
framework the Quick Terminal Window's geometry gets corrupted when the
window is added to the UI.

This works around by caching the windows geometry and reusing it
afterwards

This might fix #7690
2025-08-22 09:31:11 -07:00
Tobias Pape
4fdf0b687e typo found typos, typo may keep them 2025-08-22 09:19:25 -07:00
Tobias Pape
85cba70c2e Work around strange SwiftUI behavior in "older" macOSen.
The Quick Terminal would not appear anymore, as somewhere
in the framework the Quick Terminal Window's geometry
gets corrupted when the window is added to the UI.

This works around by caching the windows geometry and
reusing it afterwards
2025-08-22 09:17:49 -07:00
Mitchell Hashimoto
5bff354e96 fix: copy_url_to_clipboard copies full OSC8 URL instead of single cha… (#7551)
fix: copy_url_to_clipboard for OSC8 hyperlinks

OSC8 links were only detected when exact platform-specific modifiers
were held (Cmd on macOS, Ctrl on Linux), but copy_url_to_clipboard
should work with either. Additionally, OSC8 links were using
selectionString() which gets visible text instead of the actual URI. Now
we use osc8URI() for OSC8 links and fall back to selectionString() for
regex-detected links.

Fixes #7499
2025-08-22 09:14:36 -07:00
Mitchell Hashimoto
6d9cac5ffc macOS: order out alert sheets to avoid Stage Manager focus loss (#8348)
On macOS, when using stage manager, ghostty loses focus when a running
process is terminated.

This aims to fix #8336 which has reappeared since the previously fixed
#5108.

Opened a new pr following the closure of the previous #8343
2025-08-22 09:12:01 -07:00
Alex
6248030426 removed boolean logic, reverted back to ctrlOrSuper call 2025-08-22 09:10:53 -07:00
Alex
6708229a7e Removed boolean logic, reverted back to ctrlOrSuper call 2025-08-22 09:09:06 -07:00
Alex
91f973afdb Merge branch 'fix-copy-url' into fix-copy-url-minimal
Remove boolean logic as it is not needed, revert back to using ctrlOrSuper call in linkAtPos
2025-08-22 09:09:06 -07:00
Alex Straight
14f5a879a9 fix: make regular URLs work with either ctrl or super modifiers 2025-08-22 09:09:06 -07:00
Alex
eaa81be051 Merge branch 'main' into fix-copy-url 2025-08-22 09:09:06 -07:00
Alex
83b573aed7 remove commented out block 2025-08-22 09:09:06 -07:00
Alex
c78fb0f895 Removed boolean logic, reverted back to ctrlOrSuper call 2025-08-22 09:09:06 -07:00
Alex
5441578f08 Merge branch 'main' into fix-copy-url 2025-08-22 09:09:06 -07:00
Alex
c142473405 Merge branch 'main' into fix-copy-url
Merging main into fix-copy-url
2025-08-22 09:09:06 -07:00
Alex Straight
fc1307e939 fix: make regular URLs work with either ctrl or super modifiers 2025-08-22 09:09:06 -07:00
Alex Straight
5836dc4ce6 fix: copy_url_to_clipboard copies full OSC8 URL instead of single character
OSC8 links were only detected when exact platform-specific modifiers were held (Cmd on macOS, Ctrl on Linux), but copy_url_to_clipboard should work with either. Additionally, OSC8 links were using selectionString() which gets visible text instead of the actual URI. Now we use osc8URI() for OSC8 links and fall back to selectionString() for regex-detected links.

Fixes #7491
2025-08-22 09:09:06 -07:00
Alex Straight
93c2400bf4 fix: copy_url_to_clipboard copies full OSC8 URL instead of single character
OSC8 links were only detected when exact platform-specific modifiers were held (Cmd on macOS, Ctrl on Linux), but copy_url_to_clipboard should work with either. Additionally, OSC8 links were using selectionString() which gets visible text instead of the actual URI. Now we use osc8URI() for OSC8 links and fall back to selectionString() for regex-detected links.

Fixes #7491
2025-08-22 09:09:05 -07:00
Moeeze Hassan
f4009721a1 macOS: order out alert sheets to avoid Stage Manager focus loss
Signed-off-by: Moeeze Hassan <fammas.maz@gmail.com>
2025-08-22 09:05:20 -07:00
Alan Moyano
a479c9b2af i18n: add missing es_AR translations (#8347)
Part of #8344
2025-08-22 15:59:04 +02:00
Aindriú Mac Giolla Eoin
35102ddb5a Updating Irish translation for Ghostty 1.2 2025-08-22 14:53:56 +01:00
Mitchell Hashimoto
298f11166d macos: move activation to after new window/tab is created (#8338)
This is a follow-up to #8064, moving the activation into the async block
such that it happens after the window is created. As discussed in #8064,
this is necessary to bring only the newly created window to the front,
rather than both the previous main window and the new window.

Also made the same change for the new tab action, which also needs to
activate in case it was triggered from the dock menu or a global
keybind.

Finally, I removed the activations within AppDelegate that are redundant
now that TerminalController itself takes care of activating.
2025-08-21 15:38:40 -07:00
Daniel Wennberg
7d60c7c75b macos: move activation to after new window/tab is created 2025-08-21 15:16:20 -07:00
Mitchell Hashimoto
056ccc9818 macOS: update sparkle (#8337)
There aren't any noteworthy changes here we're just using a very old
version. Additionally, our CI was using... different versions!
2025-08-21 14:56:50 -07:00
Mitchell Hashimoto
3ef6de4ffa macos: in new_window action, activate App (#8064)
> [!NOTE]  
> The change might have been intentional, and so I lack context. I
mention two ways to fix below, this PR implements the first possible
fix.

This change makes sure that the new window is focused and visible.

When commit 33d128bcff removed the
TerminalManager class and moved its functionality into
TerminalController, it accidentally removed app activation for windows
triggered by global keybinds.

How the bug works:

1. Menu actions (like File → New Window) call AppDelegate.newWindow()
which: 2. Calls TerminalController.newWindow() 3. AND explicitly calls
NSApp.activate(ignoringOtherApps: true) in the AppDelegate
4. Global keybind actions trigger ghosttyNewWindow() notification
handler which:
      5. Only calls TerminalController.newWindow()
      6. Does NOT call NSApp.activate(ignoringOtherApps: true)
7. While TerminalController.newWindow() does call
NSApp.activate(ignoringOtherApps: true) internally, this call happens
before the async dispatch that shows the window, so the activation
occurs but the window isn't focused when it's actually shown.
8. In the old TerminalManager.newWindow(), the activation happened
immediately before the async dispatch, ensuring proper timing for window
focus.

To see the bug in action:
- run recent Ghostty `main`
- set up a global keybind for `new_window`
- focus some other window
- trigger keybind
- notice that Ghostty doesn't come to the foreground, but when manually
switching to Ghostty you will see that the new window _was_ created

The fix would be to either move the NSApp.activate() call back into
TerminalController.newWindow(), as it was for TerminalManager, or add
the activation call to the notification handlers in AppDelegate.
2025-08-21 14:09:44 -07:00
Mitchell Hashimoto
1ce56a12fa update sparkle
There aren't any noteworthy changes here we're just using a very old
version. Additionally, our CI was using... different versions!
2025-08-21 14:08:32 -07:00
Aljoscha Krettek
f736ee8865 macos: in new_window action, activate App
This change makes sure that the new window is focused and visible.

When commit 33d128bcff removed the
TerminalManager class and moved its functionality into
TerminalController, it accidentally removed app activation for windows
triggered by global keybinds.

How the bug works:

   1. Menu actions (like File → New Window) call AppDelegate.newWindow()
      which:
      2. Calls TerminalController.newWindow()
      3. AND explicitly calls NSApp.activate(ignoringOtherApps: true) in
         the AppDelegate
   4. Global keybind actions trigger ghosttyNewWindow() notification
      handler which:
      5. Only calls TerminalController.newWindow()
      6. Does NOT call NSApp.activate(ignoringOtherApps: true)
   7. While TerminalController.newWindow() does call
      NSApp.activate(ignoringOtherApps: true) internally, this call
      happens before the async dispatch that shows the window, so the
      activation occurs but the window isn't focused when it's actually
      shown.
   8. In the old TerminalManager.newWindow(), the activation happened
      immediately before the async dispatch, ensuring proper timing for
      window focus.

The fix would be to either move the NSApp.activate() call back into
TerminalController.newWindow(), as it was for TerminalManager, or add
the activation call to the notification handlers in AppDelegate.
2025-08-21 14:03:00 -07:00
Mitchell Hashimoto
40105e1c7e cli: add filtering hotkey to list_themes (#8082)
## Summary

Implements the theme filtering hotkey as requested in #7930. 

## Implementation

- Adds 'f' hotkey to cycle through filtering options: all, dark, and
light.
- Integrates with existing search functionality.
- Preserves CLI `--color` flag behavior for initial state.
- Updates help menu with the new hotkey. 

**NOTE**: I noticed another PR
[#8079](https://github.com/ghostty-org/ghostty/pull/8079) opened. I
started this implementation independently prior and don't want to step
on any toes. Happy to collaborate or defer to maintainers.
2025-08-21 14:01:50 -07:00
Mitchell Hashimoto
c110c0f76d gtk-ng: add a helper to reduce boilerplate in GTK IPC (#8306) 2025-08-21 13:34:53 -07:00
Mitchell Hashimoto
c675896595 ci: add 30 minute timeout to valgrind (#8333)
It usually takes less than a few minutes right now. Something is wrong
if it takes more than that.
2025-08-21 11:46:07 -07:00
Mitchell Hashimoto
53c2f915d8 gtk-ng: allow XKB remaps for non-writing-system keys (#8330)
Compromise solution to #7356

XKB is naughty. It's really really naughty. I don't understand why we
didn't just kill XKB with hammers during the Wayland migration and
change it for something much better. I don't understand why we're
content with what amounts to an OS-level software key remapper that
completely jumbles information about original physical key codes in
order to fake keyboard layouts, and not just let users who really want
to remap keys use some sort of evdev or udev-based mapper program.

In a sane system like macOS, the "c" key is always the "c" key, but it's
understood to produce the Unicode character "ц" when using a Russian
layout. XKB defies sanity, and just pretends that your "c" key is
actually a "ц" key instead, and so when you ask for the keybind "Ctrl+C"
it just shrugs in apathy (#7309). And so, we took matters into our own
hands and interpreted hardware keycodes ourselves.

But then, a *lot* of people have the ingrained muscle memory of swapping
Escape with Caps Lock so that it is easier to hit. We respect that. In a
sane system, they would use a remapper that actually makes the system
think you've hit the Escape key when in reality you've hit the Caps Lock
key, so in all intents and purposes to the OS and any app developer,
these two just have their wires swapped. But not on Linux. Somehow this
and the aforementioned case should be treated by the same key transform
algorithm, which is completely diabolical.

As a result, we have to settle for a compromise that truly satisfies
neither party — by allowing XKB remaps for keys that don't really change
depending on the layout.

The Linux input stack besets all hopes and aspirations.
2025-08-21 11:45:48 -07:00
Mitchell Hashimoto
795c745491 ci: add 30 minute timeout to valgrind
It usually takes less than a few minutes right now. Something is wrong
if it takes more than that.
2025-08-21 11:44:58 -07:00
Leah Amelia Chen
534aa508d6 gtk-ng: allow XKB remaps for non-writing-system keys
Compromise solution to #7356

XKB is naughty. It's really really naughty. I don't understand why we
didn't just kill XKB with hammers during the Wayland migration and change
it for something much better. I don't understand why we're content with
what amounts to an OS-level software key remapper that completely jumbles
information about original physical key codes in order to fake keyboard
layouts, and not just let users who really want to remap keys use some
sort of evdev or udev-based mapper program.

In a sane system like macOS, the "c" key is always the "c" key, but it's
understood to produce the Unicode character "ц" when using a Russian
layout. XKB defies sanity, and just pretends that your "c" key is
actually a "ц" key instead, and so when you ask for the keybind "Ctrl+C"
it just shrugs in apathy (#7309). And so, we took matters into our own
hands and interpreted hardware keycodes ourselves.

But then, a *lot* of people have the ingrained muscle memory of swapping
Escape with Caps Lock so that it is easier to hit. We respect that.
In a sane system, they would use a remapper that actually makes the
system think you've hit the Escape key when in reality you've hit the
Caps Lock key, so in all intents and purposes to the OS and any app
developer, these two just have their wires swapped. But not on Linux.
Somehow this and the aforementioned case should be treated by the same
key transform algorithm, which is completely diabolical.

As a result, we have to settle for a compromise that truly satisfies
neither party — by allowing XKB remaps for keys that don't really change
depending on the layout.

The Linux input stack besets all hopes and aspirations.
2025-08-22 02:02:11 +08:00
ClearAspect
f7994e6412 fix: correct the cursor Y position value exposed to shader uniforms
Fix for discussion #8113

The cursor Y position value exposed to the shader uniforms was
incorrectly calculated. As per the doc in cell_text.v.glsl: In order to
get the top left of the glyph, we compute an offset based on the
bearings. The Y bearing is the distance from the bottom of the cell to
the top of the glyph, so we subtract it from the cell height to get the
y offset.

This calculation was mistakenly left out of the original code.

This will ensure that the custom shaders using
iCurrentCursor/iPreviousCursor get the correct Y coordinate representing
the top-left corner of the cursor rectangle, matching the documented
uniform behavior
2025-08-21 10:08:49 -07:00
Jeffrey C. Ollie
108260100c gtk-ng: add a helper to reduce boilerplate in GTK IPC 2025-08-20 14:53:17 -05:00
Anthony
db2984de6e cli: update var name 2025-07-30 14:29:38 +10:00
Anthony
487b1d72ab cli: add filtering hotkey to list_themes 2025-07-27 17:45:12 +10:00
88 changed files with 3768 additions and 1468 deletions

View File

@@ -36,13 +36,13 @@ jobs:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16

View File

@@ -68,7 +68,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -223,7 +223,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@@ -83,13 +83,13 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
@@ -146,7 +146,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -311,7 +311,7 @@ jobs:
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@@ -107,12 +107,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -181,7 +181,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.6.4
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -401,7 +401,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
@@ -581,7 +581,7 @@ jobs:
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
SPARKLE_VERSION: 2.7.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle

View File

@@ -13,6 +13,7 @@ jobs:
- build-bench
- build-dist
- build-flatpak
- build-freebsd
- build-linux
- build-linux-libghostty
- build-nix
@@ -20,7 +21,6 @@ jobs:
- build-macos
- build-macos-matrix
- build-windows
- flatpak-check-zig-cache
- test
- test-gtk
- test-gtk-ng
@@ -70,14 +70,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -101,14 +101,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -137,14 +137,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -166,14 +166,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -199,14 +199,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -243,14 +243,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -378,7 +378,7 @@ jobs:
mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
@@ -474,14 +474,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -519,14 +519,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -568,14 +568,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -616,14 +616,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -674,12 +674,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -702,12 +702,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -729,12 +729,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -756,12 +756,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -783,12 +783,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -810,12 +810,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -844,12 +844,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -871,12 +871,12 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -906,14 +906,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -954,33 +954,6 @@ jobs:
build-args: |
DISTRO_VERSION=13
flatpak-check-zig-cache:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-xsm
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
useDaemon: false # sometimes fails on short jobs
- name: Check Flatpak Zig Dependencies
run: nix develop -c ./flatpak/build-support/check-zig-cache.sh
flatpak:
if: github.repository == 'ghostty-org/ghostty'
name: "Flatpak"
@@ -996,7 +969,7 @@ jobs:
- arch: aarch64
runner: namespace-profile-ghostty-md-arm64
runs-on: ${{ matrix.variant.runner }}
needs: [flatpak-check-zig-cache, test]
needs: test
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: flatpak/flatpak-github-actions/flatpak-builder@10a3c29f0162516f0f68006be14c92f34bd4fa6c # v6.5
@@ -1010,6 +983,7 @@ jobs:
valgrind:
if: github.repository == 'ghostty-org/ghostty'
runs-on: namespace-profile-ghostty-lg
timeout-minutes: 30
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
@@ -1019,14 +993,14 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -1042,3 +1016,57 @@ jobs:
- name: valgrind
run: |
nix develop -c zig build test-valgrind
build-freebsd:
name: Build on FreeBSD
needs: test
runs-on: namespace-profile-mitchellh-sm-systemd
strategy:
matrix:
release:
- "14.3"
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108
steps:
- name: Checkout Ghostty
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Start SSH
run: |
sudo systemctl start ssh
- name: Set up FreeBSD VM
uses: vmactions/freebsd-vm@05856381fab64eeee9b038a0818f6cec649ca17a # v1.2.3
with:
release: ${{ matrix.release }}
copyback: false
usesh: true
prepare: |
pkg install -y \
devel/blueprint-compiler \
devel/gettext \
devel/git \
devel/pkgconf \
graphics/wayland \
lang/zig \
security/ca_root_nss \
textproc/hs-pandoc \
x11-fonts/jetbrains-mono \
x11-toolkits/libadwaita \
x11-toolkits/gtk40 \
x11-toolkits/gtk4-layer-shell
run: |
zig env
- name: Run tests
shell: freebsd {0}
run: |
cd $GITHUB_WORKSPACE
zig build test
- name: Build GTK-NG app runtime
shell: freebsd {0}
run: |
cd $GITHUB_WORKSPACE
zig build
./zig-out/bin/ghostty +version

View File

@@ -22,14 +22,14 @@ jobs:
fetch-depth: 0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
with:
path: |
/nix
/zig
- name: Setup Nix
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
@@ -50,8 +50,6 @@ jobs:
if ! git diff --exit-code build.zig.zon; then
nix develop -c ./nix/build-support/check-zig-cache.sh --update
nix develop -c ./nix/build-support/check-zig-cache.sh
nix develop -c ./flatpak/build-support/check-zig-cache.sh --update
nix develop -c ./flatpak/build-support/check-zig-cache.sh
fi
# Verify the build still works. We choose an arbitrary build type

View File

@@ -169,6 +169,7 @@
/po/es_BO.UTF-8.po @ghostty-org/es_BO
/po/es_AR.UTF-8.po @ghostty-org/es_AR
/po/fr_FR.UTF-8.po @ghostty-org/fr_FR
/po/hu_HU.UTF-8.po @ghostty-org/hu_HU
/po/id_ID.UTF-8.po @ghostty-org/id_ID
/po/ja_JP.UTF-8.po @ghostty-org/ja_JP
/po/mk_MK.UTF-8.po @ghostty-org/mk_MK

View File

@@ -1,9 +1,9 @@
# Ghostty Development Process
# Contributing to Ghostty
This document describes the development process for Ghostty. It is intended for
anyone considering opening an **issue** or **pull request**. If in doubt,
please open a [discussion](https://github.com/ghostty-org/ghostty/discussions);
we can always convert that to an issue later.
This document describes the process of contributing to Ghostty. It is intended
for anyone considering opening an **issue**, **discussion** or **pull request**.
For people who are interested in developing Ghostty and technical details behind
it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
> [!NOTE]
>
@@ -49,13 +49,16 @@ Please be respectful to maintainers and disclose AI assistance.
## Quick Guide
**I'd like to contribute!**
### I'd like to contribute!
All issues are actionable. Pick one and start working on it. Thank you.
If you need help or guidance, comment on the issue. Issues that are extra
friendly to new contributors are tagged with "contributor friendly".
[All issues are actionable](#issues-are-actionable). Pick one and start
working on it. Thank you. If you need help or guidance, comment on the issue.
Issues that are extra friendly to new contributors are tagged with
["contributor friendly"].
**I'd like to translate Ghostty to my language!**
["contributor friendly"]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22
### I'd like to translate Ghostty to my language!
We have written a [Translator's Guide](po/README_TRANSLATORS.md) for
everyone interested in contributing translations to Ghostty.
@@ -64,25 +67,39 @@ and you can submit pull requests directly, although please make sure that
our [Style Guide](po/README_TRANSLATORS.md#style-guide) is followed before
submission.
**I have a bug!**
### I have a bug! / Something isn't working!
1. Search the issue tracker and discussions for similar issues.
2. If you don't have steps to reproduce, open a discussion.
3. If you have steps to reproduce, open an issue.
1. Search the issue tracker and discussions for similar issues. Tip: also
search for [closed issues] and [discussions] — your issue might have already
been fixed!
2. If your issue hasn't been reported already, open an ["Issue Triage" discussion]
and make sure to fill in the template **completely**. They are vital for
maintainers to figure out important details about your setup. Because of
this, please make sure that you _only_ use the "Issue Triage" category for
reporting bugs — thank you!
**I have an idea for a feature!**
[closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed
[discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed
["Issue Triage" discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage
1. Open a discussion.
### I have an idea for a feature!
**I've implemented a feature!**
Open a discussion in the ["Feature Requests, Ideas" category](https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas).
1. If there is an issue for the feature, open a pull request.
### I've implemented a feature!
1. If there is an issue for the feature, open a pull request straight away.
2. If there is no issue, open a discussion and link to your branch.
3. If you want to live dangerously, open a pull request and hope for the best.
3. If you want to live dangerously, open a pull request and
[hope for the best](#pull-requests-implement-an-issue).
**I have a question!**
### I have a question!
1. Open a discussion or use Discord.
Open an [Q&A discussion], or join our [Discord Server] and ask away in the
`#help` channel.
[Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a
[Discord Server]: https://discord.gg/ghostty
## General Patterns
@@ -120,209 +137,3 @@ pull request will be accepted with a high degree of certainty.
> **Pull requests are NOT a place to discuss feature design.** Please do
> not open a WIP pull request to discuss a feature. Instead, use a discussion
> and link to your branch.
# Developer Guide
> [!NOTE]
>
> **The remainder of this file is dedicated to developers actively
> working on Ghostty.** If you're a user reporting an issue, you can
> ignore the rest of this document.
## Including and Updating Translations
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
## Checking for Memory Leaks
While Zig does an amazing job of finding and preventing memory leaks,
Ghostty uses many third-party libraries that are written in C. Improper usage
of those libraries or bugs in those libraries can cause memory leaks that
Zig cannot detect by itself.
### On Linux
On Linux the recommended tool to check for memory leaks is Valgrind. The
recommended way to run Valgrind is via `zig build`:
```sh
zig build run-valgrind
```
This builds a Ghostty executable with Valgrind support and runs Valgrind
with the proper flags to ensure we're suppressing known false positives.
You can combine the same build args with `run-valgrind` that you can with
`run`, such as specifying additional configurations after a trailing `--`.
## Input Stack Testing
The input stack is the part of the codebase that starts with a
key event and ends with text encoding being sent to the pty (it
does not include _rendering_ the text, which is part of the
font or rendering stack).
If you modify any part of the input stack, you must manually verify
all the following input cases work properly. We unfortunately do
not automate this in any way, but if we can do that one day that'd
save a LOT of grief and time.
Note: this list may not be exhaustive, I'm still working on it.
### Linux IME
IME (Input Method Editors) are a common source of bugs in the input stack,
especially on Linux since there are multiple different IME systems
interacting with different windowing systems and application frameworks
all written by different organizations.
The following matrix should be tested to ensure that all IME input works
properly:
1. Wayland, X11
2. ibus, fcitx, none
3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
> [!NOTE]
>
> This is a **work in progress**. I'm still working on this list and it
> is not complete. As I find more test cases, I will add them here.
#### Dead Key Input
Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
1. Launch Ghostty
2. Press `'`
3. Press `a`
4. Verify that `á` is displayed
Note that the dead key may or may not show a preedit state visually.
For ibus and fcitx it does but for the "none" case it does not. Importantly,
the text should be correct when it is sent to the pty.
We should also test canceling dead key input:
1. Launch Ghostty
2. Press `'`
3. Press escape
4. Press `a`
5. Verify that `a` is displayed (no diacritic)
#### CJK Input
Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
exact layout doesn't matter.
1. Launch Ghostty
2. Press `Ctrl+Shift` to switch to "Hiragana"
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
4. Press `Enter`
5. Verify that `こん` is displayed in the terminal.
We should also test switching input methods while preedit is active, which
should commit the text:
1. Launch Ghostty
2. Press `Ctrl+Shift` to switch to "Hiragana"
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
4. Press `Ctrl+Shift` to switch to another layout (any)
5. Verify that `こん` is displayed in the terminal as committed text.
## Nix Virtual Machines
Several Nix virtual machine definitions are provided by the project for testing
and developing Ghostty against multiple different Linux desktop environments.
Running these requires a working Nix installation, either Nix on your
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
requirements for macOS are detailed below.
VMs should only be run on your local desktop and then powered off when not in
use, which will discard any changes to the VM.
The VM definitions provide minimal software "out of the box" but additional
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
with `common` or `create`.
3. The VM will build and then launch. Depending on the speed of your system, this
can take a while, but eventually you should get a new VM window.
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
writable by the VM user, so be careful!
### macOS
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
blog post for more information about the Linux builder and how to tune the performance.
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
above to launch a VM.
### Custom VMs
To easily create a custom VM without modifying the Ghostty source, create a new
directory, then create a file called `flake.nix` with the following text in the
new directory.
```
{
inputs = {
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
ghostty.url = "github:ghostty-org/ghostty";
};
outputs = {
nixpkgs,
ghostty,
...
}: {
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
nixpkgs = nixpkgs;
system = "x86_64-linux";
overlay = ghostty.overlays.releasefast;
# module = ./configuration.nix # also works
module = {pkgs, ...}: {
environment.systemPackages = [
pkgs.btop
];
};
};
};
}
```
The custom VM can then be run with a command like this:
```
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
```
A file named `ghostty.qcow2` will be created that is used to persist any changes
made in the VM. To "reset" the VM to default delete the file and it will be
recreated the next time you run the VM.
### Contributing new VM definitions
#### VM Acceptance Criteria
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
1. They should be different enough from existing VM definitions that they represent a distinct
user (and developer) experience.
2. There's a significant Ghostty user population that uses a similar environment.
3. The VMs can be built using only packages from the current stable NixOS release.
#### VM Definition Criteria
1. VMs should be as minimal as possible so that they build and launch quickly.
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
2. VMs should not expose any services to the network, or run any remote access
software like SSH daemons, VNC or RDP.
3. VMs should auto-login using the "ghostty" user.

329
HACKING.md Normal file
View File

@@ -0,0 +1,329 @@
# Developing Ghostty
This document describes the technical details behind Ghostty's development.
If you'd like to open any pull requests or would like to implement new features
into Ghostty, please make sure to read our ["Contributing to Ghostty"](CONTRIBUTING.md)
document first.
To start development on Ghostty, you need to build Ghostty from a Git checkout,
which is very similar in process to [building Ghostty from a source tarball](http://ghostty.org/docs/install/build). One key difference is that obviously
you need to clone the Git repository instead of unpacking the source tarball:
```shell
git clone https://github.com/ghostty-org/ghostty
cd ghostty
```
> [!NOTE]
>
> Ghostty may require [extra dependencies](#extra-dependencies)
> when building from a Git checkout compared to a source tarball.
> Tip versions may also require a different version of Zig or other toolchains
> (e.g. the Xcode SDK on macOS) compared to stable versions — make sure to
> follow the steps closely!
When you're developing Ghostty, it's very likely that you will want to build a
_debug_ build to diagnose issues more easily. This is already the default for
Zig builds, so simply run `zig build` **without any `-Doptimize` flags**.
There are many more build steps than just `zig build`, some of which are listed
here:
| Command | Description |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `zig build run` | Runs Ghostty |
| `zig build run-valgrind` | Runs Ghostty under Valgrind to [check for memory leaks](#checking-for-memory-leaks) |
| `zig build test` | Runs unit tests (accepts `-Dtest-filter=<filter>` to only run tests whose name matches the filter) |
| `zig build update-translations` | Updates Ghostty's translation strings (see the [Contributor's Guide on Localizing Ghostty](po/README_CONTRIBUTORS.md)) |
| `zig build dist` | Builds a source tarball |
| `zig build distcheck` | Installs and validates a source tarball |
## Extra Dependencies
Building Ghostty from a Git checkout on Linux requires some additional
dependencies:
- `blueprint-compiler` (version 0.16.0 or newer)
macOS users don't require any additional dependencies.
## Xcode Version and SDKs
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
and the iOS SDK are all installed.
A common issue is that the incorrect version of Xcode is either
installed or selected. Use the `xcode-select` command to
ensure that the correct version of Xcode is selected:
```shell-session
sudo xcode-select --switch /Applications/Xcode-beta.app
```
> [!IMPORTANT]
>
> Main branch development of Ghostty is preparing for the next major
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
> **Xcode 26 and the macOS 26 SDK**.
>
> You do not need to be running on macOS 26 to build Ghostty, you can
> still use Xcode 26 beta on macOS 15 stable.
## Linting
### Prettier
Ghostty's docs and resources (not including Zig code) are linted using
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
check will fail builds with improper formatting. Therefore, if you are
modifying anything Prettier will lint, you may want to install it locally and
run this from the repo root before you commit:
```
prettier --write .
```
Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
Nix users can use the following command to format with Prettier:
```
nix develop -c prettier --write .
```
### Alejandra
Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check
will fail builds with improper formatting.
Nix users can use the following command to format with Alejandra:
```
nix develop -c alejandra .
```
Non-Nix users should install Alejandra and use the following command to format with Alejandra:
```
alejandra .
```
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
### Updating the Zig Cache Fixed-Output Derivation Hash
The Nix package depends on a [fixed-output
derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash)
that manages the Zig package cache. This allows the package to be built in the
Nix sandbox.
Occasionally (usually when `build.zig.zon` is updated), the hash that
identifies the cache will need to be updated. There are jobs that monitor the
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
```
This will write out the `nix/zigCacheHash.nix` file with the updated hash
that can then be committed and pushed to fix the builds.
## Including and Updating Translations
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
## Checking for Memory Leaks
While Zig does an amazing job of finding and preventing memory leaks,
Ghostty uses many third-party libraries that are written in C. Improper usage
of those libraries or bugs in those libraries can cause memory leaks that
Zig cannot detect by itself.
### On Linux
On Linux the recommended tool to check for memory leaks is Valgrind. The
recommended way to run Valgrind is via `zig build`:
```sh
zig build run-valgrind
```
This builds a Ghostty executable with Valgrind support and runs Valgrind
with the proper flags to ensure we're suppressing known false positives.
You can combine the same build args with `run-valgrind` that you can with
`run`, such as specifying additional configurations after a trailing `--`.
## Input Stack Testing
The input stack is the part of the codebase that starts with a
key event and ends with text encoding being sent to the pty (it
does not include _rendering_ the text, which is part of the
font or rendering stack).
If you modify any part of the input stack, you must manually verify
all the following input cases work properly. We unfortunately do
not automate this in any way, but if we can do that one day that'd
save a LOT of grief and time.
Note: this list may not be exhaustive, I'm still working on it.
### Linux IME
IME (Input Method Editors) are a common source of bugs in the input stack,
especially on Linux since there are multiple different IME systems
interacting with different windowing systems and application frameworks
all written by different organizations.
The following matrix should be tested to ensure that all IME input works
properly:
1. Wayland, X11
2. ibus, fcitx, none
3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
> [!NOTE]
>
> This is a **work in progress**. I'm still working on this list and it
> is not complete. As I find more test cases, I will add them here.
#### Dead Key Input
Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
1. Launch Ghostty
2. Press `'`
3. Press `a`
4. Verify that `á` is displayed
Note that the dead key may or may not show a preedit state visually.
For ibus and fcitx it does but for the "none" case it does not. Importantly,
the text should be correct when it is sent to the pty.
We should also test canceling dead key input:
1. Launch Ghostty
2. Press `'`
3. Press escape
4. Press `a`
5. Verify that `a` is displayed (no diacritic)
#### CJK Input
Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
exact layout doesn't matter.
1. Launch Ghostty
2. Press `Ctrl+Shift` to switch to "Hiragana"
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
4. Press `Enter`
5. Verify that `こん` is displayed in the terminal.
We should also test switching input methods while preedit is active, which
should commit the text:
1. Launch Ghostty
2. Press `Ctrl+Shift` to switch to "Hiragana"
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
4. Press `Ctrl+Shift` to switch to another layout (any)
5. Verify that `こん` is displayed in the terminal as committed text.
## Nix Virtual Machines
Several Nix virtual machine definitions are provided by the project for testing
and developing Ghostty against multiple different Linux desktop environments.
Running these requires a working Nix installation, either Nix on your
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
requirements for macOS are detailed below.
VMs should only be run on your local desktop and then powered off when not in
use, which will discard any changes to the VM.
The VM definitions provide minimal software "out of the box" but additional
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
### Linux
1. Check out the Ghostty source and change to the directory.
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
with `common` or `create`.
3. The VM will build and then launch. Depending on the speed of your system, this
can take a while, but eventually you should get a new VM window.
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
writable by the VM user, so be careful!
### macOS
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
blog post for more information about the Linux builder and how to tune the performance.
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
above to launch a VM.
### Custom VMs
To easily create a custom VM without modifying the Ghostty source, create a new
directory, then create a file called `flake.nix` with the following text in the
new directory.
```
{
inputs = {
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
ghostty.url = "github:ghostty-org/ghostty";
};
outputs = {
nixpkgs,
ghostty,
...
}: {
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
nixpkgs = nixpkgs;
system = "x86_64-linux";
overlay = ghostty.overlays.releasefast;
# module = ./configuration.nix # also works
module = {pkgs, ...}: {
environment.systemPackages = [
pkgs.btop
];
};
};
};
}
```
The custom VM can then be run with a command like this:
```
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
```
A file named `ghostty.qcow2` will be created that is used to persist any changes
made in the VM. To "reset" the VM to default delete the file and it will be
recreated the next time you run the VM.
### Contributing new VM definitions
#### VM Acceptance Criteria
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
1. They should be different enough from existing VM definitions that they represent a distinct
user (and developer) experience.
2. There's a significant Ghostty user population that uses a similar environment.
3. The VMs can be built using only packages from the current stable NixOS release.
#### VM Definition Criteria
1. VMs should be as minimal as possible so that they build and launch quickly.
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
2. VMs should not expose any services to the network, or run any remote access
software like SSH daemons, VNC or RDP.
3. VMs should auto-login using the "ghostty" user.

128
README.md
View File

@@ -13,7 +13,9 @@
·
<a href="https://ghostty.org/docs">Documentation</a>
·
<a href="#developing-ghostty">Developing</a>
<a href="CONTRIBUTING.md">Contributing</a>
·
<a href="HACKING.md">Developing</a>
</p>
</p>
@@ -49,6 +51,14 @@ See the [download page](https://ghostty.org/download) on the Ghostty website.
See the [documentation](https://ghostty.org/docs) on the Ghostty website.
## Contributing and Developing
If you have any ideas, issues, etc. regarding Ghostty, or would like to
contribute to Ghostty through pull requests, please check out our
["Contributing to Ghostty"](CONTRIBUTING.md) document. Those who would like
to get involved with Ghostty's development as well should also read the
["Developing Ghostty"](HACKING.md) document for more technical details.
## Roadmap and Status
The high-level ambitious plan for the project, in order:
@@ -184,119 +194,3 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
> stack memory of each thread at the time of the crash. This information
> is used to rebuild the stack trace but can also contain sensitive data
> depending when the crash occurred.
## Developing Ghostty
See the documentation on the Ghostty website for
[building Ghostty from a source tarball](http://ghostty.org/docs/install/build).
Building Ghostty from a Git checkout is very similar, except you want to
omit the `-Doptimize` flag to build a debug build, and you may require
additional dependencies since the source tarball includes some processed
files that are not in the Git repository.
Other useful commands:
- `zig build test` for running unit tests.
- `zig build test -Dtest-filter=<filter>` for running a specific subset of those unit tests
- `zig build run -Dconformance=<name>` runs a conformance test case from
the `conformance` directory. The `name` is the name of the file. This runs
in the current running terminal emulator so if you want to check the
behavior of this project, you must run this command in Ghostty.
### Extra Dependencies
Building Ghostty from a Git checkout on Linux requires some additional
dependencies:
- `blueprint-compiler`
macOS users don't require any additional dependencies.
> [!NOTE]
> This only applies to building from a _Git checkout_. This section does
> not apply if you're building from a released _source tarball_. For
> source tarballs, see the
> [website](http://ghostty.org/docs/install/build).
### Xcode Version and SDKs
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
and the iOS SDK are all installed.
A common issue is that the incorrect version of Xcode is either
installed or selected. Use the `xcode-select` command to
ensure that the correct version of Xcode is selected:
```shell-session
sudo xcode-select --switch /Applications/Xcode-beta.app
```
> [!IMPORTANT]
>
> Main branch development of Ghostty is preparing for the next major
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
> **Xcode 26 and the macOS 26 SDK**.
>
> You do not need to be running on macOS 26 to build Ghostty, you can
> still use Xcode 26 beta on macOS 15 stable.
### Linting
#### Prettier
Ghostty's docs and resources (not including Zig code) are linted using
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
check will fail builds with improper formatting. Therefore, if you are
modifying anything Prettier will lint, you may want to install it locally and
run this from the repo root before you commit:
```
prettier --write .
```
Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
Nix users can use the following command to format with Prettier:
```
nix develop -c prettier --write .
```
#### Alejandra
Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check
will fail builds with improper formatting.
Nix users can use the following command to format with Alejandra:
```
nix develop -c alejandra .
```
Non-Nix users should install Alejandra and use the following command to format with Alejandra:
```
alejandra .
```
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
#### Updating the Zig Cache Fixed-Output Derivation Hash
The Nix package depends on a [fixed-output
derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash)
that manages the Zig package cache. This allows the package to be built in the
Nix sandbox.
Occasionally (usually when `build.zig.zon` is updated), the hash that
identifies the cache will need to be updated. There are jobs that monitor the
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
```
This will write out the `nix/zigCacheHash.nix` file with the updated hash
that can then be committed and pushed to fix the builds.

View File

@@ -20,8 +20,8 @@
},
.z2d = .{
// vancluever/z2d
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz",
.hash = "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5",
.url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz",
.hash = "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l",
.lazy = true,
},
.zig_objc = .{
@@ -112,8 +112,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz",
.hash = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
.hash = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME",
.lazy = true,
},
},

12
build.zig.zon.json generated
View File

@@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls": {
"N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz",
"hash": "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
"hash": "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",
@@ -129,10 +129,10 @@
"url": "https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d68607a7c232c958315d381b5cd.tar.gz",
"hash": "sha256-nkzSCr6W5sTG7enDBXEIhgEm574uLD41UVR2wlC+HBM="
},
"z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5": {
"z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l": {
"name": "z2d",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz",
"hash": "sha256-6ZqgrO/bcjgnuQcfq89VYptUUodsErM8Fz6nwBZhTJs="
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz",
"hash": "sha256-tWrLlEOU4/0ZOlzgGOXB08fW7sqfyAFf3rlv0wl9b/c="
},
"zf-0.10.3-OIRy8aiIAACLrBllz0zjxaH0aOe5oNm3KtEMyCntST-9": {
"name": "zf",

13
build.zig.zon.nix generated
View File

@@ -49,6 +49,7 @@
inherit name rev hash;
url = url_without_query;
deepClone = false;
fetchSubmodules = false;
};
fetchZigArtifact = {
@@ -162,11 +163,11 @@ in
};
}
{
name = "N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls";
name = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz";
hash = "sha256-PySWF/9IAK4DZCkd5FRpiaIl6et2Qm6t8IKCTzh/Xa0=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz";
hash = "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc=";
};
}
{
@@ -290,11 +291,11 @@ in
};
}
{
name = "z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5";
name = "z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l";
path = fetchZigArtifact {
name = "z2d";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz";
hash = "sha256-6ZqgrO/bcjgnuQcfq89VYptUUodsErM8Fz6nwBZhTJs=";
url = "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz";
hash = "sha256-tWrLlEOU4/0ZOlzgGOXB08fW7sqfyAFf3rlv0wl9b/c=";
};
}
{

4
build.zig.zon.txt generated
View File

@@ -28,8 +28,8 @@ 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.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz
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.7.1.tar.gz
https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz

25
flake.lock generated
View File

@@ -47,6 +47,19 @@
"url": "https://channels.nixos.org/nixos-25.05/nixexprs.tar.xz"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1755972213,
"narHash": "sha256-VYK7aDAv8H1enXn1ECRHmGbeY6RqLnNwUJkOwloIsko=",
"rev": "73e96df7cff5783f45e21342a75a1540c4eddce4",
"type": "tarball",
"url": "https://releases.nixos.org/nixos/unstable-small/nixos-25.11pre850642.73e96df7cff5/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
"url": "https://channels.nixos.org/nixos-unstable-small/nixexprs.tar.xz"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
@@ -102,22 +115,20 @@
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1742104771,
"narHash": "sha256-LhidlyEA9MP8jGe1rEnyjGFCzLLgCdDpYeWggibayr0=",
"lastModified": 1756000480,
"narHash": "sha256-fR5pdcjO0II5MNdCzqvyokyuFkmff7/FyBAjUS6sMfA=",
"owner": "jcollie",
"repo": "zon2nix",
"rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613",
"rev": "d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60",
"type": "github"
},
"original": {
"owner": "jcollie",
"repo": "zon2nix",
"rev": "56c159be489cc6c0e73c3930bd908ddc6fe89613",
"rev": "d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60",
"type": "github"
}
}

View File

@@ -24,9 +24,12 @@
};
zon2nix = {
url = "github:jcollie/zon2nix?rev=56c159be489cc6c0e73c3930bd908ddc6fe89613";
url = "github:jcollie/zon2nix?rev=d9dc9ef1ab9ae45b5c9d80c6a747cc9968ee0c60";
inputs = {
nixpkgs.follows = "nixpkgs";
# Don't override nixpkgs until Zig 0.15 is available in the Nix branch
# we are using for "normal" builds.
#
# nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};

View File

@@ -1,108 +0,0 @@
#!/usr/bin/env bash
#
# This script checks if the flatpak/zig-packages.json file is up-to-date.
# If the `--update` flag is passed, it will update all necessary
# files to be up to date.
#
# The files owned by this are:
#
# - flatpak/zig-packages.json
#
# All of these are auto-generated and should not be edited manually.
# Nothing in this script should fail.
set -eu
set -o pipefail
WORK_DIR=$(mktemp -d)
if [[ ! "$WORK_DIR" || ! -d "$WORK_DIR" ]]; then
echo "could not create temp dir"
exit 1
fi
function cleanup {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
help() {
echo ""
echo "To fix, please (manually) re-run the script from the repository root,"
echo "commit, and submit a PR with the update:"
echo ""
echo " ./flatpak/build-support/check-zig-cache.sh --update"
echo " git add flatpak/zig-packages.json"
echo " git commit -m \"flatpak: update zig-packages.json\""
echo ""
}
# Turn Nix's base64 hashes into regular hexadecimal form
decode_hash() {
input=$1
input=${input#sha256-}
echo "$input" | base64 -d | od -vAn -t x1 | tr -d ' \n'
}
ROOT="$(realpath "$(dirname "$0")/../../")"
ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json"
BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json"
if [ ! -f "${BUILD_ZIG_ZON_JSON}" ]; then
echo -e "\nERROR: build.zig.zon2json-lock missing."
help
exit 1
fi
if [ -f "${ZIG_PACKAGES_JSON}" ]; then
OLD_HASH=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}')
fi
while read -r url sha256 dest; do
src_type=archive
sha256=$(decode_hash "$sha256")
git_commit=
if [[ "$url" =~ ^git\+* ]]; then
src_type=git
sha256=
url=${url#git+}
git_commit=${url##*#}
url=${url%%/\?ref*}
url=${url%%#*}
fi
jq \
-nec \
--arg type "$src_type" \
--arg url "$url" \
--arg git_commit "$git_commit" \
--arg dest "$dest" \
--arg sha256 "$sha256" \
'{
type: $type,
url: $url,
commit: $git_commit,
dest: $dest,
sha256: $sha256,
} | with_entries(select(.value != ""))'
done < <(jq -rc 'to_entries[] | [.value.url, .value.hash, "vendor/p/\(.key)"] | @tsv' "$BUILD_ZIG_ZON_JSON") |
jq -s '.' >"$WORK_DIR/zig-packages.json"
NEW_HASH=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}')
if [ "${OLD_HASH}" == "${NEW_HASH}" ]; then
echo -e "\nOK: flatpak/zig-packages.json unchanged."
exit 0
elif [ "${1:-}" != "--update" ]; then
echo -e "\nERROR: flatpak/zig-packages.json needs to be updated."
echo ""
echo " * Old hash: ${OLD_HASH}"
echo " * New hash: ${NEW_HASH}"
help
exit 1
else
mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON"
echo -e "\nOK: flatpak/zig-packages.json updated."
exit 0
fi

View File

@@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/8b639f0c2605557bd23ba1b940842c67bbfd4ed0.tar.gz",
"dest": "vendor/p/N-V-__8AAAlgXwSghpDmXBXZM4Rpd80WKOXVWTrcL0ucVmls",
"sha256": "3f249617ff4800ae0364291de4546989a225e9eb76426eadf082824f387f5dad"
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
"dest": "vendor/p/N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME",
"sha256": "3655177013a6680f16fbb457b97727f532219bdeb87573b63a9e10090c610f27"
},
{
"type": "archive",
@@ -157,9 +157,9 @@
},
{
"type": "archive",
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.1.tar.gz",
"dest": "vendor/p/z2d-0.7.1-j5P_HhFQDQC6T5srG235r_nQpCrNwI15Gu2zqVrtUxN5",
"sha256": "e99aa0acefdb723827b9071fabcf55629b5452876c12b33c173ea7c016614c9b"
"url": "https://github.com/vancluever/z2d/archive/refs/tags/v0.7.2.tar.gz",
"dest": "vendor/p/z2d-0.7.2-j5P_Hm1oDQDQsWpGfSCMARhowBnuTHlQ_sBfij6TuG7l",
"sha256": "b56acb944394e3fd193a5ce018e5c1d3c7d6eeca9fc8015fdeb96fd3097d6ff7"
},
{
"type": "archive",

View File

@@ -419,6 +419,7 @@ typedef struct {
ghostty_env_var_s* env_vars;
size_t env_var_count;
const char* initial_input;
bool wait_after_command;
} ghostty_surface_config_s;
typedef struct {
@@ -450,6 +451,28 @@ typedef struct {
ghostty_config_color_s colors[256];
} ghostty_config_palette_s;
// config.QuickTerminalSize
typedef enum {
GHOSTTY_QUICK_TERMINAL_SIZE_NONE,
GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE,
GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS,
} ghostty_quick_terminal_size_tag_e;
typedef union {
float percentage;
uint32_t pixels;
} ghostty_quick_terminal_size_value_u;
typedef struct {
ghostty_quick_terminal_size_tag_e tag;
ghostty_quick_terminal_size_value_u value;
} ghostty_quick_terminal_size_s;
typedef struct {
ghostty_quick_terminal_size_s primary;
ghostty_quick_terminal_size_s secondary;
} ghostty_config_quick_terminal_size_s;
// apprt.Target.Key
typedef enum {
GHOSTTY_TARGET_APP,
@@ -680,6 +703,12 @@ typedef struct {
uintptr_t len;
} ghostty_action_open_url_s;
// apprt.action.CloseTabMode
typedef enum {
GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS,
GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER,
} ghostty_action_close_tab_mode_e;
// apprt.surface.Message.ChildExited
typedef struct {
uint32_t exit_code;
@@ -693,15 +722,15 @@ typedef enum {
GHOSTTY_PROGRESS_STATE_ERROR,
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
GHOSTTY_PROGRESS_STATE_PAUSE,
} ghostty_terminal_osc_command_progressreport_state_e;
} ghostty_action_progress_report_state_e;
// terminal.osc.Command.ProgressReport.C
typedef struct {
ghostty_terminal_osc_command_progressreport_state_e state;
ghostty_action_progress_report_state_e state;
// -1 if no progress was reported, otherwise 0-100 indicating percent
// completeness.
int8_t progress;
} ghostty_terminal_osc_command_progressreport_s;
} ghostty_action_progress_report_s;
// apprt.Action.Key
typedef enum {
@@ -786,8 +815,9 @@ typedef union {
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
ghostty_action_open_url_s open_url;
ghostty_action_close_tab_mode_e close_tab_mode;
ghostty_surface_message_childexited_s child_exited;
ghostty_terminal_osc_command_progressreport_s progress_report;
ghostty_action_progress_report_s progress_report;
} ghostty_action_u;
typedef struct {
@@ -934,7 +964,7 @@ void ghostty_surface_mouse_scroll(ghostty_surface_t,
double,
ghostty_input_scroll_mods_t);
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*);
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
void ghostty_surface_request_close(ghostty_surface_t);
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
void ghostty_surface_split_focus(ghostty_surface_t,

View File

@@ -104,6 +104,7 @@
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5AEB1642D5BE7BF00513529 /* LastWindowPosition.swift */; };
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */; };
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */; };
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
@@ -126,6 +127,7 @@
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; };
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
@@ -252,6 +254,7 @@
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
A5B4EA842DFE69140022C3A2 /* NSMenuItem+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenuItem+Extension.swift"; sourceTree = "<group>"; };
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSize.swift; sourceTree = "<group>"; };
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
@@ -637,6 +640,7 @@
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
A5BB78B82DF9D8CE009AC3FA /* QuickTerminalSize.swift */,
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
);
path = QuickTerminal;
@@ -939,6 +943,10 @@
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */,
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */,
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
A5BB78B92DF9D8CE009AC3FA /* QuickTerminalSize.swift in Sources */,
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */,
@@ -980,6 +988,7 @@
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */,
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,

View File

@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "0ef1ee0220239b3776f433314515fd849025673f",
"version" : "2.6.4"
"revision" : "df074165274afaa39539c05d57b0832620775b11",
"version" : "2.7.1"
}
}
],

View File

@@ -394,35 +394,69 @@ class AppDelegate: NSObject,
// Ghostty will validate as well but we can avoid creating an entirely new
// surface by doing our own validation here. We can also show a useful error
// this way.
var isDirectory = ObjCBool(true)
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
// Set to true if confirmation is required before starting up the
// new terminal.
var requiresConfirm: Bool = false
// Initialize the surface config which will be used to create the tab or window for the opened file.
var config = Ghostty.SurfaceConfiguration()
if (isDirectory.boolValue) {
// When opening a directory, check the configuration to decide
// whether to open in a new tab or new window.
config.workingDirectory = filename
} else {
// Unconditionally require confirmation in the file execution case.
// In the future I have ideas about making this more fine-grained if
// we can not inherit of unsandboxed state. For now, we need to confirm
// because there is a sandbox escape possible if a sandboxed application
// somehow is tricked into `open`-ing a non-sandboxed application.
requiresConfirm = true
// When opening a file, we want to execute the file. To do this, we
// don't override the command directly, because it won't load the
// profile/rc files for the shell, which is super important on macOS
// due to things like Homebrew. Instead, we set the command to
// `<filename>; exit` which is what Terminal and iTerm2 do.
config.initialInput = "\(filename); exit\n"
// For commands executed directly, we want to ensure we wait after exit
// because in most cases scripts don't block on exit and we don't want
// the window to just flash closed once complete.
config.waitAfterCommand = true
// Set the parent directory to our working directory so that relative
// paths in scripts work.
config.workingDirectory = (filename as NSString).deletingLastPathComponent
}
if requiresConfirm {
// Confirmation required. We use an app-wide NSAlert for now. In the future we
// may want to show this as a sheet on the focused window (especially if we're
// opening a tab). I'm not sure.
let alert = NSAlert()
alert.messageText = "Allow Ghostty to execute \"\(filename)\"?"
alert.addButton(withTitle: "Allow")
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
switch (alert.runModal()) {
case .alertFirstButtonReturn:
break
default:
return false
}
}
switch ghostty.config.macosDockDropBehavior {
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
}
return true
}
@@ -957,18 +991,10 @@ class AppDelegate: NSObject,
@IBAction func newWindow(_ sender: Any?) {
_ = TerminalController.newWindow(ghostty)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func newTab(_ sender: Any?) {
_ = TerminalController.newTab(ghostty)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
@IBAction func closeAllWindows(_ sender: Any?) {

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24093.7" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="24123.1" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24093.7"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="24123.1"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">

View File

@@ -22,7 +22,7 @@ class QuickTerminalController: BaseTerminalController {
private var previousActiveSpace: CGSSpace? = nil
/// The window frame saved when the quick terminal's surface tree becomes empty.
///
///
/// This preserves the user's window size and position when all terminal surfaces
/// are closed (e.g., via the `exit` command). When a new surface is created,
/// the window will be restored to this frame, preventing SwiftUI from resetting
@@ -34,6 +34,9 @@ class QuickTerminalController: BaseTerminalController {
/// The configuration derived from the Ghostty config so we don't need to rely on references.
private var derivedConfig: DerivedConfig
/// Tracks if we're currently handling a manual resize to prevent recursion
private var isHandlingResize: Bool = false
init(_ ghostty: Ghostty.App,
position: QuickTerminalPosition = .top,
@@ -76,6 +79,11 @@ class QuickTerminalController: BaseTerminalController {
selector: #selector(onNewTab),
name: Ghostty.Notification.ghosttyNewTab,
object: nil)
center.addObserver(
self,
selector: #selector(windowDidResize(_:)),
name: NSWindow.didResizeNotification,
object: nil)
}
required init?(coder: NSCoder) {
@@ -109,14 +117,29 @@ class QuickTerminalController: BaseTerminalController {
syncAppearance()
// Setup our initial size based on our configured position
position.setLoaded(window)
position.setLoaded(window, size: derivedConfig.quickTerminalSize)
// Upon first adding this Window to its host view, older SwiftUI
// seems to have a "hiccup" and corrupts the frameRect,
// sometimes setting the size to zero, sometimes corrupting it.
// We pass the actual window's frame as "initial" frame directly
// to the window, so it can use that instead of the frameworks
// "interpretation"
if let qtWindow = window as? QuickTerminalWindow {
qtWindow.initialFrame = window.frame
}
// Setup our content
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty,
viewModel: self,
delegate: self
))
// Clear out our frame at this point, the fixup from above is complete.
if let qtWindow = window as? QuickTerminalWindow {
qtWindow.initialFrame = nil
}
// Animate the window in
animateIn()
@@ -194,11 +217,28 @@ class QuickTerminalController: BaseTerminalController {
}
}
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
// We use the actual screen the window is on for this, since it should
// be on the proper screen.
guard let screen = window?.screen ?? NSScreen.main else { return frameSize }
return position.restrictFrameSize(frameSize, on: screen)
override func windowDidResize(_ notification: Notification) {
guard let window = notification.object as? NSWindow,
window == self.window,
visible,
!isHandlingResize else { return }
guard let screen = window.screen ?? NSScreen.main else { return }
// Prevent recursive loops
isHandlingResize = true
defer { isHandlingResize = false }
switch position {
case .top, .bottom, .center:
// For centered positions (top, bottom, center), we need to recenter the window
// when it's manually resized to maintain proper positioning
let newOrigin = position.centeredOrigin(for: window, on: screen)
window.setFrameOrigin(newOrigin)
case .left, .right:
// For side positions, we may need to adjust vertical centering
let newOrigin = position.verticallyCenteredOrigin(for: window, on: screen)
window.setFrameOrigin(newOrigin)
}
}
// MARK: Base Controller Overrides
@@ -318,15 +358,17 @@ class QuickTerminalController: BaseTerminalController {
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
// Grab our last closed frame to use, and clear our state since we're animating in.
let lastClosedFrame = self.lastClosedFrame
self.lastClosedFrame = nil
// Restore our previous frame if we have one
if let lastClosedFrame {
window.setFrame(lastClosedFrame, display: false)
self.lastClosedFrame = nil
}
// Move our window off screen to the top
position.setInitial(in: window, on: screen)
// Move our window off screen to the initial animation position.
position.setInitial(
in: window,
on: screen,
terminalSize: derivedConfig.quickTerminalSize,
closedFrame: lastClosedFrame)
// We need to set our window level to a high value. In testing, only
// popUpMenu and above do what we want. This gets it above the menu bar
@@ -357,7 +399,11 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setFinal(in: window.animator(), on: screen)
position.setFinal(
in: window.animator(),
on: screen,
terminalSize: derivedConfig.quickTerminalSize,
closedFrame: lastClosedFrame)
}, completionHandler: {
// There is a very minor delay here so waiting at least an event loop tick
// keeps us safe from the view not being on the window.
@@ -435,11 +481,19 @@ class QuickTerminalController: BaseTerminalController {
}
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
// If we are in fullscreen, then we exit fullscreen. We do this immediately so
// we have th correct window.frame for the save state below.
if let fullscreenStyle, fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
}
// Save the current window frame before animating out. This preserves
// the user's preferred window size and position for when the quick
// terminal is reactivated with a new surface. Without this, SwiftUI
// would reset the window to its minimum content size.
lastClosedFrame = window.frame
if window.frame.width > 0 && window.frame.height > 0 {
lastClosedFrame = window.frame
}
// If we hid the dock then we unhide it.
hiddenDock = nil
@@ -455,11 +509,6 @@ class QuickTerminalController: BaseTerminalController {
// We always animate out to whatever screen the window is actually on.
guard let screen = window.screen ?? NSScreen.main else { return }
// If we are in fullscreen, then we exit fullscreen.
if let fullscreenStyle, fullscreenStyle.isFullscreen {
fullscreenStyle.exit()
}
// If we have a previously active application, restore focus to it. We
// do this BEFORE the animation below because when the animation completes
// macOS will bring forward another window.
@@ -481,7 +530,11 @@ class QuickTerminalController: BaseTerminalController {
NSAnimationContext.runAnimationGroup({ context in
context.duration = derivedConfig.quickTerminalAnimationDuration
context.timingFunction = .init(name: .easeIn)
position.setInitial(in: window.animator(), on: screen)
position.setInitial(
in: window.animator(),
on: screen,
terminalSize: derivedConfig.quickTerminalSize,
closedFrame: window.frame)
}, completionHandler: {
// This causes the window to be removed from the screen list and macOS
// handles what should be focused next.
@@ -612,6 +665,7 @@ class QuickTerminalController: BaseTerminalController {
let quickTerminalAnimationDuration: Double
let quickTerminalAutoHide: Bool
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
let quickTerminalSize: QuickTerminalSize
let backgroundOpacity: Double
init() {
@@ -619,6 +673,7 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = 0.2
self.quickTerminalAutoHide = true
self.quickTerminalSpaceBehavior = .move
self.quickTerminalSize = QuickTerminalSize()
self.backgroundOpacity = 1.0
}
@@ -627,6 +682,7 @@ class QuickTerminalController: BaseTerminalController {
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
self.quickTerminalAutoHide = config.quickTerminalAutoHide
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
self.quickTerminalSize = config.quickTerminalSize
self.backgroundOpacity = config.backgroundOpacity
}
}

View File

@@ -7,95 +7,86 @@ enum QuickTerminalPosition : String {
case right
case center
/// Set the loaded state for a window.
func setLoaded(_ window: NSWindow) {
/// Set the loaded state for a window. This should only be called when the window is first loaded,
/// usually in `windowDidLoad` or in a similar callback. This is the initial state.
func setLoaded(_ window: NSWindow, size: QuickTerminalSize) {
guard let screen = window.screen ?? NSScreen.main else { return }
switch (self) {
case .top, .bottom:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width,
height: screen.frame.height / 4)
), display: false)
case .left, .right:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 4,
height: screen.frame.height)
), display: false)
case .center:
window.setFrame(.init(
origin: window.frame.origin,
size: .init(
width: screen.frame.width / 2,
height: screen.frame.height / 3)
), display: false)
}
window.setFrame(.init(
origin: window.frame.origin,
size: size.calculate(position: self, screenDimensions: screen.visibleFrame.size)
), display: false)
}
/// Set the initial state for a window for animating out of this position.
func setInitial(in window: NSWindow, on screen: NSScreen) {
// We always start invisible
/// Set the initial state for a window NOT yet into position (either before animating in or
/// after animating out).
func setInitial(
in window: NSWindow,
on screen: NSScreen,
terminalSize: QuickTerminalSize,
closedFrame: NSRect? = nil
) {
// Invisible
window.alphaValue = 0
// Position depends
window.setFrame(.init(
origin: initialOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen)
size: closedFrame?.size ?? configuredFrameSize(
on: screen,
terminalSize: terminalSize)
), display: false)
}
/// Set the final state for a window in this position.
func setFinal(in window: NSWindow, on screen: NSScreen) {
func setFinal(
in window: NSWindow,
on screen: NSScreen,
terminalSize: QuickTerminalSize,
closedFrame: NSRect? = nil
) {
// We always end visible
window.alphaValue = 1
// Position depends
window.setFrame(.init(
origin: finalOrigin(for: window, on: screen),
size: restrictFrameSize(window.frame.size, on: screen)
size: closedFrame?.size ?? configuredFrameSize(
on: screen,
terminalSize: terminalSize)
), display: true)
}
/// Restrict the frame size during resizing.
func restrictFrameSize(_ size: NSSize, on screen: NSScreen) -> NSSize {
var finalSize = size
switch (self) {
case .top, .bottom:
finalSize.width = screen.frame.width
case .left, .right:
finalSize.height = screen.visibleFrame.height
case .center:
finalSize.width = screen.frame.width / 2
finalSize.height = screen.frame.height / 3
}
return finalSize
/// Get the configured frame size for initial positioning and animations.
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize {
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.visibleFrame.size)
return NSSize(width: dimensions.width, height: dimensions.height)
}
/// The initial point origin for this position.
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.frame.maxY)
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: screen.visibleFrame.maxY)
case .bottom:
return .init(x: screen.frame.minX, y: -window.frame.height)
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: -window.frame.height)
case .left:
return .init(x: screen.frame.minX-window.frame.width, y: 0)
return .init(
x: screen.visibleFrame.minX-window.frame.width,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
case .right:
return .init(x: screen.frame.maxX, y: 0)
return .init(
x: screen.visibleFrame.maxX,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
case .center:
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width)
}
}
@@ -103,19 +94,27 @@ enum QuickTerminalPosition : String {
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch (self) {
case .top:
return .init(x: screen.frame.minX, y: screen.visibleFrame.maxY - window.frame.height)
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: screen.visibleFrame.maxY - window.frame.height)
case .bottom:
return .init(x: screen.frame.minX, y: screen.frame.minY)
return .init(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: screen.visibleFrame.minY)
case .left:
return .init(x: screen.frame.minX, y: window.frame.origin.y)
return .init(
x: screen.visibleFrame.minX,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
case .right:
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
return .init(
x: screen.visibleFrame.maxX - window.frame.width,
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
case .center:
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
}
}
@@ -136,4 +135,52 @@ enum QuickTerminalPosition : String {
case .right: self == .top || self == .bottom
}
}
/// Calculate the centered origin for a window, keeping it properly positioned after manual resizing
func centeredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch self {
case .top:
return CGPoint(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: window.frame.origin.y // Keep the same Y position
)
case .bottom:
return CGPoint(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: window.frame.origin.y // Keep the same Y position
)
case .center:
return CGPoint(
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
)
case .left, .right:
// For left/right positions, only adjust horizontal centering if needed
return window.frame.origin
}
}
/// Calculate the vertically centered origin for side-positioned windows
func verticallyCenteredOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
switch self {
case .left:
return CGPoint(
x: window.frame.origin.x, // Keep the same X position
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
)
case .right:
return CGPoint(
x: window.frame.origin.x, // Keep the same X position
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
)
case .top, .bottom, .center:
// These positions don't need vertical recentering during resize
return window.frame.origin
}
}
}

View File

@@ -0,0 +1,84 @@
import GhosttyKit
/// Represents the Ghostty `quick-terminal-size` configuration. See the documentation for
/// that for more details on exactly how it works. Some of those docs will be reproduced in various comments
/// in this file but that is the best source of truth for it.
///
/// The size determines the size of the quick terminal along the primary and secondary axis. The primary and
/// secondary axis is defined by the `quick-terminal-position`.
struct QuickTerminalSize {
let primary: Size?
let secondary: Size?
init(primary: Size? = nil, secondary: Size? = nil) {
self.primary = primary
self.secondary = secondary
}
init(from cStruct: ghostty_config_quick_terminal_size_s) {
self.primary = Size(from: cStruct.primary)
self.secondary = Size(from: cStruct.secondary)
}
enum Size {
case percentage(Float)
case pixels(UInt32)
init?(from cStruct: ghostty_quick_terminal_size_s) {
switch cStruct.tag {
case GHOSTTY_QUICK_TERMINAL_SIZE_NONE:
return nil
case GHOSTTY_QUICK_TERMINAL_SIZE_PERCENTAGE:
self = .percentage(cStruct.value.percentage)
case GHOSTTY_QUICK_TERMINAL_SIZE_PIXELS:
self = .pixels(cStruct.value.pixels)
default:
return nil
}
}
func toPixels(parentDimension: CGFloat) -> CGFloat {
switch self {
case .percentage(let value):
return parentDimension * CGFloat(value) / 100.0
case .pixels(let value):
return CGFloat(value)
}
}
}
/// This is an almost direct port of th Zig function QuickTerminalSize.calculate
func calculate(position: QuickTerminalPosition, screenDimensions: CGSize) -> CGSize {
let dims = CGSize(width: screenDimensions.width, height: screenDimensions.height)
switch position {
case .left, .right:
return CGSize(
width: primary?.toPixels(parentDimension: dims.width) ?? 400,
height: secondary?.toPixels(parentDimension: dims.height) ?? dims.height
)
case .top, .bottom:
return CGSize(
width: secondary?.toPixels(parentDimension: dims.width) ?? dims.width,
height: primary?.toPixels(parentDimension: dims.height) ?? 400
)
case .center:
if dims.width >= dims.height {
// Landscape
return CGSize(
width: primary?.toPixels(parentDimension: dims.width) ?? 800,
height: secondary?.toPixels(parentDimension: dims.height) ?? 400
)
} else {
// Portrait
return CGSize(
width: secondary?.toPixels(parentDimension: dims.width) ?? 400,
height: primary?.toPixels(parentDimension: dims.height) ?? 800
)
}
}
}
}

View File

@@ -29,4 +29,19 @@ class QuickTerminalWindow: NSPanel {
// We don't want to activate the owning app when quick terminal is triggered.
self.styleMask.insert(.nonactivatingPanel)
}
/// This is set to the frame prior to setting `contentView`. This is purely a hack to workaround
/// bugs in older macOS versions (Ventura): https://github.com/ghostty-org/ghostty/pull/8026
var initialFrame: NSRect? = nil
override func setFrame(_ frameRect: NSRect, display flag: Bool) {
// Upon first adding this Window to its host view, older SwiftUI
// seems to have a "hiccup" and corrupts the frameRect,
// sometimes setting the size to zero, sometimes corrupting it.
// If we find we have cached the "initial" frame, use that instead
// the propagated one through the framework
//
// https://github.com/ghostty-org/ghostty/pull/8026
super.setFrame(initialFrame ?? frameRect, display: flag)
}
}

View File

@@ -290,8 +290,12 @@ class BaseTerminalController: NSWindowController,
alert.addButton(withTitle: "Cancel")
alert.alertStyle = .warning
alert.beginSheetModal(for: window) { response in
let alertWindow = alert.window
self.alert = nil
if response == .alertFirstButtonReturn {
// This is important so that we avoid losing focus when Stage
// Manager is used (#8336)
alertWindow.orderOut(nil)
completion()
}
}

View File

@@ -95,6 +95,11 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
selector: #selector(onCloseTab),
name: .ghosttyCloseTab,
object: nil)
center.addObserver(
self,
selector: #selector(onCloseOtherTabs),
name: .ghosttyCloseOtherTabs,
object: nil)
center.addObserver(
self,
selector: #selector(onResetWindowSize),
@@ -196,7 +201,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
if let parent {
if parent.styleMask.contains(.fullScreen) {
parent.toggleFullScreen(nil)
// If our previous window was fullscreen then we want our new window to
// be fullscreen. This behavior actually doesn't match the native tabbing
// behavior of macOS apps where new windows create tabs when in native
// fullscreen but this is how we've always done it. This matches iTerm2
// behavior.
c.toggleFullscreen(mode: .native)
} else if ghostty.config.windowFullscreen {
switch (ghostty.config.windowFullscreenMode) {
case .native:
@@ -226,6 +236,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
}
c.showWindow(self)
// All new_window actions force our app to be active, so that the new
// window is focused and visible.
NSApp.activate(ignoringOtherApps: true)
}
// Setup our undo
@@ -332,6 +346,10 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
controller.showWindow(self)
window.makeKeyAndOrderFront(self)
// We also activate our app so that it becomes front. This may be
// necessary for the dock menu.
NSApp.activate(ignoringOtherApps: true)
}
// It takes an event loop cycle until the macOS tabGroup state becomes
@@ -421,8 +439,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
continue
}
let action = "goto_tab:\(tab)"
if let equiv = ghostty.config.keyboardShortcut(for: action) {
if let equiv = ghostty.config.keyboardShortcut(for: "goto_tab:\(tab)") {
window.keyEquivalent = "\(equiv)"
} else {
window.keyEquivalent = ""
@@ -546,7 +563,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeWindow(nil)
}
private func closeTabImmediately() {
private func closeTabImmediately(registerRedo: Bool = true) {
guard let window = window else { return }
guard let tabGroup = window.tabGroup,
tabGroup.windows.count > 1 else {
@@ -563,19 +580,69 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
expiresAfter: undoExpiration
) { ghostty in
let newController = TerminalController(ghostty, with: undoState)
// Register redo action
undoManager.registerUndo(
withTarget: newController,
expiresAfter: newController.undoExpiration
) { target in
target.closeTabImmediately()
if registerRedo {
undoManager.registerUndo(
withTarget: newController,
expiresAfter: newController.undoExpiration
) { target in
target.closeTabImmediately()
}
}
}
}
window.close()
}
private func closeOtherTabsImmediately() {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return }
guard tabGroup.windows.count > 1 else { return }
// Start an undo grouping
if let undoManager {
undoManager.beginUndoGrouping()
}
defer {
undoManager?.endUndoGrouping()
}
// Iterate through all tabs except the current one.
for window in tabGroup.windows where window != self.window {
// We ignore any non-terminal tabs. They don't currently exist and we can't
// properly undo them anyways so I'd rather ignore them and get a bug report
// later if and when we introduce non-terminal tabs.
if let controller = window.windowController as? TerminalController {
// We must not register a redo, because it messes with our own redo
// that we register later.
controller.closeTabImmediately(registerRedo: false)
}
}
if let undoManager {
undoManager.setActionName("Close Other Tabs")
// We need to register an undo that refocuses this window. Otherwise, the
// undo operation above for each tab will steal focus.
undoManager.registerUndo(
withTarget: self,
expiresAfter: undoExpiration
) { target in
DispatchQueue.main.async {
target.window?.makeKeyAndOrderFront(nil)
}
// Register redo action
undoManager.registerUndo(
withTarget: target,
expiresAfter: target.undoExpiration
) { target in
target.closeOtherTabsImmediately()
}
}
}
}
/// Closes the current window (including any other tabs) immediately and without
/// confirmation. This will setup proper undo state so the action can be undone.
@@ -739,6 +806,9 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
alert.alertStyle = .warning
alert.beginSheetModal(for: alertWindow, completionHandler: { response in
if (response == .alertFirstButtonReturn) {
// This is important so that we avoid losing focus when Stage
// Manager is used (#8336)
alert.window.orderOut(nil)
closeAllWindowsImmediately()
}
})
@@ -1007,6 +1077,38 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
}
}
@IBAction func closeOtherTabs(_ sender: Any?) {
guard let window = window else { return }
guard let tabGroup = window.tabGroup else { return }
// If we only have one window then we have no other tabs to close
guard tabGroup.windows.count > 1 else { return }
// Check if we have to confirm close.
guard tabGroup.windows.contains(where: { window in
// Ignore ourself
if window == self.window { return false }
// Ignore non-terminals
guard let controller = window.windowController as? TerminalController else {
return false
}
// Check if any surfaces require confirmation
return controller.surfaceTree.contains(where: { $0.needsConfirmQuit })
}) else {
self.closeOtherTabsImmediately()
return
}
confirmClose(
messageText: "Close Other Tabs?",
informativeText: "At least one other tab still has a running process. If you close the tab the process will be killed."
) {
self.closeOtherTabsImmediately()
}
}
@IBAction func returnToDefaultSize(_ sender: Any?) {
guard let defaultSize else { return }
window?.setFrame(defaultSize, display: true)
@@ -1190,6 +1292,12 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
closeTab(self)
}
@objc private func onCloseOtherTabs(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(target) else { return }
closeOtherTabs(self)
}
@objc private func onCloseWindow(notification: SwiftUI.Notification) {
guard let target = notification.object as? Ghostty.SurfaceView else { return }
guard surfaceTree.contains(target) else { return }

View File

@@ -70,4 +70,39 @@ extension Ghostty.Action {
}
}
}
struct ProgressReport {
enum State {
case remove
case set
case error
case indeterminate
case pause
init(_ c: ghostty_action_progress_report_state_e) {
switch c {
case GHOSTTY_PROGRESS_STATE_REMOVE:
self = .remove
case GHOSTTY_PROGRESS_STATE_SET:
self = .set
case GHOSTTY_PROGRESS_STATE_ERROR:
self = .error
case GHOSTTY_PROGRESS_STATE_INDETERMINATE:
self = .indeterminate
case GHOSTTY_PROGRESS_STATE_PAUSE:
self = .pause
default:
self = .remove
}
}
}
let state: State
let progress: UInt8?
init(c: ghostty_action_progress_report_s) {
self.state = State(c.state)
self.progress = c.progress >= 0 ? UInt8(c.progress) : nil
}
}
}

View File

@@ -455,7 +455,7 @@ extension Ghostty {
newSplit(app, target: target, direction: action.action.new_split)
case GHOSTTY_ACTION_CLOSE_TAB:
closeTab(app, target: target)
closeTab(app, target: target, mode: action.action.close_tab_mode)
case GHOSTTY_ACTION_CLOSE_WINDOW:
closeWindow(app, target: target)
@@ -543,6 +543,9 @@ extension Ghostty {
case GHOSTTY_ACTION_KEY_SEQUENCE:
keySequence(app, target: target, v: action.action.key_sequence)
case GHOSTTY_ACTION_PROGRESS_REPORT:
progressReport(app, target: target, v: action.action.progress_report)
case GHOSTTY_ACTION_CONFIG_CHANGE:
configChange(app, target: target, v: action.action.config_change)
@@ -778,20 +781,34 @@ extension Ghostty {
}
}
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s, mode: ghostty_action_close_tab_mode_e) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("close tab does nothing with an app target")
Ghostty.logger.warning("close tabs does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
NotificationCenter.default.post(
name: .ghosttyCloseTab,
object: surfaceView
)
switch (mode) {
case GHOSTTY_ACTION_CLOSE_TAB_MODE_THIS:
NotificationCenter.default.post(
name: .ghosttyCloseTab,
object: surfaceView
)
return
case GHOSTTY_ACTION_CLOSE_TAB_MODE_OTHER:
NotificationCenter.default.post(
name: .ghosttyCloseOtherTabs,
object: surfaceView
)
return
default:
assertionFailure()
}
default:
@@ -1509,6 +1526,33 @@ extension Ghostty {
assertionFailure()
}
}
private static func progressReport(
_ app: ghostty_app_t,
target: ghostty_target_s,
v: ghostty_action_progress_report_s) {
switch (target.tag) {
case GHOSTTY_TARGET_APP:
Ghostty.logger.warning("progress report does nothing with an app target")
return
case GHOSTTY_TARGET_SURFACE:
guard let surface = target.target.surface else { return }
guard let surfaceView = self.surfaceView(from: surface) else { return }
let progressReport = Ghostty.Action.ProgressReport(c: v)
DispatchQueue.main.async {
if progressReport.state == .remove {
surfaceView.progressReport = nil
} else {
surfaceView.progressReport = progressReport
}
}
default:
assertionFailure()
}
}
private static func configReload(
_ app: ghostty_app_t,

View File

@@ -504,6 +504,14 @@ extension Ghostty {
let str = String(cString: ptr)
return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
}
var quickTerminalSize: QuickTerminalSize {
guard let config = self.config else { return QuickTerminalSize() }
var v = ghostty_config_quick_terminal_size_s()
let key = "quick-terminal-size"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return QuickTerminalSize() }
return QuickTerminalSize(from: v)
}
#endif
var resizeOverlay: ResizeOverlay {

View File

@@ -329,6 +329,9 @@ extension Notification.Name {
/// Close tab
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
/// Close other tabs
static let ghosttyCloseOtherTabs = Notification.Name("com.mitchellh.ghostty.closeOtherTabs")
/// Close window
static let ghosttyCloseWindow = Notification.Name("com.mitchellh.ghostty.closeWindow")

View File

@@ -113,6 +113,11 @@ extension Ghostty {
}
}
.ghosttySurfaceView(surfaceView)
// Progress report overlay
if let progressReport = surfaceView.progressReport {
ProgressReportOverlay(report: progressReport)
}
#if canImport(AppKit)
// If we are in the middle of a key sequence, then we show a visual element. We only
@@ -267,6 +272,49 @@ extension Ghostty {
}
}
// Progress report overlay that shows a progress bar at the top of the terminal
struct ProgressReportOverlay: View {
let report: Action.ProgressReport
@ViewBuilder
private var progressBar: some View {
if let progress = report.progress {
// Determinate progress bar
ProgressView(value: Double(progress), total: 100)
.progressViewStyle(.linear)
.tint(report.state == .error ? .red : report.state == .pause ? .orange : nil)
.animation(.easeInOut(duration: 0.2), value: progress)
} else {
// Indeterminate states
switch report.state {
case .indeterminate:
ProgressView()
.progressViewStyle(.linear)
case .error:
ProgressView()
.progressViewStyle(.linear)
.tint(.red)
case .pause:
Rectangle().fill(Color.orange)
default:
EmptyView()
}
}
}
var body: some View {
VStack(spacing: 0) {
progressBar
.scaleEffect(x: 1, y: 0.5, anchor: .center)
.frame(height: 2)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.allowsHitTesting(false)
}
}
// This is the resize overlay that shows on top of a surface to show the current
// size during a resize operation.
struct SurfaceResizeOverlay: View {
@@ -424,6 +472,9 @@ extension Ghostty {
/// Extra input to send as stdin
var initialInput: String? = nil
/// Wait after the command
var waitAfterCommand: Bool = false
init() {}
@@ -475,6 +526,9 @@ extension Ghostty {
// Zero is our default value that means to inherit the font size.
config.font_size = fontSize ?? 0
// Set wait after command
config.wait_after_command = waitAfterCommand
// Use withCString to ensure strings remain valid for the duration of the closure
return try workingDirectory.withCString { cWorkingDir in

View File

@@ -41,6 +41,23 @@ extension Ghostty {
// The hovered URL string
@Published var hoverUrl: String? = nil
// The progress report (if any)
@Published var progressReport: Action.ProgressReport? = nil {
didSet {
// Cancel any existing timer
progressReportTimer?.invalidate()
progressReportTimer = nil
// If we have a new progress report, start a timer to remove it after 15 seconds
if progressReport != nil {
progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
self?.progressReport = nil
self?.progressReportTimer = nil
}
}
}
}
// The currently active key sequence. The sequence is not active if this is empty.
@Published var keySequence: [KeyboardShortcut] = []
@@ -142,6 +159,9 @@ extension Ghostty {
// A timer to fallback to ghost emoji if no title is set within the grace period
private var titleFallbackTimer: Timer?
// Timer to remove progress report after 15 seconds
private var progressReportTimer: Timer?
// This is the title from the terminal. This is nil if we're currently using
// the terminal title as the main title property. If the title is set manually
@@ -348,6 +368,9 @@ extension Ghostty {
// Remove any notifications associated with this surface
let identifiers = Array(self.notificationIdentifiers)
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
// Cancel progress report timer
progressReportTimer?.invalidate()
}
func focusDidChange(_ focused: Bool) {
@@ -1241,7 +1264,7 @@ extension Ghostty {
var key_ev = event.ghosttyKeyEvent(action, translationMods: translationEvent?.modifierFlags)
key_ev.composing = composing
// For text, we only encode UTF8 if we don't have a single control
// character. Control characters are encoded by Ghostty itself.
// Without this, `ctrl+enter` does the wrong thing.
@@ -1660,8 +1683,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
}
// Ghostty will tell us where it thinks an IME keyboard should render.
var x: Double = 0;
var y: Double = 0;
var x: Double = 0
var y: Double = 0
var width: Double = cellSize.width
var height: Double = cellSize.height
// QuickLook never gives us a matching range to our selection so if we detect
// this then we return the top-left selection point rather than the cursor point.
@@ -1679,15 +1704,19 @@ extension Ghostty.SurfaceView: NSTextInputClient {
// Free our text
ghostty_surface_free_text(surface, &text)
} else {
ghostty_surface_ime_point(surface, &x, &y)
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
}
} else {
ghostty_surface_ime_point(surface, &x, &y)
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
}
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
// bottom-left since that is what UIKit expects
let viewRect = NSMakeRect(x, frame.size.height - y, 0, 0)
let viewRect = NSMakeRect(
x,
frame.size.height - y,
max(width, cellSize.width),
max(height, cellSize.height))
// Convert the point to the window coordinates
let winRect = self.convert(viewRect, to: nil)

View File

@@ -30,6 +30,9 @@ extension Ghostty {
// The hovered URL
@Published var hoverUrl: String? = nil
// The progress report (if any)
@Published var progressReport: Action.ProgressReport? = nil
// The time this surface last became focused. This is a ContinuousClock.Instant
// on supported platforms.

View File

@@ -407,8 +407,14 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
self.styleMask = window.styleMask
self.toolbar = window.toolbar
self.toolbarStyle = window.toolbarStyle
self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers
self.dock = window.screen?.hasDock ?? false
self.titlebarAccessoryViewControllers = if (window.hasTitleBar) {
// Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash.
window.titlebarAccessoryViewControllers
} else {
[]
}
if let cgWindowId = window.cgWindowId {
// We hide the menu only if this window is not on any fullscreen

View File

@@ -9,6 +9,7 @@
# - build.zig.zon.nix
# - build.zig.zon.txt
# - build.zig.zon.json
# - flatpak/zig-packages.json
#
# All of these are auto-generated and should not be edited manually.
@@ -34,8 +35,8 @@ help() {
echo "commit, and submit a PR with the update:"
echo ""
echo " ./nix/build-support/check-zig-cache-hash.sh --update"
echo " git add build.zig.zon.nix build.zig.zon.txt build.zig.zon.json"
echo " git commit -m \"nix: update build.zig.zon.nix build.zig.zon.txt build.zig.zon.json\""
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 ""
}
@@ -44,6 +45,7 @@ BUILD_ZIG_ZON="$ROOT/build.zig.zon"
BUILD_ZIG_ZON_NIX="$ROOT/build.zig.zon.nix"
BUILD_ZIG_ZON_TXT="$ROOT/build.zig.zon.txt"
BUILD_ZIG_ZON_JSON="$ROOT/build.zig.zon.json"
ZIG_PACKAGES_JSON="$ROOT/flatpak/zig-packages.json"
if [ -f "${BUILD_ZIG_ZON_NIX}" ]; then
OLD_HASH_NIX=$(sha512sum "${BUILD_ZIG_ZON_NIX}" | awk '{print $1}')
@@ -69,27 +71,40 @@ elif [ "$1" != "--update" ]; then
exit 1
fi
zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json"
if [ -f "${ZIG_PACKAGES_JSON}" ]; then
OLD_HASH_FLATPAK=$(sha512sum "${ZIG_PACKAGES_JSON}" | awk '{print $1}')
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: flatpak/zig-packages.json missing."
help
exit 1
fi
zon2nix "$BUILD_ZIG_ZON" --nix "$WORK_DIR/build.zig.zon.nix" --txt "$WORK_DIR/build.zig.zon.txt" --json "$WORK_DIR/build.zig.zon.json" --flatpak "$WORK_DIR/zig-packages.json"
alejandra --quiet "$WORK_DIR/build.zig.zon.nix"
prettier --write "$WORK_DIR/build.zig.zon.json"
prettier --log-level warn --write "$WORK_DIR/build.zig.zon.json"
prettier --log-level warn --write "$WORK_DIR/zig-packages.json"
NEW_HASH_NIX=$(sha512sum "$WORK_DIR/build.zig.zon.nix" | awk '{print $1}')
NEW_HASH_TXT=$(sha512sum "$WORK_DIR/build.zig.zon.txt" | awk '{print $1}')
NEW_HASH_JSON=$(sha512sum "$WORK_DIR/build.zig.zon.json" | awk '{print $1}')
NEW_HASH_FLATPAK=$(sha512sum "$WORK_DIR/zig-packages.json" | awk '{print $1}')
if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ]; then
if [ "${OLD_HASH_NIX}" == "${NEW_HASH_NIX}" ] && [ "${OLD_HASH_TXT}" == "${NEW_HASH_TXT}" ] && [ "${OLD_HASH_JSON}" == "${NEW_HASH_JSON}" ] && [ "${OLD_HASH_FLATPAK}" == "${NEW_HASH_FLATPAK}" ]; then
echo -e "\nOK: build.zig.zon.nix unchanged."
echo -e "OK: build.zig.zon.txt unchanged."
echo -e "OK: build.zig.zon.json unchanged."
echo -e "OK: flatpak/zig-packages.json unchanged."
exit 0
elif [ "$1" != "--update" ]; then
echo -e "\nERROR: build.zig.zon.nix, build.zig.zon.txt, or build.zig.zon.json needs to be updated.\n"
echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}"
echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}"
echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}"
echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}"
echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}"
echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}"
echo " * Old build.zig.zon.nix hash: ${OLD_HASH_NIX}"
echo " * New build.zig.zon.nix hash: ${NEW_HASH_NIX}"
echo " * Old build.zig.zon.txt hash: ${OLD_HASH_TXT}"
echo " * New build.zig.zon.txt hash: ${NEW_HASH_TXT}"
echo " * Old build.zig.zon.json hash: ${OLD_HASH_JSON}"
echo " * New build.zig.zon.json hash: ${NEW_HASH_JSON}"
echo " * Old flatpak/zig-packages.json hash: ${OLD_HASH_FLATPAK}"
echo " * New flatpak/zig-packages.json hash: ${NEW_HASH_FLATPAK}"
help
exit 1
else
@@ -99,6 +114,8 @@ else
echo -e "OK: build.zig.zon.txt updated."
mv "$WORK_DIR/build.zig.zon.json" "$BUILD_ZIG_ZON_JSON"
echo -e "OK: build.zig.zon.json updated."
mv "$WORK_DIR/zig-packages.json" "$ZIG_PACKAGES_JSON"
echo -e "OK: flatpak/zig-packages.json updated."
exit 0
fi

View File

@@ -2,14 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Damyan Bogoev <damyan.bogoev@gmail.com>, 2025.
# reo101 <pavel.atanasov2001@gmail.com>, 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-05-19 11:34+0300\n"
"Last-Translator: Damyan Bogoev <damyan.bogoev@gmail.com>\n"
"PO-Revision-Date: 2025-08-22 14:52+0300\n"
"Last-Translator: reo101 <pavel.atanasov2001@gmail.com>\n"
"Language-Team: Bulgarian <dict@ludost.net>\n"
"Language: bg\n"
"MIME-Version: 1.0\n"
@@ -208,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
@@ -278,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"

View File

@@ -2,14 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Francesc Arpi <francesc.arpi@gmail.com>, 2025.
# Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>, 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-03-20 08:07+0100\n"
"Last-Translator: Francesc Arpi <francesc.arpi@gmail.com>\n"
"PO-Revision-Date: 2025-08-24 19:22+0200\n"
"Last-Translator: Kristofer Soler <31729650+KristoferSoler@users.noreply.github.com>\n"
"Language-Team: \n"
"Language: ca\n"
"MIME-Version: 1.0\n"
@@ -87,7 +88,7 @@ msgstr "Divideix a la dreta"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Executa una ordre…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -160,7 +161,7 @@ msgstr "Obre la configuració"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Paleta de comandes"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -208,12 +209,12 @@ msgstr "Permet"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Recorda lopció per a aquest panell dividit"
#: 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 "Recarrega la configuració per tornar a mostrar aquest missatge"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -278,15 +279,15 @@ msgstr "Copiat al porta-retalls"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Porta-retalls netejat"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Comanda completada amb èxit"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Comanda fallida"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -298,7 +299,7 @@ msgstr "Mostra les pestanyes obertes"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Nova divisió"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -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-06 14:57+0100\n"
"PO-Revision-Date: 2025-08-25 19:38+0100\n"
"Last-Translator: Robin <r@rpfaeffle.com>\n"
"Language-Team: German <translation-team-de@lists.sourceforge.net>\n"
"Language: de\n"
@@ -39,7 +39,7 @@ 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 ""
msgstr "Konfigurationsfehler"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
@@ -47,11 +47,14 @@ msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Ein oder mehrere Konfigurationsfehler wurden gefunden. Bitte überprüfe "
"die untenstehenden Fehler und lade entweder deine Konfiguration erneut oder "
"ignoriere die Fehler."
#: 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 ""
msgstr "Ignorieren"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:10
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:97
@@ -86,7 +89,7 @@ msgstr "Fenster nach rechts teilen"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Einen Befehl ausführen…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -159,7 +162,7 @@ msgstr "Konfiguration öffnen"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Befehlspalette"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -207,12 +210,13 @@ msgstr "Erlauben"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Auswahl für dieses geteilte Fenster beibehalten"
#: 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 ""
"Lade die Konfiguration erneut, um diese Eingabeaufforderung erneut anzuzeigen"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -277,15 +281,15 @@ msgstr "In die Zwischenablage kopiert"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Zwischenablage geleert"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Befehl erfolgreich"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Befehl fehlgeschlagen"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -297,7 +301,7 @@ msgstr "Offene Tabs einblenden"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Neues geteiltes Fenster"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,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-05-19 20:17-0300\n"
"PO-Revision-Date: 2025-08-22 09:35-0300\n"
"Last-Translator: Alan Moyano <alanmoyano203@gmail.com>\n"
"Language-Team: Argentinian <es@tp.org.es>\n"
"Language: es_AR\n"
@@ -208,12 +208,12 @@ msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Recordar elección para esta división"
#: 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 "Recargar la configuración para volver a mostrar este mensaje"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -278,15 +278,15 @@ msgstr "Copiado al portapapeles"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Portapapeles limpiado"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Comando ejecutado correctamente"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "El comando ha fallado"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -8,7 +8,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-28 17:46+0200\n"
"PO-Revision-Date: 2025-08-23 17:46+0200\n"
"Last-Translator: Miguel Peredo <miguelp@quientienemail.com>\n"
"Language-Team: Spanish <es@tp.org.es>\n"
"Language: es_BO\n"
@@ -87,7 +87,7 @@ msgstr "Dividir a la derecha"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Ejecutar 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
@@ -160,7 +160,7 @@ msgstr "Abrir configuración"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Paleta de comandos"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -208,12 +208,12 @@ msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Recordar su elección para esta división de ventana"
#: 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 "Recargar configuración para mostrar este aviso nuevamente"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -278,15 +278,15 @@ msgstr "Copiado al portapapeles"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "El portapapeles está limpio"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Comando ejecutado con éxito"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Comando fallido"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -298,7 +298,7 @@ msgstr "Ver pestañas abiertas"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Nueva ventana dividida"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,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-22 09:31+0100\n"
"PO-Revision-Date: 2025-08-23 21:01+0200\n"
"Last-Translator: Kirwiisp <swiip__@hotmail.com>\n"
"Language-Team: French <traduc@traduc.org>\n"
"Language: fr\n"
@@ -88,7 +88,7 @@ msgstr "Panneau à droite"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Exécuter une commande…"
#: 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 "Ouvrir la configuration"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Palette de commandes"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -209,12 +209,12 @@ msgstr "Autoriser"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Se rappeler du choix pour ce panneau"
#: 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 "Recharger la configuration pour afficher à nouveau ce message"
#: 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 "Copié dans le presse-papiers"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Presse-papiers vidé"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Commande réussie"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "La commande a échoué"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@@ -299,7 +299,7 @@ msgstr "Voir les onglets ouverts"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Nouveau panneau"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,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-06-29 21:15+0100\n"
"PO-Revision-Date: 2025-08-26 15:46+0100\n"
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
"Language-Team: Irish <gaeilge-gnulinux@lists.sourceforge.net>\n"
"Language: ga\n"
@@ -16,7 +16,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n"
"X-Generator: Poedit 3.4.4\n"
"X-Generator: Poedit 3.4.2\n"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
@@ -209,12 +209,12 @@ msgstr "Ceadaigh"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Sábháil an rogha don scoilt seo"
#: 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 "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -280,15 +280,15 @@ msgstr "Cóipeáilte chuig an ghearrthaisce"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Gearrthaisce glanta"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "D'éirigh leis an ordú"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Theip ar an ordú"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -2,15 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Sl (Shahaf Levi), Sl's Repository Ltd <ghostty@slsrepo.com>, 2025.
# CraziestOwl <craziestowl@proton.me>, 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-03-13 00:00+0000\n"
"Last-Translator: Sl (Shahaf Levi), Sl's Repository Ltd <ghostty@slsrepo."
"com>\n"
"PO-Revision-Date: 2025-08-23 08:00+0300\n"
"Last-Translator: CraziestOwl <craziestowl@proton.me>\n"
"Language-Team: Hebrew <he_IL@lists.sourceforge.net>\n"
"Language: he\n"
"MIME-Version: 1.0\n"
@@ -276,15 +276,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"

320
po/hu_HU.UTF-8.po Normal file
View File

@@ -0,0 +1,320 @@
# Hungarian translations for com.mitchellh.ghostty package.
# Copyright (C) 2025 Mitchell Hashimoto
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Balázs Szücs <bszucs1209@gmail.com>, 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-23 17:14+0200\n"
"Last-Translator: Balázs Szücs <bszucs1209@gmail.com>\n"
"Language-Team: Hungarian <translation-team-hu@lists.sourceforge.net>\n"
"Language: hu\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 "Terminál címének módosítása"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Hagyja üresen az alapértelmezett cím visszaállításához."
#: 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 "Mégse"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
msgstr "Rendben"
#: 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 "Konfigurációs hibák"
#: 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 ""
"Egy vagy több konfigurációs hiba található. Kérjük, ellenőrizze az alábbi "
"hibákat, és frissítse a konfigurációt, vagy hagyja figyelmen kívül ezeket a "
"hibákat."
#: 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 "Figyelmen kívül hagyás"
#: 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 "Konfiguráció frissítése"
#: 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 "Felosztás felfelé"
#: 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 "Felosztás lefelé"
#: 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 "Felosztás balra"
#: 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 "Felosztás jobbra"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr "Parancs végrehajtása…"
#: 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 "Másolás"
#: 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 "Beillesztés"
#: 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 "Törlés"
#: 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 "Visszaállítás"
#: 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 "Felosztás"
#: 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 "Cím módosítása…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:59
msgid "Tab"
msgstr "Fül"
#: 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 "Új fül"
#: 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 "Fül bezárása"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:73
msgid "Window"
msgstr "Ablak"
#: 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 "Új ablak"
#: 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 "Ablak bezárása"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Konfiguráció"
#: 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 "Konfiguráció megnyitása"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr "Parancspaletta"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
msgstr "Terminálvizsgáló"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:107
#: src/apprt/gtk/Window.zig:1038
msgid "About Ghostty"
msgstr "A Ghostty névjegye"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:112
msgid "Quit"
msgstr "Kilépés"
#: 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 "Vágólap-hozzáférés engedélyezése"
#: 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 ""
"Egy alkalmazás megpróbál olvasni a vágólapról. A vágólap jelenlegi tartalma "
"lent látható."
#: 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 "Elutasítás"
#: 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 "Engedélyezés"
#: 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 "Választás megjegyzése erre a felosztásra"
#: 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 "Konfiguráció frissítése a kérdés újbóli megjelenítéséhez"
#: 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 ""
"Egy alkalmazás megpróbál írni a vágólapra. A vágólap jelenlegi tartalma lent "
"látható."
#: 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 "Figyelem: potenciálisan veszélyes beillesztés"
#: 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 ""
"Ennek a szövegnek a terminálba való beillesztése veszélyes lehet, mivel "
"néhány parancs végrehajtásra kerülhet."
#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531
msgid "Close"
msgstr "Bezárás"
#: src/apprt/gtk/CloseDialog.zig:87
msgid "Quit Ghostty?"
msgstr "Kilép a Ghostty-ból?"
#: src/apprt/gtk/CloseDialog.zig:88
msgid "Close Window?"
msgstr "Ablak bezárása?"
#: src/apprt/gtk/CloseDialog.zig:89
msgid "Close Tab?"
msgstr "Fül bezárása?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "Felosztás bezárása?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
msgstr "Minden terminál munkamenet lezárul."
#: src/apprt/gtk/CloseDialog.zig:97
msgid "All terminal sessions in this window will be terminated."
msgstr "Ebben az ablakban minden terminál munkamenet lezárul."
#: src/apprt/gtk/CloseDialog.zig:98
msgid "All terminal sessions in this tab will be terminated."
msgstr "Ezen a fülön minden terminál munkamenet lezárul."
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr "Ebben a felosztásban a jelenleg futó folyamat lezárul."
#: src/apprt/gtk/Surface.zig:1266
msgid "Copied to clipboard"
msgstr "Vágólapra másolva"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr "Vágólap törölve"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr "Parancs sikeres"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr "Parancs sikertelen"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
msgstr "Főmenü"
#: src/apprt/gtk/Window.zig:239
msgid "View Open Tabs"
msgstr "Megnyitott fülek megtekintése"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr "Új felosztás"
#: src/apprt/gtk/Window.zig:329
msgid ""
"⚠️ You're running a debug build of Ghostty! Performance will be degraded."
msgstr ""
"⚠️ A Ghostty hibakereső verzióját futtatja! A teljesítmény csökkenni fog."
#: src/apprt/gtk/Window.zig:775
msgid "Reloaded the configuration"
msgstr "Konfiguráció frissítve"
#: src/apprt/gtk/Window.zig:1019
msgid "Ghostty Developers"
msgstr "Ghostty fejlesztők"
#: src/apprt/gtk/inspector.zig:144
msgid "Ghostty: Terminal Inspector"
msgstr "Ghostty: Terminálvizsgáló"

View File

@@ -2,14 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Andrej Daskalov <andrej.daskalov@gmail.com>, 2025.
# Marija Gjorgjieva Gjondeva <mgjorgjieva2013@gmail.com>, 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-03-23 14:17+0100\n"
"Last-Translator: Andrej Daskalov <andrej.daskalov@gmail.com>\n"
"PO-Revision-Date: 2025-08-25 22:17+0200\n"
"Last-Translator: Marija Gjorgjieva Gjondeva <mgjorgjieva2013@gmail.com>\n"
"Language-Team: Macedonian\n"
"Language: mk\n"
"MIME-Version: 1.0\n"
@@ -87,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
@@ -160,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"
@@ -208,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
@@ -278,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"
@@ -298,7 +299,7 @@ msgstr "Прегледај отворени јазичиња"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Нова поделба"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -1,7 +1,7 @@
# Norwegian Bokmal translations for com.mitchellh.ghostty package.
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Hanna Rose <hanna@hanna.lol>, 2025.
# Hanna Rose <me@hanna.lol>, 2025.
# Uzair Aftab <uzaaft@outlook.com>, 2025.
# Christoffer Tønnessen <christoffer@cto.gg>, 2025.
# cryptocode <cryptocode@zolo.io>, 2025.
@@ -11,8 +11,8 @@ 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-04-14 16:25+0200\n"
"Last-Translator: cryptocode <cryptocode@zolo.io>\n"
"PO-Revision-Date: 2025-08-23 12:52+0000\n"
"Last-Translator: Hanna Rose <me@hanna.lol>\n"
"Language-Team: Norwegian Bokmal <l10n-no@lister.huftis.org>\n"
"Language: nb\n"
"MIME-Version: 1.0\n"
@@ -90,7 +90,7 @@ msgstr "Del til høyre"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Kjør en kommando..."
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@@ -163,7 +163,7 @@ msgstr "Åpne konfigurasjon"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Kommandopalett"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -211,12 +211,12 @@ msgstr "Tillat"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Husk valget for dette delte vinduet?"
#: 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 "Last inn konfigurasjonen på nytt for å vise denne meldingen igjen"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -281,15 +281,15 @@ msgstr "Kopiert til utklippstavlen"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Utklippstavle tømt"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Kommando lyktes"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Kommando mislyktes"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -3,14 +3,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Gustavo Peres <gsodevel@gmail.com>, 2025.
# Guilherme Tiscoski <github@guilhermetiscoski.com>, 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-06-20 10:19-0300\n"
"Last-Translator: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>\n"
"PO-Revision-Date: 2025-08-25 11:46-0500\n"
"Last-Translator: Guilherme Tiscoski <github@guihermetiscoski.com>\n"
"Language-Team: Brazilian Portuguese <ldpbr-translation@lists.sourceforge."
"net>\n"
"Language: pt_BR\n"
@@ -89,7 +90,7 @@ msgstr "Dividir à direita"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Executar um 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
@@ -162,7 +163,7 @@ msgstr "Abrir configuração"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Paleta de comandos"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -210,12 +211,12 @@ msgstr "Permitir"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Lembrar escolha para esta divisão"
#: 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 "Recarregue a configuração para mostrar este aviso novamente"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@@ -280,15 +281,15 @@ msgstr "Copiado para a área de transferência"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Área de transferência limpa"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Comando executado com sucesso"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Comando falhou"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -3,15 +3,16 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# blackzeshi <sergey_zhuzhgov@mail.ru>, 2025.
#
# Ivan Bastrakov <bastaynav@proton.me>, 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-03-24 00:01+0500\n"
"Last-Translator: blackzeshi <sergey_zhuzhgov@mail.ru>\n"
"Language-Team: Russian <gnu@d07.ru>\n"
"PO-Revision-Date: 2025-09-03 01:50+0300\n"
"Last-Translator: Ivan Bastrakov <bastaynav@proton.me>\n"
"Language: ru\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -89,7 +90,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
@@ -162,7 +163,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"
@@ -210,12 +211,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
@@ -223,7 +224,8 @@ msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Приложение пытается записать данные в буфер обмена. Эти данные показаны ниже."
"Приложение пытается записать данные в буфер обмена. Текущее содержимое "
"буфера обмена показано ниже."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
@@ -279,15 +281,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 +301,7 @@ msgstr "Просмотреть открытые вкладки"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Новый сплит"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@@ -8,7 +8,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-24 22:01+0300\n"
"PO-Revision-Date: 2025-08-23 17:30+0300\n"
"Last-Translator: Emir SARI <emir_sari@icloud.com>\n"
"Language-Team: Turkish\n"
"Language: tr\n"
@@ -88,7 +88,7 @@ msgstr "Sağa Doğru Böl"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Bir komut çalıştır…"
#: 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 "Yapılandırmayı Aç"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Komut Paleti"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@@ -209,12 +209,12 @@ msgstr "İzin Ver"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Bu bölme için tercihi anımsa"
#: 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 "Bu istemi tekrar göstermek için yapılandırmayı yeniden yükle"
#: 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 "Panoya kopyalandı"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Pano temizlendi"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Komut başarılı oldu"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Komut başarısız oldu"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"

View File

@@ -2,14 +2,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Danylo Zalizchuk <danilmail0110@gmail.com>, 2025.
# Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>, 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-03-16 20:16+0200\n"
"Last-Translator: Danylo Zalizchuk <danilmail0110@gmail.com>\n"
"PO-Revision-Date: 2025-08-25 19:59+0100\n"
"Last-Translator: Volodymyr Chernetskyi <19735328+chernetskyi@users.noreply.github.com>\n"
"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
"Language: uk\n"
"MIME-Version: 1.0\n"
@@ -20,17 +21,17 @@ msgstr ""
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
msgid "Change Terminal Title"
msgstr "Змінити назву терміналу"
msgstr "Змінити заголовок терміналу"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:6
msgid "Leave blank to restore the default title."
msgstr "Залиште порожнім, щоб відновити назву за замовчуванням."
msgstr "Залиште порожнім, щоб відновити заголовок за замовчуванням."
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:9
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:10 src/apprt/gtk/ui/1.2/ccw-paste.blp:10
#: src/apprt/gtk/CloseDialog.zig:44
msgid "Cancel"
msgstr "Відмінити"
msgstr "Скасувати"
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:10
msgid "OK"
@@ -39,7 +40,7 @@ msgstr "ОК"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:5
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:5
msgid "Configuration Errors"
msgstr "Помилки конфігурації"
msgstr "Помилки налаштування"
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:6
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:6
@@ -47,9 +48,8 @@ msgid ""
"One or more configuration errors were found. Please review the errors below, "
"and either reload your configuration or ignore these errors."
msgstr ""
"Виявлено одну або декілька помилок у конфігурації. Будь ласка, перегляньте "
"наведені нижче помилки і або перезавантажте конфігурацію, або проігноруйте "
"ці помилки."
"Виявлено одну або декілька помилок налаштування. Будь ласка, перегляньте "
"помилки нижче і або перезавантажте налаштування, або проігноруйте ці помилки."
#: src/apprt/gtk/ui/1.5/config-errors-dialog.blp:9
#: src/apprt/gtk/ui/1.2/config-errors-dialog.blp:9
@@ -61,35 +61,35 @@ msgstr "Ігнорувати"
#: 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 "Перезавантажити конфігурацію"
msgstr "Перезавантажити налаштування"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:38
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:50
msgid "Split Up"
msgstr "Розділити панель вгору"
msgstr "Нова панель зверху"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:11
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:43
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:55
msgid "Split Down"
msgstr "Розділити панель вниз"
msgstr "Нова панель знизу"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:16
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:48
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:60
msgid "Split Left"
msgstr "Розділити панель ліворуч"
msgstr "Нова панель ліворуч"
#: src/apprt/gtk/ui/1.0/menu-headerbar-split_menu.blp:21
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:53
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:65
msgid "Split Right"
msgstr "Розділити панель праворуч"
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
@@ -115,7 +115,7 @@ msgstr "Скинути"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:30
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:42
msgid "Split"
msgstr "Розділена панель"
msgstr "Панель"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:33
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:45
@@ -153,16 +153,16 @@ msgstr "Закрити вікно"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:89
msgid "Config"
msgstr "Конфігурація"
msgstr "Налаштування"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:92
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:95
msgid "Open Configuration"
msgstr "Відкрити конфігурацію"
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"
@@ -182,7 +182,7 @@ msgstr "Завершити"
#: 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 "Дозволити доступ до буфера обміну"
msgstr "Надати доступ до буфера обміну"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:7
@@ -190,15 +190,15 @@ msgid ""
"An application is attempting to read from the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Програма намагається прочитати дані з буфера обміну. Нижче показано поточний "
"вміст буфера обміну."
"Програма намагається прочитати дані з буфера обміну. Нижче наведено вміст "
"буфера обміну."
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-read.blp:10
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:10
msgid "Deny"
msgstr "Відхилити"
msgstr "Заборонити"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:11
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:11
@@ -210,12 +210,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
@@ -223,8 +223,8 @@ msgid ""
"An application is attempting to write to the clipboard. The current "
"clipboard contents are shown below."
msgstr ""
"Програма намагається записати дані до буфера обміну. Нижче показано поточний "
"вміст буфера обміну."
"Програма намагається записати дані до буфера обміну. Нижче наведено вміст "
"буфера обміну."
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
msgid "Warning: Potentially Unsafe Paste"
@@ -235,8 +235,8 @@ msgid ""
"Pasting this text into the terminal may be dangerous as it looks like some "
"commands may be executed."
msgstr ""
"Вставка цього тексту в термінал може бути небезпечною, оскільки виглядає "
"так, ніби деякі команди можуть бути виконані."
"Вставка цього тексту в термінал може бути небезпечною, бо схоже, що деякі "
"команди можуть бути виконані."
#: src/apprt/gtk/CloseDialog.zig:47 src/apprt/gtk/Surface.zig:2531
msgid "Close"
@@ -256,7 +256,7 @@ msgstr "Закрити вкладку?"
#: src/apprt/gtk/CloseDialog.zig:90
msgid "Close Split?"
msgstr "Закрити розділену панель?"
msgstr "Закрити панель?"
#: src/apprt/gtk/CloseDialog.zig:96
msgid "All terminal sessions will be terminated."
@@ -272,24 +272,23 @@ msgstr "Всі сесії терміналу в цій вкладці будут
#: src/apprt/gtk/CloseDialog.zig:99
msgid "The currently running process in this split will be terminated."
msgstr ""
"Поточний процес, що виконується в цій розділеній панелі, буде завершено."
msgstr "Процес, що виконується в цій панелі, буде завершено."
#: src/apprt/gtk/Surface.zig:1266
msgid "Copied to clipboard"
msgstr "Скопійовано в буфер обміну"
msgstr "Скопійовано до буферa обміну"
#: 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"
@@ -301,7 +300,7 @@ msgstr "Переглянути відкриті вкладки"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Нова панель"
#: src/apprt/gtk/Window.zig:329
msgid ""
@@ -311,7 +310,7 @@ msgstr ""
#: src/apprt/gtk/Window.zig:775
msgid "Reloaded the configuration"
msgstr "Конфігурацію перезавантажено"
msgstr "Налаштування перезавантажено"
#: src/apprt/gtk/Window.zig:1019
msgid "Ghostty Developers"

View File

@@ -258,6 +258,7 @@ const DerivedConfig = struct {
mouse_shift_capture: configpkg.MouseShiftCapture,
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
macos_option_as_alt: ?configpkg.OptionAsAlt,
selection_clear_on_copy: bool,
selection_clear_on_typing: bool,
vt_kam_allowed: bool,
wait_after_command: bool,
@@ -272,6 +273,7 @@ const DerivedConfig = struct {
title_report: bool,
links: []Link,
link_previews: configpkg.LinkPreviews,
scroll_to_bottom: configpkg.Config.ScrollToBottom,
const Link = struct {
regex: oni.Regex,
@@ -326,6 +328,7 @@ const DerivedConfig = struct {
.mouse_shift_capture = config.@"mouse-shift-capture",
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
.macos_option_as_alt = config.@"macos-option-as-alt",
.selection_clear_on_copy = config.@"selection-clear-on-copy",
.selection_clear_on_typing = config.@"selection-clear-on-typing",
.vt_kam_allowed = config.@"vt-kam-allowed",
.wait_after_command = config.@"wait-after-command",
@@ -340,6 +343,7 @@ const DerivedConfig = struct {
.title_report = config.@"title-report",
.links = links,
.link_previews = config.@"link-previews",
.scroll_to_bottom = config.@"scroll-to-bottom",
// Assignments happen sequentially so we have to do this last
// so that the memory is captured from allocs above.
@@ -1728,6 +1732,7 @@ pub fn pwd(
pub fn imePoint(self: *const Surface) apprt.IMEPos {
self.renderer_state.mutex.lock();
const cursor = self.renderer_state.terminal.screen.cursor;
const preedit_width: usize = if (self.renderer_state.preedit) |preedit| preedit.width() else 0;
self.renderer_state.mutex.unlock();
// TODO: need to handle when scrolling and the cursor is not
@@ -1762,7 +1767,38 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
break :y y;
};
return .{ .x = x, .y = y };
// Our height for now is always just the cell height because our preedit
// rendering only renders in a single line.
const height: f64 = height: {
var height: f64 = @floatFromInt(self.size.cell.height);
height /= content_scale.y;
break :height height;
};
const width: f64 = width: {
var width: f64 = @floatFromInt(preedit_width * self.size.cell.width);
// Our max width is the remaining screen width after the cursor.
// We don't have to deal with wrapping because the preedit doesn't
// wrap right now.
const screen_width: f64 = @floatFromInt(self.size.terminal().width);
const x_offset: f64 = @floatFromInt((cursor.x + 1) * self.size.cell.width);
const max = screen_width - x_offset;
width = @min(width, max);
// Note: we don't apply content scale here because it looks like
// for some reason in macOS its already scaled. I'm not sure why
// that is so I'm going to just leave this comment here so its known
// that I left this out on purpose pending more investigation.
break :width width;
};
return .{
.x = x,
.y = y,
.width = width,
.height = height,
};
}
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
@@ -2280,7 +2316,8 @@ pub fn keyCallback(
try self.setSelection(null);
}
try self.io.terminal.scrollViewport(.{ .bottom = {} });
if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom);
try self.queueRender();
}
@@ -2766,8 +2803,21 @@ pub fn scrollCallback(
// that a wheel tick of 1 results in single scroll event.
const yoff_adjusted: f64 = if (scroll_mods.precision)
yoff
else
yoff * cell_size * self.config.mouse_scroll_multiplier;
else yoff_adjusted: {
// Round out the yoff to an absolute minimum of 1. macos tries to
// simulate precision scrolling with non precision events by
// ramping up the magnitude of the offsets as it detects faster
// scrolling. Single click (very slow) scrolls are reported with a
// magnitude of 0.1 which would normally require a few clicks
// before we register an actual scroll event (depending on cell
// height and the mouse_scroll_multiplier setting).
const yoff_max: f64 = if (yoff > 0)
@max(yoff, 1)
else
@min(yoff, -1);
break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier;
};
// Add our previously saved pending amount to the offset to get the
// new offset value. The signs of the pending and yoff should match
@@ -3555,26 +3605,7 @@ pub fn mouseButtonCallback(
};
switch (self.config.right_click_action) {
.ignore => {
// Return early to skip clearing the selection.
try self.queueRender();
return true;
},
.copy => {
if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
}
},
.@"copy-or-paste" => {
if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
} else {
try self.startClipboardRequest(.standard, .paste);
}
},
.paste => {
try self.startClipboardRequest(.standard, .paste);
},
.ignore => {},
.@"context-menu" => {
// If we already have a selection and the selection contains
// where we clicked then we don't want to modify the selection.
@@ -3588,12 +3619,45 @@ pub fn mouseButtonCallback(
const sel = screen.selectWord(pin) orelse break :sel;
try self.setSelection(sel);
try self.queueRender();
// Don't consume so that we show the context menu in apprt.
return false;
},
}
.copy => {
if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
}
try self.setSelection(null);
try self.queueRender();
try self.setSelection(null);
try self.queueRender();
},
.@"copy-or-paste" => if (self.io.terminal.screen.selection) |sel| {
self.copySelectionToClipboards(sel, &.{.standard});
try self.setSelection(null);
try self.queueRender();
} else {
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
try self.startClipboardRequest(.standard, .paste);
// We don't need to clear selection because we didn't have
// one to begin with.
},
.paste => {
// Before we yield the lock, clear our selection if we have
// one.
try self.setSelection(null);
try self.queueRender();
// Pasting can trigger a lock grab in complete clipboard
// request so we need to unlock.
self.renderer_state.mutex.unlock();
defer self.renderer_state.mutex.lock();
try self.startClipboardRequest(.standard, .paste);
},
}
// Consume the event such that the context menu is not displayed.
return true;
@@ -4482,6 +4546,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
return true;
};
// Clear the selection if configured to do so.
if (self.config.selection_clear_on_copy) {
if (self.setSelection(null)) {
self.queueRender() catch |err| {
log.warn("failed to queue render after clear selection err={}", .{err});
};
} else |err| {
log.warn("failed to clear selection after copy err={}", .{err});
}
}
return true;
}
@@ -4494,19 +4569,32 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
const pos = try self.rt_surface.getCursorPos();
if (try self.linkAtPos(pos)) |link_info| {
// Get the URL text from selection
const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{
.sel = link_info[1],
.trim = self.config.clipboard_trim_trailing_spaces,
})) catch |err| {
log.err("error reading url string err={}", .{err});
return false;
const url_text = switch (link_info[0]) {
.open => url_text: {
// For regex links, get the text from selection
break :url_text (self.io.terminal.screen.selectionString(self.alloc, .{
.sel = link_info[1],
.trim = self.config.clipboard_trim_trailing_spaces,
})) catch |err| {
log.err("error reading url string err={}", .{err});
return false;
};
},
._open_osc8 => url_text: {
// For OSC8 links, get the URI directly from hyperlink data
const uri = self.osc8URI(link_info[1].start()) orelse {
log.warn("failed to get URI for OSC8 hyperlink", .{});
return false;
};
break :url_text try self.alloc.dupeZ(u8, uri);
},
};
defer self.alloc.free(url_text);
self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| {
log.err("error copying url to clipboard err={}", .{err});
return true;
return false;
};
return true;
@@ -4674,10 +4762,13 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
{},
),
.close_tab => return try self.rt_app.performAction(
.close_tab => |v| return try self.rt_app.performAction(
.{ .surface = self },
.close_tab,
{},
switch (v) {
.this => .this,
.other => .other,
},
),
inline .previous_tab,

View File

@@ -70,13 +70,15 @@ pub const Runtime = enum {
gtk,
pub fn default(target: std.Target) Runtime {
// The Linux default is GTK because it is a full featured application.
if (target.os.tag == .linux) return .@"gtk-ng";
// Otherwise, we do NONE so we don't create an exe and we
// create libghostty. On macOS, Xcode is used to build the app
// that links to libghostty.
return .none;
return switch (target.os.tag) {
// The Linux and FreeBSD default is GTK because it is a full
// featured application.
.linux, .freebsd => .@"gtk-ng",
// Otherwise, we do NONE so we don't create an exe and we create
// libghostty. On macOS, Xcode is used to build the app that links
// to libghostty.
else => .none,
};
}
};

View File

@@ -83,8 +83,9 @@ pub const Action = union(Key) {
/// the tab should be opened in a new window.
new_tab,
/// Closes the tab belonging to the currently focused split.
close_tab,
/// Closes the tab belonging to the currently focused split, or all other
/// tabs, depending on the mode.
close_tab: CloseTabMode,
/// Create a new split. The value determines the location of the split
/// relative to the target.
@@ -701,3 +702,11 @@ pub const OpenUrl = struct {
};
}
};
/// sync with ghostty_action_close_tab_mode_e in ghostty.h
pub const CloseTabMode = enum(c_int) {
/// Close the current tab.
this,
/// Close all other tabs.
other,
};

View File

@@ -447,6 +447,9 @@ pub const Surface = struct {
/// Input to send to the command after it is started.
initial_input: ?[*:0]const u8 = null,
/// Wait after the command exits
wait_after_command: bool = false,
};
pub fn init(self: *Surface, app: *App, opts: Options) !void {
@@ -540,6 +543,11 @@ pub const Surface = struct {
);
}
// Wait after command
if (opts.wait_after_command) {
config.@"wait-after-command" = true;
}
// Initialize our surface right away. We're given a view that is
// ready to use.
try self.core_surface.init(
@@ -1814,10 +1822,18 @@ pub const CAPI = struct {
surface.mousePressureCallback(stage, pressure);
}
export fn ghostty_surface_ime_point(surface: *Surface, x: *f64, y: *f64) void {
export fn ghostty_surface_ime_point(
surface: *Surface,
x: *f64,
y: *f64,
width: *f64,
height: *f64,
) void {
const pos = surface.core_surface.imePoint();
x.* = pos.x;
y.* = pos.y;
width.* = pos.width;
height.* = pos.height;
}
/// Request that the surface become closed. This will go through the

View File

@@ -116,6 +116,11 @@ pub const Application = extern struct {
/// and initialization was successful.
transient_cgroup_base: ?[]const u8 = null,
/// This is set to true so long as we request a window exactly
/// once. This prevents quitting the app before we've shown one
/// window.
requested_window: bool = false,
/// This is set to false internally when the event loop
/// should exit and the application should quit. This must
/// only be set by the main loop thread.
@@ -461,11 +466,24 @@ pub const Application = extern struct {
// If the quit timer has expired, quit.
if (priv.quit_timer == .expired) break :q true;
// There's no quit timer running, or it hasn't expired, don't quit.
// If we have no windows attached to our app, also quit.
if (priv.requested_window and @as(
?*glib.List,
self.as(gtk.Application).getWindows(),
) == null) break :q true;
// No quit conditions met
break :q false;
};
if (must_quit) self.quit();
if (must_quit) {
// All must quit scenarios do not need confirmation.
// Furthermore, must quit scenarios may result in a situation
// where its unsafe to even access the app/surface memory
// since its in the process of being freed. We must simply
// begin our exit immediately.
self.quitNow();
}
}
}
@@ -488,7 +506,15 @@ pub const Application = extern struct {
const parent: ?*gtk.Widget = parent: {
const list = gtk.Window.listToplevels();
defer list.free();
const focused = list.findCustom(null, findActiveWindow);
const focused = @as(?*glib.List, list.findCustom(
null,
findActiveWindow,
)) orelse {
// If we have an active surface then we should have
// a window available but in the rare case we don't we
// should exit so we don't crash.
break :parent null;
};
break :parent @ptrCast(@alignCast(focused.f_data));
};
@@ -542,7 +568,7 @@ pub const Application = extern struct {
value: apprt.Action.Value(action),
) !bool {
switch (action) {
.close_tab => return Action.closeTab(target),
.close_tab => return Action.closeTab(target, value),
.close_window => return Action.closeWindow(target),
.config_change => try Action.configChange(
@@ -713,27 +739,24 @@ pub const Application = extern struct {
}
}
fn loadRuntimeCss(
self: *Self,
) Allocator.Error!void {
fn loadRuntimeCss(self: *Self) Allocator.Error!void {
const alloc = self.allocator();
var buf: std.ArrayListUnmanaged(u8) = .empty;
const config = self.private().config.get();
var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048);
defer buf.deinit(alloc);
const writer = buf.writer(alloc);
const config = self.private().config.get();
const window_theme = config.@"window-theme";
const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
try writer.print(
\\widget.unfocused-split {{
\\ opacity: {d:.2};
\\ background-color: rgb({d},{d},{d});
\\}}
\\
, .{
1.0 - config.@"unfocused-split-opacity",
unfocused_fill.r,
@@ -747,6 +770,7 @@ pub const Application = extern struct {
\\ color: rgb({[r]d},{[g]d},{[b]d});
\\ background: rgb({[r]d},{[g]d},{[b]d});
\\}}
\\
, .{
.r = color.r,
.g = color.g,
@@ -759,9 +783,129 @@ pub const Application = extern struct {
\\.window headerbar {{
\\ font-family: "{[font_family]s}";
\\}}
\\
, .{ .font_family = font_family });
}
try loadRuntimeCss414(config, &writer);
try loadRuntimeCss416(config, &writer);
// ensure that we have a sentinel
try writer.writeByte(0);
const data = buf.items[0 .. buf.items.len - 1 :0];
log.debug("runtime CSS is {d} bytes", .{data.len + 1});
// Clears any previously loaded CSS from this provider
loadCssProviderFromData(
self.private().css_provider,
data,
);
}
/// Load runtime CSS for older than GTK 4.16
fn loadRuntimeCss414(
config: *const CoreConfig,
writer: *const std.ArrayListUnmanaged(u8).Writer,
) Allocator.Error!void {
if (gtk_version.runtimeAtLeast(4, 16, 0)) return;
const window_theme = config.@"window-theme";
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
switch (window_theme) {
.ghostty => try writer.print(
\\windowhandle {{
\\ background-color: rgb({d},{d},{d});
\\ color: rgb({d},{d},{d});
\\}}
\\windowhandle:backdrop {{
\\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha);
\\}}
\\
, .{
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
headerbar_foreground.r,
headerbar_foreground.g,
headerbar_foreground.b,
headerbar_background.r,
headerbar_background.g,
headerbar_background.b,
}),
else => {},
}
}
/// Load runtime for GTK 4.16 and newer
fn loadRuntimeCss416(
config: *const CoreConfig,
writer: *const std.ArrayListUnmanaged(u8).Writer,
) Allocator.Error!void {
if (gtk_version.runtimeUntil(4, 16, 0)) return;
const window_theme = config.@"window-theme";
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
try writer.writeAll(
\\/*
\\ * Child Exited Overlay
\\ */
\\
\\.child-exited.normal revealer widget {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--success-bg-color),
\\ transparent 50%
\\ );
\\}
\\
\\.child-exited.abnormal revealer widget {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--error-bg-color),
\\ transparent 50%
\\ );
\\}
\\
\\/*
\\ * Surface
\\ */
\\
\\.surface progressbar.error trough progress {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--error-bg-color),
\\ transparent 50%
\\ );
\\}
\\
\\.surface .bell-overlay {
\\ border-color: color-mix(
\\ in srgb,
\\ var(--accent-color),
\\ transparent 50%
\\ );
\\}
\\
\\/*
\\ * Splits
\\ */
\\
\\.window .split paned > separator {
\\ background-color: color-mix(
\\ in srgb,
\\ var(--window-bg-color),
\\ transparent 0%
\\ );
\\}
\\
);
switch (window_theme) {
.ghostty => try writer.print(
\\:root {{
@@ -794,15 +938,6 @@ pub const Application = extern struct {
}),
else => {},
}
const data = try alloc.dupeZ(u8, buf.items);
defer alloc.free(data);
// Clears any previously loaded CSS from this provider
loadCssProviderFromData(
self.private().css_provider,
data,
);
}
fn loadCustomCss(self: *Self) !void {
@@ -872,7 +1007,8 @@ pub const Application = extern struct {
self.syncActionAccelerator("win.close", .{ .close_window = {} });
self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
self.syncActionAccelerator("win.close-tab::this", .{ .close_tab = .this });
self.syncActionAccelerator("tab.close::this", .{ .close_tab = .this });
self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
@@ -1576,12 +1712,16 @@ pub const Application = extern struct {
/// All apprt action handlers
const Action = struct {
pub fn closeTab(target: apprt.Target) bool {
pub fn closeTab(target: apprt.Target, value: apprt.Action.Value(.close_tab)) bool {
switch (target) {
.app => return false,
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("tab.close", null) != 0;
return surface.as(gtk.Widget).activateAction(
"tab.close",
glib.ext.VariantType.stringFor([:0]const u8),
@as([*:0]const u8, @tagName(value)),
) != 0;
},
}
}
@@ -1853,6 +1993,13 @@ const Action = struct {
self: *Application,
parent: ?*CoreSurface,
) !void {
// Note that we've requested a window at least once. This is used
// to trigger quit on no windows. Note I'm not sure if this is REALLY
// necessary, but I don't want to risk a bug where on a slow machine
// or something we quit immediately after starting up because there
// was a delay in the event loop before we created a Window.
self.private().requested_window = true;
const win = Window.new(self);
initAndShowWindow(self, win, parent);
}

View File

@@ -10,7 +10,7 @@ const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Dialog = @import("dialog.zig").Dialog;
const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
pub const CloseConfirmationDialog = extern struct {
const Self = @This();

View File

@@ -105,6 +105,24 @@ pub const Surface = extern struct {
);
};
pub const @"error" = struct {
pub const name = "error";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
Private,
&Private.offset,
"error",
),
},
);
};
pub const @"font-size-request" = struct {
pub const name = "font-size-request";
const impl = gobject.ext.defineProperty(
@@ -472,6 +490,12 @@ pub const Surface = extern struct {
// false by a parent widget.
bell_ringing: bool = false,
/// True if this surface is in an error state. This is currently
/// a simple boolean with no additional information on WHAT the
/// error state is, because we don't yet need it or use it. For now,
/// if this is true, then it means the terminal is non-functional.
@"error": bool = false,
/// A weak reference to an inspector window.
inspector: ?*InspectorWindow = null,
@@ -571,6 +595,17 @@ pub const Surface = extern struct {
return @intFromBool(config.@"bell-features".border);
}
fn closureStackChildName(
_: *Self,
error_: c_int,
) callconv(.c) ?[*:0]const u8 {
const err = error_ != 0;
return if (err)
glib.ext.dupeZ(u8, "error")
else
glib.ext.dupeZ(u8, "terminal");
}
pub fn toggleFullscreen(self: *Self) void {
signals.@"toggle-fullscreen".impl.emit(
self,
@@ -838,7 +873,7 @@ pub const Surface = extern struct {
// such as single quote on a US international keyboard layout.
if (priv.im_composing) return true;
// If we were composing and now we're not it means that we committed
// If we were composing and now we're not, it means that we committed
// the text. We also don't want to encode a key event for this.
// Example: enable Japanese input method, press "konn" and then
// press enter. The final enter should not be encoded and "konn"
@@ -878,9 +913,24 @@ pub const Surface = extern struct {
// We want to get the physical unmapped key to process physical keybinds.
// (These are keybinds explicitly marked as requesting physical mapping).
const physical_key = keycode: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :keycode entry.key;
} else .unidentified;
const physical_key = keycode: {
const w3c_key: input.Key = w3c: for (input.keycodes.entries) |entry| {
if (entry.native == keycode) break :w3c entry.key;
} else .unidentified;
// If the key should be remappable, then consult the pre-remapped
// XKB keyval/keysym to get the (possibly) remapped key.
//
// See the docs for `shouldBeRemappable` for why we even have to
// do this in the first place.
if (w3c_key.shouldBeRemappable()) {
if (gtk_key.keyFromKeyval(keyval)) |remapped|
break :keycode remapped;
}
// Return the original physical key
break :keycode w3c_key;
};
// Get our modifier for the event
const mods: input.Mods = gtk_key.eventMods(
@@ -1525,6 +1575,12 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
}
pub fn setError(self: *Self, v: bool) void {
const priv = self.private();
priv.@"error" = v;
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
}
fn propConfig(
self: *Self,
_: *gobject.ParamSpec,
@@ -1577,6 +1633,28 @@ pub const Surface = extern struct {
}
}
fn propError(
self: *Self,
_: *gobject.ParamSpec,
_: ?*anyopaque,
) callconv(.c) void {
const priv = self.private();
if (priv.@"error") {
// Ensure we have an opaque background. The window will NOT set
// this if we have transparency set and we need an opaque
// background for the error message to be readable.
self.as(gtk.Widget).addCssClass("background");
} else {
// Regardless of transparency setting, we remove the background
// CSS class from this widget. Parent widgets will set it
// appropriately (see window.zig for example).
self.as(gtk.Widget).removeCssClass("background");
}
// Note above: in both cases setting our error view is handled by
// a Gtk.Stack visible-child-name binding.
}
fn propMouseHoverUrl(
self: *Self,
_: *gobject.ParamSpec,
@@ -1927,8 +2005,11 @@ pub const Surface = extern struct {
// Bell stops ringing if any mouse button is pressed.
self.setBellRinging(false);
// If we don't have focus, grab it.
// Get our surface. If we don't have one, ignore this.
const priv = self.private();
const core_surface = priv.core_surface orelse return;
// If we don't have focus, grab it.
const gl_area_widget = priv.gl_area.as(gtk.Widget);
if (gl_area_widget.hasFocus() == 0) {
_ = gl_area_widget.grabFocus();
@@ -1936,10 +2017,10 @@ pub const Surface = extern struct {
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const consumed = if (priv.core_surface) |surface| consumed: {
const consumed = consumed: {
const gtk_mods = event.getModifierState();
const mods = gtk_key.translateMods(gtk_mods);
break :consumed surface.mouseButtonCallback(
break :consumed core_surface.mouseButtonCallback(
.press,
button,
mods,
@@ -1947,7 +2028,7 @@ pub const Surface = extern struct {
log.warn("error in key callback err={}", .{err});
break :err false;
};
} else false;
};
// If a right click isn't consumed, mouseButtonCallback selects the hovered
// word and returns false. We can use this to handle the context menu
@@ -2288,21 +2369,23 @@ pub const Surface = extern struct {
) callconv(.c) void {
log.debug("realize", .{});
// Make the GL area current so we can detect any OpenGL errors. If
// we have errors here we can't render and we switch to the error
// state.
const priv = self.private();
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is almost always due to a library, driver, or GTK issue", .{});
log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
self.setError(true);
return;
}
// If we already have an initialized surface then we notify it.
// If we don't, we'll initialize it on the first resize so we have
// our proper initial dimensions.
const priv = self.private();
if (priv.core_surface) |v| realize: {
// We need to make the context current so we can call GL functions.
// This is required for all surface operations.
priv.gl_area.makeCurrent();
if (priv.gl_area.getError()) |err| {
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
log.warn("this error is usually due to a driver or gtk bug", .{});
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
break :realize;
}
v.renderer.displayRealized() catch |err| {
log.warn("core displayRealized failed err={}", .{err});
break :realize;
@@ -2647,11 +2730,13 @@ pub const Surface = extern struct {
class.bindTemplateCallback("child_exited_close", &childExitedClose);
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_error", &propError);
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
class.bindTemplateCallback("notify_bell_ringing", &propBellRinging);
class.bindTemplateCallback("should_border_be_shown", &closureShouldBorderBeShown);
class.bindTemplateCallback("stack_child_name", &closureStackChildName);
// Properties
gobject.ext.registerProperties(class, &.{
@@ -2659,6 +2744,7 @@ pub const Surface = extern struct {
properties.config.impl,
properties.@"child-exited".impl,
properties.@"default-size".impl,
properties.@"error".impl,
properties.@"font-size-request".impl,
properties.focused.impl,
properties.@"min-size".impl,

View File

@@ -18,7 +18,6 @@ const gresource = @import("../build/gresource.zig");
const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const SplitTree = @import("split_tree.zig").SplitTree;
const Surface = @import("surface.zig").Surface;
@@ -199,8 +198,11 @@ pub const Tab = extern struct {
}
fn initActionMap(self: *Self) void {
const s_param_type = glib.ext.VariantType.newFor([:0]const u8);
defer s_param_type.free();
const actions = [_]ext.actions.Action(Self){
.init("close", actionClose, null),
.init("close", actionClose, s_param_type),
.init("ring-bell", actionRingBell, null),
};
@@ -314,18 +316,44 @@ pub const Tab = extern struct {
fn actionClose(
_: *gio.SimpleAction,
_: ?*glib.Variant,
param_: ?*glib.Variant,
self: *Self,
) callconv(.c) void {
const param = param_ orelse {
log.warn("tab.close-tab called without a parameter", .{});
return;
};
var str: ?[*:0]const u8 = null;
param.get("&s", &str);
const tab_view = ext.getAncestor(
adw.TabView,
self.as(gtk.Widget),
) orelse return;
const page = tab_view.getPage(self.as(gtk.Widget));
const mode = std.meta.stringToEnum(
apprt.action.CloseTabMode,
std.mem.span(
str orelse {
log.warn("invalid mode provided to tab.close-tab", .{});
return;
},
),
) orelse {
// Need to be defensive here since actions can be triggered externally.
log.warn("invalid mode provided to tab.close-tab: {s}", .{str.?});
return;
};
// Delegate to our parent to handle this, since this will emit
// a close-page signal that the parent can intercept.
tab_view.closePage(page);
switch (mode) {
.this => tab_view.closePage(page),
.other => tab_view.closeOtherPages(page),
}
}
fn actionRingBell(

View File

@@ -320,10 +320,13 @@ pub const Window = extern struct {
/// Setup our action map.
fn initActionMap(self: *Self) void {
const s_variant_type = glib.ext.VariantType.newFor([:0]const u8);
defer s_variant_type.free();
const actions = [_]ext.actions.Action(Self){
.init("about", actionAbout, null),
.init("close", actionClose, null),
.init("close-tab", actionCloseTab, null),
.init("close-tab", actionCloseTab, s_variant_type),
.init("new-tab", actionNewTab, null),
.init("new-window", actionNewWindow, null),
.init("ring-bell", actionRingBell, null),
@@ -961,7 +964,14 @@ pub const Window = extern struct {
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
self.addToast(i18n._("Reloaded the configuration"));
const priv = self.private();
if (priv.config) |config_obj| {
const config = config_obj.get();
if (config.@"app-notifications".@"config-reload") {
self.addToast(i18n._("Reloaded the configuration"));
}
}
self.syncAppearance();
}
@@ -980,6 +990,22 @@ pub const Window = extern struct {
};
}
fn propIsActive(
_: *gtk.Window,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// Don't change urgency if we're not the active window.
if (self.as(gtk.Window).isActive() == 0) return;
self.winproto().setUrgent(false) catch |err| {
log.warn(
"winproto failed to reset urgency={}",
.{err},
);
};
}
fn propGdkSurfaceWidth(
_: *gdk.Surface,
_: *gobject.ParamSpec,
@@ -1656,10 +1682,31 @@ pub const Window = extern struct {
fn actionCloseTab(
_: *gio.SimpleAction,
_: ?*glib.Variant,
param_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.close_tab);
const param = param_ orelse {
log.warn("win.close-tab called without a parameter", .{});
return;
};
var str: ?[*:0]const u8 = null;
param.get("&s", &str);
const mode = std.meta.stringToEnum(
input.Binding.Action.CloseTabMode,
std.mem.span(
str orelse {
log.warn("invalid mode provided to win.close-tab", .{});
return;
},
),
) orelse {
log.warn("invalid mode provided to win.close-tab: {s}", .{str.?});
return;
};
self.performBindingAction(.{ .close_tab = mode });
}
fn actionNewWindow(
@@ -1758,10 +1805,13 @@ pub const Window = extern struct {
native.beep();
}
if (config.@"bell-features".attention) {
if (config.@"bell-features".attention) attention: {
// Dont set urgency if the window is already active.
if (self.as(gtk.Window).isActive() != 0) break :attention;
// Request user attention
self.winproto().setUrgent(true) catch |err| {
log.warn("failed to request user attention={}", .{err});
log.warn("winproto failed to set urgency={}", .{err});
};
}
}
@@ -1905,6 +1955,7 @@ pub const Window = extern struct {
class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
class.bindTemplateCallback("notify_is_active", &propIsActive);
class.bindTemplateCallback("notify_maximized", &propMaximized);
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);

View File

@@ -12,7 +12,7 @@ window.ssd.no-border-radius {
border-radius: 0 0;
}
/*
/*
* GhosttySurface URL overlay
*/
label.url-overlay {
@@ -83,13 +83,13 @@ label.resize-overlay {
*/
.child-exited.normal revealer widget {
background-color: rgba(38, 162, 105, 0.5);
/* after GTK 4.16 is a requirement, switch to the following:
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--success-bg-color), transparent 50%); */
}
.child-exited.abnormal revealer widget {
background-color: rgba(192, 28, 40, 0.5);
/* after GTK 4.16 is a requirement, switch to the following:
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
}
@@ -97,13 +97,15 @@ label.resize-overlay {
* Surface
*/
.surface progressbar.error trough progress {
background-color: rgb(192, 28, 40);
background-color: rgba(192, 28, 40, 0.5);
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent 50%); */
}
.surface .bell-overlay {
border-color: color-mix(in srgb, var(--accent-color), transparent 50%);
border-color: rgba(58, 148, 74, 0.5);
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--accent-color), transparent 50%); */
border-width: 3px;
border-style: solid;
}
@@ -127,6 +129,8 @@ label.resize-overlay {
.window .split paned > separator {
background-color: rgba(250, 250, 250, 1);
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--window-bg-color), transparent 0%); */
background-clip: content-box;
/* This works around the oversized drag area for the right side of GtkPaned.

View File

@@ -0,0 +1,189 @@
//! DBus helper for IPC
const Self = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const apprt = @import("../../../apprt.zig");
const ApprtApp = @import("../App.zig");
/// The target for this IPC.
target: apprt.ipc.Target,
/// Connection to the DBus session bus.
dbus: *gio.DBusConnection,
/// The bus name of the Ghostty instance that we are calling.
bus_name: [:0]const u8,
/// The object path of the Ghostty instance that we are calling.
object_path: [:0]const u8,
/// Used to build the DBus payload.
payload_builder: *glib.VariantBuilder,
/// Used to build the parameters for the IPC.
parameters_builder: *glib.VariantBuilder,
/// Initialize the helper.
pub fn init(alloc: Allocator, target: apprt.ipc.Target, action: [:0]const u8) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!Self {
// Get the appropriate bus name and object path for contacting the
// Ghostty instance we're interested in.
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
.class => |class| result: {
// Force the usage of the class specified on the CLI to determine the
// bus name and object path.
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
std.mem.replaceScalar(u8, object_path, '.', '/');
std.mem.replaceScalar(u8, object_path, '-', '_');
break :result .{ class, object_path };
},
.detect => .{ ApprtApp.application_id, ApprtApp.object_path },
};
errdefer {
switch (target) {
.class => alloc.free(object_path),
.detect => {},
}
}
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
const stderr = std.io.getStdErr().writer();
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
return error.IPCFailed;
}
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
const stderr = std.io.getStdErr().writer();
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
return error.IPCFailed;
}
// Get a connection to the DBus session bus.
const dbus = dbus: {
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const dbus_ = gio.busGetSync(.session, null, &err_);
if (err_) |err| {
const stderr = std.io.getStdErr().writer();
try stderr.print(
"Unable to establish connection to D-Bus session bus: {s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
break :dbus dbus_ orelse {
const stderr = std.io.getStdErr().writer();
try stderr.print("gio.busGetSync returned null\n", .{});
return error.IPCFailed;
};
};
// Set up the payload builder.
const payload_variant_type = glib.VariantType.new("(sava{sv})");
defer glib.free(payload_variant_type);
const payload_builder = glib.VariantBuilder.new(payload_variant_type);
// Add the action name to the payload.
{
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
const bytes = glib.Bytes.new(action.ptr, action.len + 1);
defer bytes.unref();
const value = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
payload_builder.addValue(value);
}
// Set up the parameter builder.
const parameters_variant_type = glib.VariantType.new("av");
defer parameters_variant_type.free();
const parameters_builder = glib.VariantBuilder.new(parameters_variant_type);
return .{
.target = target,
.dbus = dbus,
.bus_name = bus_name,
.object_path = object_path,
.payload_builder = payload_builder,
.parameters_builder = parameters_builder,
};
}
/// Add a parameter to the IPC call.
pub fn addParameter(self: *Self, variant: *glib.Variant) void {
self.parameters_builder.add("v", variant);
}
/// Send the IPC to the remote Ghostty. Once it completes, nothing further
/// should be done with this object other than call `deinit`.
pub fn send(self: *Self) (std.posix.WriteError || apprt.ipc.Errors)!void {
// finish building the parameters
const parameters = self.parameters_builder.end();
// Add the parameters to the payload.
self.payload_builder.addValue(parameters);
// Add the platform data to the payload.
{
const platform_data_variant_type = glib.VariantType.new("a{sv}");
defer platform_data_variant_type.free();
self.payload_builder.open(platform_data_variant_type);
defer self.payload_builder.close();
// We have no platform data.
}
const payload = self.payload_builder.end();
{
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const result_ = self.dbus.callSync(
self.bus_name,
self.object_path,
"org.gtk.Actions",
"Activate",
payload,
null, // We don't care about the return type, we don't do anything with it.
.{}, // no flags
-1, // default timeout
null, // not cancellable
&err_,
);
defer if (result_) |result| result.unref();
if (err_) |err| {
const stderr = std.io.getStdErr().writer();
try stderr.print(
"D-Bus method call returned an error err={s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
}
}
/// Free/unref any data held by this instance.
pub fn deinit(self: *Self, alloc: Allocator) void {
switch (self.target) {
.class => alloc.free(self.object_path),
.detect => {},
}
self.parameters_builder.unref();
self.payload_builder.unref();
self.dbus.unref();
}

View File

@@ -1,11 +1,10 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const gio = @import("gio");
const glib = @import("glib");
const apprt = @import("../../../apprt.zig");
const ApprtApp = @import("../App.zig");
const DBus = @import("DBus.zig");
// Use a D-Bus method call to open a new window on GTK.
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
@@ -22,149 +21,42 @@ const ApprtApp = @import("../App.zig");
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
// ```
pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
const stderr = std.io.getStdErr().writer();
var dbus = try DBus.init(
alloc,
target,
if (value.arguments == null)
"new-window"
else
"new-window-command",
);
defer dbus.deinit(alloc);
// Get the appropriate bus name and object path for contacting the
// Ghostty instance we're interested in.
const bus_name: [:0]const u8, const object_path: [:0]const u8 = switch (target) {
.class => |class| result: {
// Force the usage of the class specified on the CLI to determine the
// bus name and object path.
const object_path = try std.fmt.allocPrintZ(alloc, "/{s}", .{class});
if (value.arguments) |arguments| {
// If `-e` was specified on the command line, the first
// parameter is an array of strings that contain the arguments
// that came after `-e`, which will be interpreted as a command
// to run.
const as_variant_type = glib.VariantType.new("as");
defer as_variant_type.free();
std.mem.replaceScalar(u8, object_path, '.', '/');
std.mem.replaceScalar(u8, object_path, '-', '_');
const s_variant_type = glib.VariantType.new("s");
defer s_variant_type.free();
break :result .{ class, object_path };
},
.detect => .{ ApprtApp.application_id, ApprtApp.object_path },
};
defer {
switch (target) {
.class => alloc.free(object_path),
.detect => {},
var command: glib.VariantBuilder = undefined;
command.init(as_variant_type);
errdefer command.clear();
for (arguments) |argument| {
const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
defer bytes.unref();
const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
command.addValue(string);
}
dbus.addParameter(command.end());
}
if (gio.Application.idIsValid(bus_name.ptr) == 0) {
try stderr.print("D-Bus bus name is not valid: {s}\n", .{bus_name});
return error.IPCFailed;
}
if (glib.Variant.isObjectPath(object_path.ptr) == 0) {
try stderr.print("D-Bus object path is not valid: {s}\n", .{object_path});
return error.IPCFailed;
}
const dbus = dbus: {
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const dbus_ = gio.busGetSync(.session, null, &err_);
if (err_) |err| {
try stderr.print(
"Unable to establish connection to D-Bus session bus: {s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
break :dbus dbus_ orelse {
try stderr.print("gio.busGetSync returned null\n", .{});
return error.IPCFailed;
};
};
defer dbus.unref();
// use a builder to create the D-Bus method call payload
const payload = payload: {
const payload_variant_type = glib.VariantType.new("(sava{sv})");
defer glib.free(payload_variant_type);
// Initialize our builder to build up our parameters
var builder: glib.VariantBuilder = undefined;
builder.init(payload_variant_type);
errdefer builder.clear();
// action
if (value.arguments == null) {
builder.add("s", "new-window");
} else {
builder.add("s", "new-window-command");
}
// parameters
{
const av_variant_type = glib.VariantType.new("av");
defer av_variant_type.free();
var parameters: glib.VariantBuilder = undefined;
parameters.init(av_variant_type);
errdefer parameters.clear();
if (value.arguments) |arguments| {
// If `-e` was specified on the command line, the first
// parameter is an array of strings that contain the arguments
// that came after `-e`, which will be interpreted as a command
// to run.
{
const as = glib.VariantType.new("as");
defer as.free();
var command: glib.VariantBuilder = undefined;
command.init(as);
errdefer command.clear();
for (arguments) |argument| {
command.add("s", argument.ptr);
}
parameters.add("v", command.end());
}
}
builder.addValue(parameters.end());
}
{
const platform_data_variant_type = glib.VariantType.new("a{sv}");
defer platform_data_variant_type.free();
builder.open(platform_data_variant_type);
defer builder.close();
// we have no platform data
}
break :payload builder.end();
};
{
var err_: ?*glib.Error = null;
defer if (err_) |err| err.free();
const result_ = dbus.callSync(
bus_name,
object_path,
"org.gtk.Actions",
"Activate",
payload,
null, // We don't care about the return type, we don't do anything with it.
.{}, // no flags
-1, // default timeout
null, // not cancellable
&err_,
);
defer if (result_) |result| result.unref();
if (err_) |err| {
try stderr.print(
"D-Bus method call returned an error err={s}\n",
.{err.f_message orelse "(unknown)"},
);
return error.IPCFailed;
}
}
try dbus.send();
return true;
}

View File

@@ -7,4 +7,6 @@ template $GhosttyCloseConfirmationDialog: $GhosttyDialog {
cancel: _("Cancel"),
close: _("Close") destructive,
]
close-response: "cancel";
}

View File

@@ -8,146 +8,172 @@ template $GhosttySurface: Adw.Bin {
notify::bell-ringing => $notify_bell_ringing();
notify::config => $notify_config();
notify::error => $notify_error();
notify::mouse-hover-url => $notify_mouse_hover_url();
notify::mouse-hidden => $notify_mouse_hidden();
notify::mouse-shape => $notify_mouse_shape();
Overlay {
focusable: false;
focus-on-click: false;
Stack {
StackPage {
name: "terminal";
child: Box {
hexpand: true;
vexpand: true;
child: Overlay {
focusable: false;
focus-on-click: false;
GLArea gl_area {
realize => $gl_realize();
unrealize => $gl_unrealize();
render => $gl_render();
resize => $gl_resize();
hexpand: true;
vexpand: true;
focusable: true;
focus-on-click: true;
has-stencil-buffer: false;
has-depth-buffer: false;
allowed-apis: gl;
}
child: Box {
hexpand: true;
vexpand: true;
PopoverMenu context_menu {
closed => $context_menu_closed();
menu-model: context_menu_model;
flags: nested;
halign: start;
has-arrow: false;
}
};
GLArea gl_area {
realize => $gl_realize();
unrealize => $gl_unrealize();
render => $gl_render();
resize => $gl_resize();
hexpand: true;
vexpand: true;
focusable: true;
focus-on-click: true;
has-stencil-buffer: false;
has-depth-buffer: false;
allowed-apis: gl;
}
[overlay]
ProgressBar progress_bar_overlay {
styles [
"osd",
]
PopoverMenu context_menu {
closed => $context_menu_closed();
menu-model: context_menu_model;
flags: nested;
halign: start;
has-arrow: false;
}
};
visible: false;
halign: fill;
valign: start;
[overlay]
ProgressBar progress_bar_overlay {
styles [
"osd",
]
visible: false;
halign: fill;
valign: start;
}
[overlay]
// The "border" bell feature is implemented here as an overlay rather than
// just adding a border to the GLArea or other widget for two reasons.
// First, adding a border to an existing widget causes a resize of the
// widget which undesirable side effects. Second, we can make it reactive
// here in the blueprint with relatively little code.
Revealer {
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
transition-type: crossfade;
transition-duration: 500;
Box bell_overlay {
styles [
"bell-overlay",
]
halign: fill;
valign: fill;
}
}
[overlay]
$GhosttySurfaceChildExited child_exited_overlay {
visible: bind template.child-exited;
close-request => $child_exited_close();
}
[overlay]
$GhosttyResizeOverlay resize_overlay {}
[overlay]
Label url_left {
styles [
"background",
"url-overlay",
]
visible: false;
halign: start;
valign: end;
label: bind template.mouse-hover-url;
EventControllerMotion url_ec_motion {
enter => $url_mouse_enter();
leave => $url_mouse_leave();
}
}
[overlay]
Label url_right {
styles [
"background",
"url-overlay",
]
visible: false;
halign: end;
valign: end;
label: bind template.mouse-hover-url;
}
// Event controllers for interactivity
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
EventControllerMotion {
motion => $mouse_motion();
leave => $mouse_leave();
}
EventControllerScroll {
scroll => $scroll();
scroll-begin => $scroll_begin();
scroll-end => $scroll_end();
flags: both_axes;
}
GestureClick {
pressed => $mouse_down();
released => $mouse_up();
button: 0;
}
DropTarget drop_target {
drop => $drop();
actions: copy;
}
};
}
[overlay]
// The "border" bell feature is implemented here as an overlay rather than
// just adding a border to the GLArea or other widget for two reasons.
// First, adding a border to an existing widget causes a resize of the
// widget which undesirable side effects. Second, we can make it reactive
// here in the blueprint with relatively little code.
Revealer {
reveal-child: bind $should_border_be_shown(template.config, template.bell-ringing) as <bool>;
transition-type: crossfade;
transition-duration: 500;
StackPage {
name: "error";
Box bell_overlay {
styles [
"bell-overlay",
]
child: Adw.StatusPage {
icon-name: "computer-fail-symbolic";
title: _("Oh, no.");
description: _("Unable to acquire an OpenGL context for rendering.");
halign: fill;
valign: fill;
}
child: LinkButton {
label: "https://ghostty.org/docs/help/gtk-opengl-context";
uri: "https://ghostty.org/docs/help/gtk-opengl-context";
};
};
}
[overlay]
$GhosttySurfaceChildExited child_exited_overlay {
visible: bind template.child-exited;
close-request => $child_exited_close();
}
[overlay]
$GhosttyResizeOverlay resize_overlay {}
[overlay]
Label url_left {
styles [
"background",
"url-overlay",
]
visible: false;
halign: start;
valign: end;
label: bind template.mouse-hover-url;
EventControllerMotion url_ec_motion {
enter => $url_mouse_enter();
leave => $url_mouse_leave();
}
}
[overlay]
Label url_right {
styles [
"background",
"url-overlay",
]
visible: false;
halign: end;
valign: end;
label: bind template.mouse-hover-url;
}
}
// Event controllers for interactivity
EventControllerFocus {
enter => $focus_enter();
leave => $focus_leave();
}
EventControllerKey {
key-pressed => $key_pressed();
key-released => $key_released();
}
EventControllerMotion {
motion => $mouse_motion();
leave => $mouse_leave();
}
EventControllerScroll {
scroll => $scroll();
scroll-begin => $scroll_begin();
scroll-end => $scroll_end();
flags: both_axes;
}
GestureClick {
pressed => $mouse_down();
released => $mouse_up();
button: 0;
}
DropTarget drop_target {
drop => $drop();
actions: copy;
// The order matters here: we can only set this after the stack
// pages above have been created.
visible-child-name: bind $stack_child_name(template.error) as <string>;
}
}
@@ -228,7 +254,8 @@ menu context_menu_model {
item {
label: _("Close Tab");
action: "win.close-tab";
action: "tab.close";
target: "this";
}
}

View File

@@ -10,6 +10,7 @@ template $GhosttyWindow: Adw.ApplicationWindow {
realize => $realize();
notify::config => $notify_config();
notify::fullscreened => $notify_fullscreened();
notify::is-active => $notify_is_active();
notify::maximized => $notify_maximized();
notify::quick-terminal => $notify_quick_terminal();
notify::scale-factor => $notify_scale_factor();
@@ -225,6 +226,7 @@ menu main_menu {
item {
label: _("Close Tab");
action: "win.close-tab";
target: "this";
}
}

View File

@@ -491,7 +491,7 @@ pub fn performAction(
.toggle_maximize => self.toggleMaximize(target),
.toggle_fullscreen => self.toggleFullscreen(target, value),
.new_tab => try self.newTab(target),
.close_tab => return try self.closeTab(target),
.close_tab => return try self.closeTab(target, value),
.goto_tab => return self.gotoTab(target, value),
.move_tab => self.moveTab(target, value),
.new_split => try self.newSplit(target, value),
@@ -585,7 +585,7 @@ fn newTab(_: *App, target: apprt.Target) !void {
}
}
fn closeTab(_: *App, target: apprt.Target) !bool {
fn closeTab(_: *App, target: apprt.Target, value: apprt.Action.Value(.close_tab)) !bool {
switch (target) {
.app => return false,
.surface => |v| {
@@ -597,8 +597,16 @@ fn closeTab(_: *App, target: apprt.Target) !bool {
return false;
};
tab.closeWithConfirmation();
return true;
switch (value) {
.this => {
tab.closeWithConfirmation();
return true;
},
.other => {
log.warn("close-tab:other is not implemented", .{});
return false;
},
}
},
}
}
@@ -1145,7 +1153,7 @@ fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("win.close", .{ .close_window = {} });
try self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
try self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
try self.syncActionAccelerator("win.close-tab", .{ .close_tab = .this });
try self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
try self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
try self.syncActionAccelerator("win.split-left", .{ .new_split = .left });

View File

@@ -772,7 +772,9 @@ pub fn focusCurrentTab(self: *Window) void {
}
pub fn onConfigReloaded(self: *Window) void {
self.sendToast(i18n._("Reloaded the configuration"));
if (self.app.config.@"app-notifications".@"config-reload") {
self.sendToast(i18n._("Reloaded the configuration"));
}
}
pub fn sendToast(self: *Window, title: [*:0]const u8) void {
@@ -1074,7 +1076,7 @@ fn gtkActionCloseTab(
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .close_tab = {} });
self.performBindingAction(.{ .close_tab = .this });
}
fn gtkActionSplitRight(

View File

@@ -24,6 +24,8 @@ pub const CursorPos = struct {
pub const IMEPos = struct {
x: f64,
y: f64,
width: f64,
height: f64,
};
/// The clipboard type.

View File

@@ -131,6 +131,13 @@ pub const VTable = struct {
};
test Benchmark {
// This test fails on FreeBSD so skip:
//
// /home/runner/work/ghostty/ghostty/src/benchmark/Benchmark.zig:165:5: 0x3cd2de1 in decltest.Benchmark (ghostty-test)
// try testing.expect(result.duration > 0);
// ^
if (builtin.os.tag == .freebsd) return error.SkipZigTest;
const testing = std.testing;
const Simple = struct {
const Self = @This();

View File

@@ -1,13 +1,20 @@
const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const Action = @import("ghostty.zig").Action;
const args = @import("args.zig");
const x11_color = @import("../terminal/main.zig").x11_color;
const vaxis = @import("vaxis");
const tui = @import("tui.zig");
pub const Options = struct {
pub fn deinit(self: Options) void {
_ = self;
}
/// If `true`, print without formatting even if printing to a tty
plain: bool = false,
/// Enables "-h" and "--help" to work.
pub fn help(self: Options) !void {
_ = self;
@@ -17,7 +24,12 @@ pub const Options = struct {
/// The `list-colors` command is used to list all the named RGB colors in
/// Ghostty.
pub fn run(alloc: std.mem.Allocator) !u8 {
///
/// Flags:
///
/// * `--plain`: will disable formatting and make the output more
/// friendly for Unix tooling. This is default when not printing to a tty.
pub fn run(alloc: Allocator) !u8 {
var opts: Options = .{};
defer opts.deinit();
@@ -27,7 +39,7 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
try args.parse(Options, alloc, &opts, &iter);
}
const stdout = std.io.getStdOut().writer();
const stdout = std.io.getStdOut();
var keys = std.ArrayList([]const u8).init(alloc);
defer keys.deinit();
@@ -39,15 +51,163 @@ pub fn run(alloc: std.mem.Allocator) !u8 {
}
}.lessThan);
for (keys.items) |name| {
const rgb = x11_color.map.get(name).?;
try stdout.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{
name,
rgb.r,
rgb.g,
rgb.b,
});
// Despite being under the posix namespace, this also works on Windows as of zig 0.13.0
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(stdout.handle)) {
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
return prettyPrint(arena.allocator(), keys.items);
} else {
const writer = stdout.writer();
for (keys.items) |name| {
const rgb = x11_color.map.get(name).?;
try writer.print("{s} = #{x:0>2}{x:0>2}{x:0>2}\n", .{
name,
rgb.r,
rgb.g,
rgb.b,
});
}
}
return 0;
}
fn prettyPrint(alloc: Allocator, keys: [][]const u8) !u8 {
// Set up vaxis
var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc, tty.anyWriter());
// We know we are ghostty, so let's enable mode 2027. Vaxis normally does this but you need an
// event loop to auto-enable it.
vx.caps.unicode = .unicode;
try tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_set);
defer tty.anyWriter().writeAll(vaxis.ctlseqs.unicode_reset) catch {};
var buf_writer = tty.bufferedWriter();
const writer = buf_writer.writer().any();
const winsize: vaxis.Winsize = switch (builtin.os.tag) {
// We use some default, it doesn't really matter for what
// we're doing because we don't do any wrapping.
.windows => .{
.rows = 24,
.cols = 120,
.x_pixel = 1024,
.y_pixel = 768,
},
else => try vaxis.Tty.getWinsize(tty.fd),
};
try vx.resize(alloc, tty.anyWriter(), winsize);
const win = vx.window();
var max_name_len: usize = 0;
for (keys) |name| {
if (name.len > max_name_len) max_name_len = name.len;
}
// max name length plus " = #RRGGBB XX" plus " " gutter between columns
const column_size = max_name_len + 15;
// add two to take into account lack of gutter after last column
const columns: usize = @divFloor(win.width + 2, column_size);
var i: usize = 0;
const step = @divFloor(keys.len, columns) + 1;
while (i < step) : (i += 1) {
win.clear();
var result: vaxis.Window.PrintResult = .{ .col = 0, .row = 0, .overflow = false };
for (0..columns) |j| {
const k = i + (step * j);
if (k >= keys.len) continue;
const name = keys[k];
const rgb = x11_color.map.get(name).?;
const style1: vaxis.Style = .{
.fg = .{
.rgb = .{ rgb.r, rgb.g, rgb.b },
},
};
const style2: vaxis.Style = .{
.fg = .{
.rgb = .{ rgb.r, rgb.g, rgb.b },
},
.bg = .{
.rgb = .{ rgb.r, rgb.g, rgb.b },
},
};
// name of the color
result = win.printSegment(
.{ .text = name },
.{ .col_offset = result.col },
);
// push the color data to the end of the column
for (0..max_name_len - name.len) |_| {
result = win.printSegment(
.{ .text = " " },
.{ .col_offset = result.col },
);
}
result = win.printSegment(
.{ .text = " = " },
.{ .col_offset = result.col },
);
// rgb triple
result = win.printSegment(.{
.text = try std.fmt.allocPrint(
alloc,
"#{x:0>2}{x:0>2}{x:0>2}",
.{
rgb.r, rgb.g, rgb.b,
},
),
.style = style1,
}, .{ .col_offset = result.col });
result = win.printSegment(
.{ .text = " " },
.{ .col_offset = result.col },
);
// colored block
result = win.printSegment(
.{
.text = " ",
.style = style2,
},
.{ .col_offset = result.col },
);
// add the gutter if needed
if (j + 1 < columns) {
result = win.printSegment(
.{
.text = " ",
},
.{ .col_offset = result.col },
);
}
}
// clear the rest of the line
while (result.col != 0) {
result = win.printSegment(
.{
.text = " ",
},
.{ .col_offset = result.col },
);
}
// output the data
try vx.prettyPrint(writer);
}
// be sure to flush!
try buf_writer.flush();
return 0;
}

View File

@@ -17,6 +17,8 @@ const zf = @import("zf");
// scroll position for larger lists.
const SMALL_LIST_THRESHOLD = 10;
const ColorScheme = enum { all, dark, light };
pub const Options = struct {
/// If true, print the full path to the theme.
path: bool = false,
@@ -25,7 +27,7 @@ pub const Options = struct {
plain: bool = false,
/// Specifies the color scheme of the themes to include in the list.
color: enum { all, dark, light } = .all,
color: ColorScheme = .all,
pub fn deinit(self: Options) void {
_ = self;
@@ -146,28 +148,11 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
count += 1;
const path = try std.fs.path.join(alloc, &.{ loc.dir, entry.name });
// if there is no need to filter just append the theme to the list
if (opts.color == .all) {
try themes.append(.{
.path = path,
.location = loc.location,
.theme = try alloc.dupe(u8, entry.name),
});
continue;
}
// otherwise check if the theme should be included based on the provided options
var config = try Config.default(alloc);
defer config.deinit();
try config.loadFile(config._arena.?.allocator(), path);
if (shouldIncludeTheme(opts, config)) {
try themes.append(.{
.path = path,
.location = loc.location,
.theme = try alloc.dupe(u8, entry.name),
});
}
try themes.append(.{
.path = path,
.location = loc.location,
.theme = try alloc.dupe(u8, entry.name),
});
},
else => {},
}
@@ -182,7 +167,7 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
std.mem.sortUnstable(ThemeListElement, themes.items, {}, ThemeListElement.lessThan);
if (tui.can_pretty_print and !opts.plain and std.posix.isatty(std.io.getStdOut().handle)) {
try preview(gpa_alloc, themes.items);
try preview(gpa_alloc, themes.items, opts.color);
return 0;
}
@@ -222,8 +207,9 @@ const Preview = struct {
},
color_scheme: vaxis.Color.Scheme,
text_input: vaxis.widgets.TextInput,
theme_filter: ColorScheme,
pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement) !*Preview {
pub fn init(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !*Preview {
const self = try allocator.create(Preview);
self.* = .{
@@ -240,11 +226,10 @@ const Preview = struct {
.mode = .normal,
.color_scheme = .light,
.text_input = vaxis.widgets.TextInput.init(allocator, &self.vx.unicode),
.theme_filter = theme_filter,
};
for (0..themes.len) |i| {
try self.filtered.append(i);
}
try self.updateFiltered();
return self;
}
@@ -308,6 +293,8 @@ const Preview = struct {
self.filtered.clearRetainingCapacity();
var theme_config = try Config.default(self.allocator);
defer theme_config.deinit();
if (self.text_input.buf.realLength() > 0) {
const first_half = self.text_input.buf.firstHalf();
const second_half = self.text_input.buf.secondHalf();
@@ -328,6 +315,9 @@ const Preview = struct {
while (it.next()) |token| try tokens.append(token);
for (self.themes, 0..) |*theme, i| {
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
if (!shouldIncludeTheme(self.theme_filter, theme_config)) continue;
theme.rank = zf.rank(theme.theme, tokens.items, .{
.to_lower = true,
.plain = true,
@@ -336,8 +326,11 @@ const Preview = struct {
}
} else {
for (self.themes, 0..) |*theme, i| {
try self.filtered.append(i);
theme.rank = null;
try theme_config.loadFile(theme_config._arena.?.allocator(), theme.path);
if (shouldIncludeTheme(self.theme_filter, theme_config)) {
try self.filtered.append(i);
theme.rank = null;
}
}
}
@@ -438,6 +431,14 @@ const Preview = struct {
self.themes[self.filtered.items[self.current]].path,
alloc,
);
if (key.matches('f', .{})) {
switch (self.theme_filter) {
.all => self.theme_filter = .dark,
.dark => self.theme_filter = .light,
.light => self.theme_filter = .all,
}
try self.updateFiltered();
}
},
.help => {
if (key.matches('q', .{}))
@@ -695,6 +696,7 @@ const Preview = struct {
const key_help = [_]struct { keys: []const u8, help: []const u8 }{
.{ .keys = "^C, q, ESC", .help = "Quit." },
.{ .keys = "F1, ?, ^H", .help = "Toggle help window." },
.{ .keys = "f", .help = "Cycle through theme filters." },
.{ .keys = "k, ↑", .help = "Move up 1 theme." },
.{ .keys = "ScrollUp", .help = "Move up 1 theme." },
.{ .keys = "PgUp", .help = "Move up 20 themes." },
@@ -1615,18 +1617,17 @@ fn color(config: Config, palette: usize) vaxis.Color {
const lorem_ipsum = @embedFile("lorem_ipsum.txt");
fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement) !void {
var app = try Preview.init(allocator, themes);
fn preview(allocator: std.mem.Allocator, themes: []ThemeListElement, theme_filter: ColorScheme) !void {
var app = try Preview.init(allocator, themes, theme_filter);
defer app.deinit();
try app.run();
}
fn shouldIncludeTheme(opts: Options, theme_config: Config) bool {
fn shouldIncludeTheme(theme_filter: ColorScheme, theme_config: Config) bool {
const rf = @as(f32, @floatFromInt(theme_config.background.r)) / 255.0;
const gf = @as(f32, @floatFromInt(theme_config.background.g)) / 255.0;
const bf = @as(f32, @floatFromInt(theme_config.background.b)) / 255.0;
const luminance = 0.2126 * rf + 0.7152 * gf + 0.0722 * bf;
const is_dark = luminance < 0.5;
return (opts.color == .dark and is_dark) or (opts.color == .light and !is_dark);
return (theme_filter == .all) or (theme_filter == .dark and is_dark) or (theme_filter == .light and !is_dark);
}

View File

@@ -654,6 +654,18 @@ foreground: Color = .{ .r = 0xFF, .g = 0xFF, .b = 0xFF },
/// Available since: 1.2.0
@"selection-clear-on-typing": bool = true,
/// Whether to clear selected text after copying. This defaults to `false`.
///
/// When set to `true`, the selection will be automatically cleared after
/// any copy operation that invokes the `copy_to_clipboard` keyboard binding.
/// Importantly, this will not clear the selection if the copy operation
/// was invoked via `copy-on-select`.
///
/// When set to `false`, the selection remains visible after copying, allowing
/// to see what was copied and potentially perform additional operations
/// on the same selection.
@"selection-clear-on-copy": bool = false,
/// The minimum contrast ratio between the foreground and background colors.
/// The contrast ratio is a value between 1 and 21. A value of 1 allows for no
/// contrast (e.g. black on black). This value is the contrast ratio as defined
@@ -767,6 +779,22 @@ palette: Palette = .{},
/// the mouse is shown again when a new window, tab, or split is created.
@"mouse-hide-while-typing": bool = false,
/// When to scroll the surface to the bottom. The format of this is a list of
/// options to enable separated by commas. If you prefix an option with `no-`
/// then it is disabled. If you omit an option, its default value is used.
///
/// Available options:
///
/// - `keystroke` If set, scroll the surface to the bottom when the user
/// presses a key that results in data being sent to the PTY (basically
/// anything but modifiers or keybinds that are processed by Ghostty).
///
/// - `output` If set, scroll the surface to the bottom if there is new data
/// to display. (Currently unimplemented.)
///
/// The default is `keystroke, no-output`.
@"scroll-to-bottom": ScrollToBottom = .default,
/// Determines whether running programs can detect the shift key pressed with a
/// mouse click. Typically, the shift key is used to extend mouse selection.
///
@@ -2499,6 +2527,8 @@ keybind: Keybinds = .{},
///
/// - `clipboard-copy` (default: true) - Show a notification when text is copied
/// to the clipboard.
/// - `config-reload` (default: true) - Show a notification when
/// the configuration is reloaded.
///
/// To specify a notification to enable, specify the name of the notification.
/// To specify a notification to disable, prefix the name with `no-`. For
@@ -3017,6 +3047,13 @@ else
/// Available since Ghostty 1.2.0.
@"bold-color": ?BoldColor = null,
/// The opacity level (opposite of transparency) of the faint text. A value of
/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0
/// or greater than 1 will be clamped to the nearest valid value.
///
/// Available since Ghostty 1.2.0.
@"faint-opacity": f64 = 0.5,
/// This will be used to set the `TERM` environment variable.
/// HACK: We set this with an `xterm` prefix because vim uses that to enable key
/// protocols (specifically this will enable `modifyOtherKeys`), among other
@@ -3999,6 +4036,8 @@ pub fn finalize(self: *Config) !void {
if (self.@"auto-update-channel" == null) {
self.@"auto-update-channel" = build_config.release_channel;
}
self.@"faint-opacity" = std.math.clamp(self.@"faint-opacity", 0.0, 1.0);
}
/// Callback for src/cli/args.zig to allow us to handle special cases
@@ -5596,7 +5635,7 @@ pub const Keybinds = struct {
try self.set.put(
alloc,
.{ .key = .{ .unicode = 'w' }, .mods = .{ .ctrl = true, .shift = true } },
.{ .close_tab = {} },
.{ .close_tab = .this },
);
try self.set.putFlags(
alloc,
@@ -5758,15 +5797,24 @@ pub const Keybinds = struct {
else
.{ .alt = true };
// Cmd+N for goto tab N
// Cmd/Alt+N for goto tab N
const start: u21 = '1';
const end: u21 = '8';
var i: u21 = start;
while (i <= end) : (i += 1) {
comptime var i: u21 = start;
inline while (i <= end) : (i += 1) {
// We register BOTH the physical `digit_N` key and the unicode
// `N` key. This allows most keyboard layouts to work with
// this shortcut. Namely, AZERTY doesn't produce unicode `N`
// for their digit keys (they're on shifted keys on the same
// physical keys).
try self.set.putFlags(
alloc,
.{
.key = .{ .unicode = i },
.key = .{ .physical = @field(
inputpkg.Key,
std.fmt.comptimePrint("digit_{u}", .{i}),
) },
.mods = mods,
},
.{ .goto_tab = (i - start) + 1 },
@@ -5779,6 +5827,22 @@ pub const Keybinds = struct {
.performable = !builtin.target.os.tag.isDarwin(),
},
);
// Important: this must be the LAST binding set so that the
// libghostty trigger API returns this one for the action,
// so that things like the macOS tab bar key equivalent label
// work properly.
try self.set.putFlags(
alloc,
.{
.key = .{ .unicode = i },
.mods = mods,
},
.{ .goto_tab = (i - start) + 1 },
.{
.performable = !builtin.target.os.tag.isDarwin(),
},
);
}
try self.set.putFlags(
alloc,
@@ -5902,7 +5966,7 @@ pub const Keybinds = struct {
try self.set.put(
alloc,
.{ .key = .{ .unicode = 'w' }, .mods = .{ .super = true, .alt = true } },
.{ .close_tab = {} },
.{ .close_tab = .this },
);
try self.set.put(
alloc,
@@ -7058,6 +7122,7 @@ pub const GtkTitlebarStyle = enum(c_int) {
/// See app-notifications
pub const AppNotifications = packed struct {
@"clipboard-copy": bool = true,
@"config-reload": bool = true,
};
/// See bell-features
@@ -7195,6 +7260,53 @@ pub const QuickTerminalSize = struct {
height: u32,
};
/// C API structure for QuickTerminalSize
pub const C = extern struct {
primary: C.Size,
secondary: C.Size,
pub const Size = extern struct {
tag: Tag,
value: Value,
pub const Tag = enum(u8) { none, percentage, pixels };
pub const Value = extern union {
percentage: f32,
pixels: u32,
};
pub const none: C.Size = .{ .tag = .none, .value = undefined };
pub fn percentage(v: f32) C.Size {
return .{
.tag = .percentage,
.value = .{ .percentage = v },
};
}
pub fn pixels(v: u32) C.Size {
return .{
.tag = .pixels,
.value = .{ .pixels = v },
};
}
};
};
pub fn cval(self: QuickTerminalSize) C {
return .{
.primary = if (self.primary) |p| switch (p) {
.percentage => |v| .percentage(v),
.pixels => |v| .pixels(v),
} else .none,
.secondary = if (self.secondary) |s| switch (s) {
.percentage => |v| .percentage(v),
.pixels => |v| .pixels(v),
} else .none,
};
}
pub fn calculate(
self: QuickTerminalSize,
position: QuickTerminalPosition,
@@ -7268,6 +7380,7 @@ pub const QuickTerminalSize = struct {
try formatter.formatEntry([]const u8, fbs.getWritten());
}
test "parse QuickTerminalSize" {
const testing = std.testing;
var v: QuickTerminalSize = undefined;
@@ -7980,6 +8093,14 @@ pub const WindowPadding = struct {
}
};
/// See scroll-to-bottom
pub const ScrollToBottom = packed struct {
keystroke: bool = true,
output: bool = false,
pub const default: ScrollToBottom = .{};
};
test "parse duration" {
inline for (Duration.units) |unit| {
var buf: [16]u8 = undefined;

View File

@@ -806,14 +806,41 @@ pub const Face = struct {
const ic_width: ?f64 = ic_width: {
const glyph = self.glyphIndex('水') orelse break :ic_width null;
var advances: [1]macos.graphics.Size = undefined;
_ = ct_font.getAdvancesForGlyphs(
const advance = ct_font.getAdvancesForGlyphs(
.horizontal,
&.{@intCast(glyph)},
&advances,
null,
);
break :ic_width advances[0].width;
const bounds = ct_font.getBoundingRectsForGlyphs(
.horizontal,
&.{@intCast(glyph)},
null,
);
// If the advance of the glyph is less than the width of the actual
// glyph then we just treat it as invalid since it's probably wrong
// and using it for size normalization will instead make the font
// way too big.
//
// This can sometimes happen if there's a CJK font that has been
// patched with the nerd fonts patcher and it butchers the advance
// values so the advance ends up half the width of the actual glyph.
if (bounds.size.width > advance) {
var buf: [1024]u8 = undefined;
const font_name = self.name(&buf) catch "<Error getting font name>";
log.warn(
"(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.",
.{
font_name,
bounds.size.width,
advance,
},
);
break :ic_width null;
}
break :ic_width advance;
};
return .{

View File

@@ -1007,7 +1007,31 @@ pub const Face = struct {
.no_svg = true,
}) catch break :ic_width null;
break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x);
const ft_glyph = face.handle.*.glyph;
// If the advance of the glyph is less than the width of the actual
// glyph then we just treat it as invalid since it's probably wrong
// and using it for size normalization will instead make the font
// way too big.
//
// This can sometimes happen if there's a CJK font that has been
// patched with the nerd fonts patcher and it butchers the advance
// values so the advance ends up half the width of the actual glyph.
if (ft_glyph.*.metrics.width > ft_glyph.*.advance.x) {
var buf: [1024]u8 = undefined;
const font_name = self.name(&buf) catch "<Error getting font name>";
log.warn(
"(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.",
.{
font_name,
f26dot6ToF64(ft_glyph.*.metrics.width),
f26dot6ToF64(ft_glyph.*.advance.x),
},
);
break :ic_width null;
}
break :ic_width f26dot6ToF64(ft_glyph.*.advance.x);
};
return .{

View File

@@ -552,11 +552,15 @@ pub const Action = union(enum) {
/// of the `confirm-close-surface` configuration setting.
close_surface,
/// Close the current tab and all splits therein.
/// Close the current tab and all splits therein _or_ close all tabs and
/// splits thein of tabs _other_ than the current tab, depending on the
/// mode.
///
/// If the mode is not specified, defaults to closing the current tab.
///
/// This might trigger a close confirmation popup, depending on the value
/// of the `confirm-close-surface` configuration setting.
close_tab,
close_tab: CloseTabMode,
/// Close the current window and all tabs and splits therein.
///
@@ -858,6 +862,13 @@ pub const Action = union(enum) {
hide,
};
pub const CloseTabMode = enum {
this,
other,
pub const default: CloseTabMode = .this;
};
fn parseEnum(comptime T: type, value: []const u8) !T {
return std.meta.stringToEnum(T, value) orelse return Error.InvalidFormat;
}

View File

@@ -393,11 +393,18 @@ fn actionCommands(action: Action.Key) []const Command {
.description = "Close the current terminal.",
}},
.close_tab => comptime &.{.{
.action = .close_tab,
.title = "Close Tab",
.description = "Close the current tab.",
}},
.close_tab => comptime &.{
.{
.action = .{ .close_tab = .this },
.title = "Close Tab",
.description = "Close the current tab.",
},
.{
.action = .{ .close_tab = .other },
.title = "Close Other Tabs",
.description = "Close all tabs in this window except the current one.",
},
},
.close_window => comptime &.{.{
.action = .close_window,

View File

@@ -589,6 +589,84 @@ pub const Key = enum(c_int) {
};
}
/// Whether this key should be remappable by the operating system.
///
/// On certain OSes (namely Linux and the BSDs) certain keys like the
/// functional keys are expected to be remappable by the user, such as
/// in the very common use case of swapping the Caps Lock key with the
/// Escape key with the XKB option `caps:swapescape`.
///
/// However, the way XKB implements this is by essentially acting as a
/// software key remapper that destroys all information about the original
/// physical key, leading to very annoying bugs like #7309 where the
/// physical key `XKB_KEY_c` gets remapped into `XKB_KEY_Cyrillic_tse`,
/// which causes all of our physical key handling to completely break down.
/// _Very naughty._
///
/// As a compromise, given that writing system keys (§3.1.1) comprise the
/// majority of keys that "change meaning [...] based on the current locale
/// and keyboard layout", we allow all other keys to be remapped by default
/// since they should be fairly harmless. We might consider making this
/// configurable, but for now this should at least placate most people.
pub fn shouldBeRemappable(self: Key) bool {
return switch (self) {
// "Writing System Keys" § 3.1.1
.backquote,
.backslash,
.bracket_left,
.bracket_right,
.comma,
.digit_0,
.digit_1,
.digit_2,
.digit_3,
.digit_4,
.digit_5,
.digit_6,
.digit_7,
.digit_8,
.digit_9,
.equal,
.intl_backslash,
.intl_ro,
.intl_yen,
.key_a,
.key_b,
.key_c,
.key_d,
.key_e,
.key_f,
.key_g,
.key_h,
.key_i,
.key_j,
.key_k,
.key_l,
.key_m,
.key_n,
.key_o,
.key_p,
.key_q,
.key_r,
.key_s,
.key_t,
.key_u,
.key_v,
.key_w,
.key_x,
.key_y,
.key_z,
.minus,
.period,
.quote,
.semicolon,
.slash,
=> false,
else => true,
};
}
/// Returns true if this is a keypad key.
pub fn keypad(self: Key) bool {
return switch (self) {

View File

@@ -49,6 +49,7 @@ pub const locales = [_][:0]const u8{
"ca_ES.UTF-8",
"bg_BG.UTF-8",
"ga_IE.UTF-8",
"hu_HU.UTF-8",
"he_IL.UTF-8",
};
@@ -136,7 +137,12 @@ pub fn canonicalizeLocale(
buf: []u8,
locale: []const u8,
) error{NoSpaceLeft}![:0]const u8 {
if (comptime !build_config.i18n) return locale;
if (comptime !build_config.i18n) {
if (buf.len < locale.len + 1) return error.NoSpaceLeft;
@memcpy(buf[0..locale.len], locale);
buf[locale.len] = 0;
return buf[0..locale.len :0];
}
// Fix zh locales for macOS
if (fixZhLocale(locale)) |fixed| {

View File

@@ -522,6 +522,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
selection_background: ?configpkg.Config.TerminalColor,
selection_foreground: ?configpkg.Config.TerminalColor,
bold_color: ?configpkg.BoldColor,
faint_opacity: u8,
min_contrast: f32,
padding_color: configpkg.WindowPaddingColor,
custom_shaders: configpkg.RepeatablePath,
@@ -584,6 +585,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
.background = config.background.toTerminalRGB(),
.foreground = config.foreground.toTerminalRGB(),
.bold_color = config.@"bold-color",
.faint_opacity = @intFromFloat(@ceil(config.@"faint-opacity" * 255)),
.min_contrast = @floatCast(config.@"minimum-contrast"),
.padding_color = config.@"window-padding-color",
@@ -1549,15 +1551,17 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
// Look up the image
const image = self.images.get(p.image_id) orelse {
log.warn("image not found for placement image_id={}", .{p.image_id});
return;
continue;
};
// Get the texture
const texture = switch (image.image) {
.ready => |t| t,
.ready,
.unload_ready,
=> |t| t,
else => {
log.warn("image not ready for placement image_id={}", .{p.image_id});
return;
continue;
},
};
@@ -1907,7 +1911,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
if (img.isUnloading()) {
img.deinit(self.alloc);
self.images.removeByPtr(kv.key_ptr);
return;
continue;
}
if (img.isPending()) try img.upload(self.alloc, &self.api);
}
@@ -2225,23 +2229,44 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
const cursor_width: f32 = @floatFromInt(cursor.glyph_size[0]);
const cursor_height: f32 = @floatFromInt(cursor.glyph_size[1]);
// Left edge of the cell the cursor is in.
var pixel_x: f32 = @floatFromInt(
cursor.grid_pos[0] * cell.width + padding.left,
);
// Top edge, relative to the top of the
// screen, of the cell the cursor is in.
var pixel_y: f32 = @floatFromInt(
cursor.grid_pos[1] * cell.height + padding.top,
);
pixel_x += @floatFromInt(cursor.bearings[0]);
pixel_y += @floatFromInt(cursor.bearings[1]);
// If +Y is up in our shaders, we need to flip the coordinate.
// If +Y is up in our shaders, we need to flip the coordinate
// so that it's instead the top edge of the cell relative to
// the *bottom* of the screen.
if (!GraphicsAPI.custom_shader_y_is_down) {
pixel_y = @as(f32, @floatFromInt(screen.height)) - pixel_y;
// We need to add the cursor height because we need the +Y
// edge for the Y coordinate, and flipping means that it's
// the -Y edge now.
pixel_y += cursor_height;
}
// Add the X bearing to get the -X (left) edge of the cursor.
pixel_x += @floatFromInt(cursor.bearings[0]);
// How we deal with the Y bearing depends on which direction
// is "up", since we want our final `pixel_y` value to be the
// +Y edge of the cursor.
if (GraphicsAPI.custom_shader_y_is_down) {
// As a reminder, the Y bearing is the distance from the
// bottom of the cell to the top of the glyph, so to get
// the +Y edge we need to add the cell height, subtract
// the Y bearing, and add the glyph height to get the +Y
// (bottom) edge of the cursor.
pixel_y += @floatFromInt(cell.height);
pixel_y -= @floatFromInt(cursor.bearings[1]);
pixel_y += @floatFromInt(cursor.glyph_size[1]);
} else {
// If the Y direction is reversed though, we instead want
// the *top* edge of the cursor, which means we just need
// to subtract the cell height and add the Y bearing.
pixel_y -= @floatFromInt(cell.height);
pixel_y += @floatFromInt(cursor.bearings[1]);
}
const new_cursor: [4]f32 = .{
@@ -2612,7 +2637,7 @@ pub fn Renderer(comptime GraphicsAPI: type) type {
};
// Foreground alpha for this cell.
const alpha: u8 = if (style.flags.faint) 175 else 255;
const alpha: u8 = if (style.flags.faint) self.config.faint_opacity else 255;
// Set the cell's background color.
{

View File

@@ -193,7 +193,7 @@ pub const Action = union(enum) {
/// Maximum number of intermediate characters during parsing. This is
/// 4 because we also use the intermediates array for UTF8 decoding which
/// can be at most 4 bytes.
const MAX_INTERMEDIATE = 4;
pub const MAX_INTERMEDIATE = 4;
/// Maximum number of CSI parameters. This is arbitrary. Practically, the
/// only CSI command that uses more than 3 parameters is the SGR command
@@ -206,7 +206,7 @@ const MAX_INTERMEDIATE = 4;
/// number. I implore TUI authors to not use more than this number of CSI
/// params, but I suspect we'll introduce a slow path with heap allocation
/// one day.
const MAX_PARAMS = 24;
pub const MAX_PARAMS = 24;
/// Current state of the state machine
state: State,
@@ -949,6 +949,55 @@ test "csi: too many params" {
}
}
test "csi: sgr with up to our max parameters" {
for (1..MAX_PARAMS + 1) |max| {
var p = init();
_ = p.next(0x1B);
_ = p.next('[');
for (0..max - 1) |_| {
_ = p.next('1');
_ = p.next(';');
}
_ = p.next('2');
{
const a = p.next('H');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1].? == .csi_dispatch);
try testing.expect(a[2] == null);
const csi = a[1].?.csi_dispatch;
try testing.expectEqual(@as(usize, max), csi.params.len);
try testing.expectEqual(@as(u16, 2), csi.params[max - 1]);
}
}
}
test "csi: sgr beyond our max drops it" {
// Has to be +2 for the loops below
const max = MAX_PARAMS + 2;
var p = init();
_ = p.next(0x1B);
_ = p.next('[');
for (0..max - 1) |_| {
_ = p.next('1');
_ = p.next(';');
}
_ = p.next('2');
{
const a = p.next('H');
try testing.expect(p.state == .ground);
try testing.expect(a[0] == null);
try testing.expect(a[1] == null);
try testing.expect(a[2] == null);
}
}
test "dcs: XTGETTCAP" {
var p = init();
_ = p.next(0x1B);

View File

@@ -147,25 +147,28 @@ pub const Command = union(enum) {
/// End a hyperlink (OSC 8)
hyperlink_end: void,
/// Sleep (OSC 9;1)
sleep: struct {
/// ConEmu sleep (OSC 9;1)
conemu_sleep: struct {
duration_ms: u16,
},
/// Show GUI message Box (OSC 9;2)
show_message_box: []const u8,
/// ConEmu show GUI message box (OSC 9;2)
conemu_show_message_box: []const u8,
/// Change ConEmu tab (OSC 9;3)
change_conemu_tab_title: union(enum) {
reset: void,
/// ConEmu change tab title (OSC 9;3)
conemu_change_tab_title: union(enum) {
reset,
value: []const u8,
},
/// Set progress state (OSC 9;4)
progress_report: ProgressReport,
/// ConEmu progress report (OSC 9;4)
conemu_progress_report: ProgressReport,
/// Wait input (OSC 9;5)
wait_input: void,
/// ConEmu wait input (OSC 9;5)
conemu_wait_input,
/// ConEmu GUI macro (OSC 9;6)
conemu_guimacro: []const u8,
pub const ColorOperation = union(enum) {
pub const Source = enum(u16) {
@@ -208,7 +211,6 @@ pub const Command = union(enum) {
};
pub const ProgressReport = struct {
// sync with ghostty_terminal_osc_command_progressreport_state_e in include/ghostty.h
pub const State = enum(c_int) {
remove,
set,
@@ -220,7 +222,7 @@ pub const Command = union(enum) {
state: State,
progress: ?u8 = null,
// sync with ghostty_terminal_osc_command_progressreport_s in include/ghostty.h
// sync with ghostty_action_progress_report_s
pub const C = extern struct {
state: c_int,
progress: i8,
@@ -229,7 +231,11 @@ pub const Command = union(enum) {
pub fn cval(self: ProgressReport) C {
return .{
.state = @intFromEnum(self.state),
.progress = if (self.progress) |progress| @intCast(std.math.clamp(progress, 0, 100)) else -1,
.progress = if (self.progress) |progress| @intCast(std.math.clamp(
progress,
0,
100,
)) else -1,
};
}
};
@@ -431,6 +437,7 @@ pub const Parser = struct {
conemu_progress_state,
conemu_progress_prevalue,
conemu_progress_value,
conemu_guimacro,
};
pub fn init() Parser {
@@ -957,107 +964,147 @@ pub const Parser = struct {
.osc_9 => switch (c) {
'1' => {
self.state = .conemu_sleep;
// This will end up being either a ConEmu sleep OSC 9;1,
// or a desktop notification OSC 9 that begins with '1', so
// mark as complete.
self.complete = true;
},
'2' => {
self.state = .conemu_message_box;
// This will end up being either a ConEmu message box OSC 9;2,
// or a desktop notification OSC 9 that begins with '2', so
// mark as complete.
self.complete = true;
},
'3' => {
self.state = .conemu_tab;
// This will end up being either a ConEmu message box OSC 9;3,
// or a desktop notification OSC 9 that begins with '3', so
// mark as complete.
self.complete = true;
},
'4' => {
self.state = .conemu_progress_prestate;
// This will end up being either a ConEmu progress report
// OSC 9;4, or a desktop notification OSC 9 that begins with
// '4', so mark as complete.
self.complete = true;
},
'5' => {
// Note that sending an OSC 9 desktop notification that
// starts with 5 is impossible due to this.
self.state = .swallow;
self.command = .{ .wait_input = {} };
self.command = .conemu_wait_input;
self.complete = true;
},
'6' => {
self.state = .conemu_guimacro;
// This will end up being either a ConEmu GUI macro OSC 9;6,
// or a desktop notification OSC 9 that begins with '6', so
// mark as complete.
self.complete = true;
},
// Todo: parse out other ConEmu operating system commands.
// Even if we don't support them we probably don't want
// them showing up as desktop notifications.
// Todo: parse out other ConEmu operating system commands. Even
// if we don't support them we probably don't want them showing
// up as desktop notifications.
else => self.showDesktopNotification(),
},
.conemu_sleep => switch (c) {
';' => {
self.command = .{ .sleep = .{ .duration_ms = 100 } };
self.command = .{ .conemu_sleep = .{ .duration_ms = 100 } };
self.buf_start = self.buf_idx;
self.complete = true;
self.state = .conemu_sleep_value;
},
else => self.state = .invalid,
},
.conemu_message_box => switch (c) {
';' => {
self.command = .{ .show_message_box = undefined };
self.temp_state = .{ .str = &self.command.show_message_box };
self.buf_start = self.buf_idx;
self.complete = true;
self.prepAllocableString();
},
else => self.state = .invalid,
// OSC 9;1 <something other than semicolon> is a desktop
// notification.
else => self.showDesktopNotification(),
},
.conemu_sleep_value => switch (c) {
else => self.complete = true,
},
.conemu_message_box => switch (c) {
';' => {
self.command = .{ .conemu_show_message_box = undefined };
self.temp_state = .{ .str = &self.command.conemu_show_message_box };
self.buf_start = self.buf_idx;
self.complete = true;
self.prepAllocableString();
},
// OSC 9;2 <something other than semicolon> is a desktop
// notification.
else => self.showDesktopNotification(),
},
.conemu_tab => switch (c) {
';' => {
self.state = .conemu_tab_txt;
self.command = .{ .change_conemu_tab_title = .{ .reset = {} } };
self.command = .{ .conemu_change_tab_title = .reset };
self.buf_start = self.buf_idx;
self.complete = true;
},
else => self.state = .invalid,
// OSC 9;3 <something other than semicolon> is a desktop
// notification.
else => self.showDesktopNotification(),
},
.conemu_tab_txt => {
self.command = .{ .change_conemu_tab_title = .{ .value = undefined } };
self.temp_state = .{ .str = &self.command.change_conemu_tab_title.value };
self.command = .{ .conemu_change_tab_title = .{ .value = undefined } };
self.temp_state = .{ .str = &self.command.conemu_change_tab_title.value };
self.complete = true;
self.prepAllocableString();
},
.conemu_progress_prestate => switch (c) {
';' => {
self.command = .{ .progress_report = .{
self.command = .{ .conemu_progress_report = .{
.state = undefined,
} };
self.state = .conemu_progress_state;
},
// OSC 9;4 <something other than semicolon> is a desktop
// notification.
else => self.showDesktopNotification(),
},
.conemu_progress_state => switch (c) {
'0' => {
self.command.progress_report.state = .remove;
self.command.conemu_progress_report.state = .remove;
self.state = .swallow;
self.complete = true;
},
'1' => {
self.command.progress_report.state = .set;
self.command.progress_report.progress = 0;
self.command.conemu_progress_report.state = .set;
self.command.conemu_progress_report.progress = 0;
self.state = .conemu_progress_prevalue;
},
'2' => {
self.command.progress_report.state = .@"error";
self.command.conemu_progress_report.state = .@"error";
self.complete = true;
self.state = .conemu_progress_prevalue;
},
'3' => {
self.command.progress_report.state = .indeterminate;
self.command.conemu_progress_report.state = .indeterminate;
self.complete = true;
self.state = .swallow;
},
'4' => {
self.command.progress_report.state = .pause;
self.command.conemu_progress_report.state = .pause;
self.complete = true;
self.state = .conemu_progress_prevalue;
},
// OSC 9;4; <something other than 0-4> is a desktop
// notification.
else => self.showDesktopNotification(),
},
@@ -1066,6 +1113,8 @@ pub const Parser = struct {
self.state = .conemu_progress_value;
},
// OSC 9;4;<0-4> <something other than semicolon> is a desktop
// notification.
else => self.showDesktopNotification(),
},
@@ -1077,8 +1126,16 @@ pub const Parser = struct {
// If we aren't a set substate, then we don't care
// about the value.
const p = &self.command.progress_report;
if (p.state != .set and p.state != .@"error" and p.state != .pause) break :value;
const p = &self.command.conemu_progress_report;
switch (p.state) {
.remove,
.indeterminate,
=> break :value,
.set,
.@"error",
.pause,
=> {},
}
if (p.state == .set)
assert(p.progress != null)
@@ -1104,6 +1161,20 @@ pub const Parser = struct {
},
},
.conemu_guimacro => switch (c) {
';' => {
self.command = .{ .conemu_guimacro = undefined };
self.temp_state = .{ .str = &self.command.conemu_guimacro };
self.buf_start = self.buf_idx;
self.state = .string;
self.complete = true;
},
// OSC 9;6 <something other than semicolon> is a desktop
// notification.
else => self.showDesktopNotification(),
},
.semantic_prompt => switch (c) {
'A' => {
self.state = .semantic_option_start;
@@ -1212,6 +1283,11 @@ pub const Parser = struct {
self.temp_state = .{ .str = &self.command.show_desktop_notification.body };
self.state = .string;
// Set as complete as we've already seen one character that should be
// part of the notification. If we wait for another character to set
// `complete` when the state is `.string` we won't be able to send any
// single character notifications.
self.complete = true;
}
fn prepAllocableString(self: *Parser) void {
@@ -1332,7 +1408,7 @@ pub const Parser = struct {
fn endConEmuSleepValue(self: *Parser) void {
switch (self.command) {
.sleep => |*v| v.duration_ms = value: {
.conemu_sleep => |*v| v.duration_ms = value: {
const str = self.buf[self.buf_start..self.buf_idx];
if (str.len == 0) break :value 100;
@@ -1595,6 +1671,26 @@ pub const Parser = struct {
.hyperlink_uri => self.endHyperlink(),
.string => self.endString(),
.conemu_sleep_value => self.endConEmuSleepValue(),
// We received OSC 9;X ST, but nothing else, finish off as a
// desktop notification with "X" as the body.
.conemu_sleep,
.conemu_message_box,
.conemu_tab,
.conemu_progress_prestate,
.conemu_progress_state,
.conemu_guimacro,
=> {
self.showDesktopNotification();
self.endString();
},
// A ConEmu progress report that has reached these states is
// complete, don't do anything to them.
.conemu_progress_prevalue,
.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),
@@ -2770,7 +2866,7 @@ test "OSC: OSC104: empty palette index" {
try std.testing.expect(it.next() == null);
}
test "OSC: conemu sleep" {
test "OSC: OSC 9;1 ConEmu sleep" {
const testing = std.testing;
var p: Parser = .init();
@@ -2780,11 +2876,11 @@ test "OSC: conemu sleep" {
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .sleep);
try testing.expectEqual(420, cmd.sleep.duration_ms);
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(420, cmd.conemu_sleep.duration_ms);
}
test "OSC: conemu sleep with no value default to 100ms" {
test "OSC: OSC 9;1 ConEmu sleep with no value default to 100ms" {
const testing = std.testing;
var p: Parser = .init();
@@ -2794,11 +2890,11 @@ test "OSC: conemu sleep with no value default to 100ms" {
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .sleep);
try testing.expectEqual(100, cmd.sleep.duration_ms);
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
}
test "OSC: conemu sleep cannot exceed 10000ms" {
test "OSC: OSC 9;1 conemu sleep cannot exceed 10000ms" {
const testing = std.testing;
var p: Parser = .init();
@@ -2808,11 +2904,11 @@ test "OSC: conemu sleep cannot exceed 10000ms" {
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .sleep);
try testing.expectEqual(10000, cmd.sleep.duration_ms);
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(10000, cmd.conemu_sleep.duration_ms);
}
test "OSC: conemu sleep invalid input" {
test "OSC: OSC 9;1 conemu sleep invalid input" {
const testing = std.testing;
var p: Parser = .init();
@@ -2822,11 +2918,39 @@ test "OSC: conemu sleep invalid input" {
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .sleep);
try testing.expectEqual(100, cmd.sleep.duration_ms);
try testing.expect(cmd == .conemu_sleep);
try testing.expectEqual(100, cmd.conemu_sleep.duration_ms);
}
test "OSC: show desktop notification" {
test "OSC: OSC 9;1 conemu sleep -> desktop notification 1" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;1";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("1", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;1 conemu sleep -> desktop notification 2" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;1a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("1a", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9 show desktop notification" {
const testing = std.testing;
var p: Parser = .init();
@@ -2836,11 +2960,25 @@ test "OSC: show desktop notification" {
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings(cmd.show_desktop_notification.title, "");
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world");
try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
try testing.expectEqualStrings("Hello world", cmd.show_desktop_notification.body);
}
test "OSC: show desktop notification with title" {
test "OSC: OSC 9 show single character desktop notification" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;H";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("", cmd.show_desktop_notification.title);
try testing.expectEqualStrings("H", cmd.show_desktop_notification.body);
}
test "OSC: OSC 777 show desktop notification with title" {
const testing = std.testing;
var p: Parser = .init();
@@ -2854,7 +2992,7 @@ test "OSC: show desktop notification with title" {
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Body");
}
test "OSC: conemu message box" {
test "OSC: OSC 9;2 ConEmu message box" {
const testing = std.testing;
var p: Parser = .init();
@@ -2863,11 +3001,11 @@ test "OSC: conemu message box" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_message_box);
try testing.expectEqualStrings("hello world", cmd.show_message_box);
try testing.expect(cmd == .conemu_show_message_box);
try testing.expectEqualStrings("hello world", cmd.conemu_show_message_box);
}
test "OSC: conemu message box invalid input" {
test "OSC: 9;2 ConEmu message box invalid input" {
const testing = std.testing;
var p: Parser = .init();
@@ -2875,11 +3013,12 @@ test "OSC: conemu message box invalid input" {
const input = "9;2";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b');
try testing.expect(cmd == null);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
}
test "OSC: conemu message box empty message" {
test "OSC: 9;2 ConEmu message box empty message" {
const testing = std.testing;
var p: Parser = .init();
@@ -2888,11 +3027,11 @@ test "OSC: conemu message box empty message" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_message_box);
try testing.expectEqualStrings("", cmd.show_message_box);
try testing.expect(cmd == .conemu_show_message_box);
try testing.expectEqualStrings("", cmd.conemu_show_message_box);
}
test "OSC: conemu message box spaces only message" {
test "OSC: 9;2 ConEmu message box spaces only message" {
const testing = std.testing;
var p: Parser = .init();
@@ -2901,11 +3040,39 @@ test "OSC: conemu message box spaces only message" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_message_box);
try testing.expectEqualStrings(" ", cmd.show_message_box);
try testing.expect(cmd == .conemu_show_message_box);
try testing.expectEqualStrings(" ", cmd.conemu_show_message_box);
}
test "OSC: conemu change tab title" {
test "OSC: OSC 9;2 message box -> desktop notification 1" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;2";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("2", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;2 message box -> desktop notification 2" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;2a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("2a", cmd.show_desktop_notification.body);
}
test "OSC: 9;3 ConEmu change tab title" {
const testing = std.testing;
var p: Parser = .init();
@@ -2914,11 +3081,11 @@ test "OSC: conemu change tab title" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .change_conemu_tab_title);
try testing.expectEqualStrings("foo bar", cmd.change_conemu_tab_title.value);
try testing.expect(cmd == .conemu_change_tab_title);
try testing.expectEqualStrings("foo bar", cmd.conemu_change_tab_title.value);
}
test "OSC: conemu change tab reset title" {
test "OSC: 9;3 ConEmu change tab title reset" {
const testing = std.testing;
var p: Parser = .init();
@@ -2928,11 +3095,11 @@ test "OSC: conemu change tab reset title" {
const cmd = p.end('\x1b').?;
const expected_command: Command = .{ .change_conemu_tab_title = .{ .reset = {} } };
const expected_command: Command = .{ .conemu_change_tab_title = .reset };
try testing.expectEqual(expected_command, cmd);
}
test "OSC: conemu change tab spaces only title" {
test "OSC: 9;3 ConEmu change tab title spaces only" {
const testing = std.testing;
var p: Parser = .init();
@@ -2942,11 +3109,11 @@ test "OSC: conemu change tab spaces only title" {
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .change_conemu_tab_title);
try testing.expectEqualStrings(" ", cmd.change_conemu_tab_title.value);
try testing.expect(cmd == .conemu_change_tab_title);
try testing.expectEqualStrings(" ", cmd.conemu_change_tab_title.value);
}
test "OSC: conemu change tab invalid input" {
test "OSC: OSC 9;3 change tab title -> desktop notification 1" {
const testing = std.testing;
var p: Parser = .init();
@@ -2954,11 +3121,27 @@ test "OSC: conemu change tab invalid input" {
const input = "9;3";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b');
try testing.expect(cmd == null);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("3", cmd.show_desktop_notification.body);
}
test "OSC: OSC9 progress set" {
test "OSC: OSC 9;3 message box -> desktop notification 2" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;3a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("3a", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;4 ConEmu progress set" {
const testing = std.testing;
var p: Parser = .init();
@@ -2967,12 +3150,12 @@ test "OSC: OSC9 progress set" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .set);
try testing.expect(cmd.progress_report.progress == 100);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expect(cmd.conemu_progress_report.progress == 100);
}
test "OSC: OSC9 progress set overflow" {
test "OSC: OSC 9;4 ConEmu progress set overflow" {
const testing = std.testing;
var p: Parser = .init();
@@ -2981,12 +3164,12 @@ test "OSC: OSC9 progress set overflow" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .set);
try testing.expect(cmd.progress_report.progress == 100);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expectEqual(100, cmd.conemu_progress_report.progress);
}
test "OSC: OSC9 progress set single digit" {
test "OSC: OSC 9;4 ConEmu progress set single digit" {
const testing = std.testing;
var p: Parser = .init();
@@ -2995,12 +3178,12 @@ test "OSC: OSC9 progress set single digit" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .set);
try testing.expect(cmd.progress_report.progress == 9);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expect(cmd.conemu_progress_report.progress == 9);
}
test "OSC: OSC9 progress set double digit" {
test "OSC: OSC 9;4 ConEmu progress set double digit" {
const testing = std.testing;
var p: Parser = .init();
@@ -3009,12 +3192,12 @@ test "OSC: OSC9 progress set double digit" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .set);
try testing.expect(cmd.progress_report.progress == 94);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expectEqual(94, cmd.conemu_progress_report.progress);
}
test "OSC: OSC9 progress set extra semicolon ignored" {
test "OSC: OSC 9;4 ConEmu progress set extra semicolon ignored" {
const testing = std.testing;
var p: Parser = .init();
@@ -3023,12 +3206,12 @@ test "OSC: OSC9 progress set extra semicolon ignored" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .set);
try testing.expect(cmd.progress_report.progress == 100);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .set);
try testing.expectEqual(100, cmd.conemu_progress_report.progress);
}
test "OSC: OSC9 progress remove with no progress" {
test "OSC: OSC 9;4 ConEmu progress remove with no progress" {
const testing = std.testing;
var p: Parser = .init();
@@ -3037,12 +3220,12 @@ test "OSC: OSC9 progress remove with no progress" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .remove);
try testing.expect(cmd.progress_report.progress == null);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
try testing.expect(cmd.conemu_progress_report.progress == null);
}
test "OSC: OSC9 progress remove with double semicolon" {
test "OSC: OSC 9;4 ConEmu progress remove with double semicolon" {
const testing = std.testing;
var p: Parser = .init();
@@ -3051,12 +3234,12 @@ test "OSC: OSC9 progress remove with double semicolon" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .remove);
try testing.expect(cmd.progress_report.progress == null);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
try testing.expect(cmd.conemu_progress_report.progress == null);
}
test "OSC: OSC9 progress remove ignores progress" {
test "OSC: OSC 9;4 ConEmu progress remove ignores progress" {
const testing = std.testing;
var p: Parser = .init();
@@ -3065,12 +3248,12 @@ test "OSC: OSC9 progress remove ignores progress" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .remove);
try testing.expect(cmd.progress_report.progress == null);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
try testing.expect(cmd.conemu_progress_report.progress == null);
}
test "OSC: OSC9 progress remove extra semicolon" {
test "OSC: OSC 9;4 ConEmu progress remove extra semicolon" {
const testing = std.testing;
var p: Parser = .init();
@@ -3079,11 +3262,11 @@ test "OSC: OSC9 progress remove extra semicolon" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .remove);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .remove);
}
test "OSC: OSC9 progress error" {
test "OSC: OSC 9;4 ConEmu progress error" {
const testing = std.testing;
var p: Parser = .init();
@@ -3092,12 +3275,12 @@ test "OSC: OSC9 progress error" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .@"error");
try testing.expect(cmd.progress_report.progress == null);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .@"error");
try testing.expect(cmd.conemu_progress_report.progress == null);
}
test "OSC: OSC9 progress error with progress" {
test "OSC: OSC 9;4 ConEmu progress error with progress" {
const testing = std.testing;
var p: Parser = .init();
@@ -3106,12 +3289,12 @@ test "OSC: OSC9 progress error with progress" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .@"error");
try testing.expect(cmd.progress_report.progress == 100);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .@"error");
try testing.expect(cmd.conemu_progress_report.progress == 100);
}
test "OSC: OSC9 progress pause" {
test "OSC: OSC 9;4 progress pause" {
const testing = std.testing;
var p: Parser = .init();
@@ -3120,12 +3303,12 @@ test "OSC: OSC9 progress pause" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .pause);
try testing.expect(cmd.progress_report.progress == null);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .pause);
try testing.expect(cmd.conemu_progress_report.progress == null);
}
test "OSC: OSC9 progress pause with progress" {
test "OSC: OSC 9;4 ConEmu progress pause with progress" {
const testing = std.testing;
var p: Parser = .init();
@@ -3134,12 +3317,68 @@ test "OSC: OSC9 progress pause with progress" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .progress_report);
try testing.expect(cmd.progress_report.state == .pause);
try testing.expect(cmd.progress_report.progress == 100);
try testing.expect(cmd == .conemu_progress_report);
try testing.expect(cmd.conemu_progress_report.state == .pause);
try testing.expect(cmd.conemu_progress_report.progress == 100);
}
test "OSC: OSC9 conemu wait input" {
test "OSC: OSC 9;4 progress -> desktop notification 1" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;4";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;4 progress -> desktop notification 2" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;4;";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4;", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;4 progress -> desktop notification 3" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;4;5";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4;5", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;4 progress -> desktop notification 4" {
const testing = std.testing;
var p: Parser = .init();
const input = "9;4;5a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("4;5a", cmd.show_desktop_notification.body);
}
test "OSC: OSC 9;5 ConEmu wait input" {
const testing = std.testing;
var p: Parser = .init();
@@ -3148,10 +3387,10 @@ test "OSC: OSC9 conemu wait input" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .wait_input);
try testing.expect(cmd == .conemu_wait_input);
}
test "OSC: OSC9 conemu wait ignores trailing characters" {
test "OSC: OSC 9;5 ConEmu wait ignores trailing characters" {
const testing = std.testing;
var p: Parser = .init();
@@ -3160,7 +3399,7 @@ test "OSC: OSC9 conemu wait ignores trailing characters" {
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .wait_input);
try testing.expect(cmd == .conemu_wait_input);
}
test "OSC: empty param" {
@@ -3415,3 +3654,45 @@ test "OSC: kitty color protocol no key" {
try testing.expect(cmd == .kitty_color_protocol);
try testing.expectEqual(0, cmd.kitty_color_protocol.list.items.len);
}
test "OSC: 9;6: ConEmu guimacro 1" {
const testing = std.testing;
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "9;6;a";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .conemu_guimacro);
try testing.expectEqualStrings("a", cmd.conemu_guimacro);
}
test "OSC: 9;6: ConEmu guimacro 2" {
const testing = std.testing;
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "9;6;ab";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .conemu_guimacro);
try testing.expectEqualStrings("ab", cmd.conemu_guimacro);
}
test "OSC: 9;6: ConEmu guimacro 3 incomplete -> desktop notification" {
const testing = std.testing;
var p: Parser = .initAlloc(testing.allocator);
defer p.deinit();
const input = "9;6";
for (input) |ch| p.next(ch);
const cmd = p.end('\x1b').?;
try testing.expect(cmd == .show_desktop_notification);
try testing.expectEqualStrings("6", cmd.show_desktop_notification.body);
}

View File

@@ -134,7 +134,7 @@ pub const Parser = struct {
self.idx += 1;
return .{ .unknown = .{
.full = self.params,
.partial = slice[0 .. self.idx - start + 1],
.partial = slice[0..@min(self.idx - start + 1, slice.len)],
} };
},
};

View File

@@ -249,7 +249,7 @@ pub fn Stream(comptime Handler: type) type {
// the parser state to ground.
0x18, 0x1A => self.parser.state = .ground,
// A parameter digit:
'0'...'9' => if (self.parser.params_idx < 16) {
'0'...'9' => if (self.parser.params_idx < Parser.MAX_PARAMS) {
self.parser.param_acc *|= 10;
self.parser.param_acc +|= c - '0';
// The parser's CSI param action uses param_acc_idx
@@ -259,7 +259,7 @@ pub fn Stream(comptime Handler: type) type {
self.parser.param_acc_idx |= 1;
},
// A parameter separator:
':', ';' => if (self.parser.params_idx < 16) {
':', ';' => if (self.parser.params_idx < Parser.MAX_PARAMS) {
self.parser.params[self.parser.params_idx] = self.parser.param_acc;
if (c == ':') self.parser.params_sep.set(self.parser.params_idx);
self.parser.params_idx += 1;
@@ -1598,14 +1598,19 @@ pub fn Stream(comptime Handler: type) type {
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},
.progress_report => |v| {
.conemu_progress_report => |v| {
if (@hasDecl(T, "handleProgressReport")) {
try self.handler.handleProgressReport(v);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},
.sleep, .show_message_box, .change_conemu_tab_title, .wait_input => {
.conemu_sleep,
.conemu_show_message_box,
.conemu_change_tab_title,
.conemu_wait_input,
.conemu_guimacro,
=> {
log.warn("unimplemented OSC callback: {}", .{cmd});
},
@@ -2596,3 +2601,22 @@ test "stream CSI ? W reset tab stops" {
try s.nextSlice("\x1b[?1;2;3W");
try testing.expect(s.handler.reset);
}
test "stream: SGR with 17+ parameters for underline color" {
const H = struct {
attrs: ?sgr.Attribute = null,
called: bool = false,
pub fn setAttribute(self: *@This(), attr: sgr.Attribute) !void {
self.attrs = attr;
self.called = true;
}
};
var s: Stream(H) = .init(.{});
// Kakoune-style SGR with underline color as 17th parameter
// This tests the fix where param 17 was being dropped
try s.nextSlice("\x1b[4:3;38;2;51;51;51;48;2;170;170;170;58;2;255;97;136;0m");
try testing.expect(s.handler.called);
}

View File

@@ -84,10 +84,23 @@ pub const Style = struct {
}
/// True if the style is equal to another style.
/// For performance do direct comparisons first.
pub fn eql(self: Style, other: Style) bool {
// We convert the styles to packed structs and compare as integers
// because this is much faster than comparing each field separately.
return PackedStyle.fromStyle(self) == PackedStyle.fromStyle(other);
inline for (comptime std.meta.fields(Style)) |field| {
if (comptime std.meta.hasUniqueRepresentation(field.type)) {
if (@field(self, field.name) != @field(other, field.name)) {
return false;
}
}
}
inline for (comptime std.meta.fields(Style)) |field| {
if (comptime !std.meta.hasUniqueRepresentation(field.type)) {
if (!std.meta.eql(@field(self, field.name), @field(other, field.name))) {
return false;
}
}
}
return true;
}
/// Returns the bg color for a cell with this style given the cell

View File

@@ -1513,7 +1513,8 @@ fn execCommand(
}
return switch (command) {
.direct => |v| v,
// We need to clone the command since there's no guarantee the config remains valid.
.direct => |_| (try command.clone(alloc)).direct,
.shell => |v| shell: {
var args: std.ArrayList([:0]const u8) = try .initCapacity(alloc, 4);
@@ -1688,3 +1689,35 @@ test "execCommand: direct command, error passwd" {
try testing.expectEqualStrings(result[0], "foo");
try testing.expectEqualStrings(result[1], "bar baz");
}
test "execCommand: direct command, config freed" {
if (comptime builtin.os.tag == .windows) return error.SkipZigTest;
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var command_arena = ArenaAllocator.init(alloc);
const command_alloc = command_arena.allocator();
const command = try (configpkg.Command{
.direct = &.{
"foo",
"bar baz",
},
}).clone(command_alloc);
const result = try execCommand(alloc, command, struct {
fn get(_: Allocator) !PasswdEntry {
// Failed passwd entry means we can't construct a macOS
// login command and falls back to POSIX behavior.
return error.Fail;
}
});
command_arena.deinit();
try testing.expectEqual(2, result.len);
try testing.expectEqualStrings(result[0], "foo");
try testing.expectEqualStrings(result[1], "bar baz");
}