227 Commits

Author SHA1 Message Date
Mitchell Hashimoto
3f7c3afaf9 ci: source tarball files must not be quoted 2024-12-31 12:47:50 -08:00
Mitchell Hashimoto
a857d56fb6 ci: proper blob file setup for source tarballs on release 2024-12-31 12:43:33 -08:00
Mitchell Hashimoto
df0620afe9 Fix SGR direct-color parsing issue (#4216)
Fix for a little parsing issue I took note of
[here](https://github.com/ghostty-org/ghostty/issues/2125#issuecomment-2466537683).

The disparity in behavior between `ghostty@main` and `xterm` can be seen
with this reproduction script:
```sh
printf "\e[0m\nForeground:\n";
printf "\e[38:2:0:255:0mGreen\n";
printf "\e[38;2;0;255;0mGreen\n";
printf "\e[38:2:0:255:0:255mMagenta\n";
printf "\e[38;2;0;255;0;255mGreen\n";

printf "\e[0m\nBackground:\n";
printf "\e[48:2:0:255:0mGreen\n";
printf "\e[48;2;0;255;0mGreen\n";
printf "\e[48:2:0:255:0:255mMagenta\n";
printf "\e[48;2;0;255;0;255mGreen\n";

printf "\e[0m\nUnderline:\n";
printf "\e[58:2:0:255:0m\e[4mGreen\n";
printf "\e[58;2;0;255;0m\e[4mGreen\n";
printf "\e[58:2:0:255:0:255m\e[4mMagenta\n";
printf "\e[58;2;0;255;0;255m\e[4mGreen\n";

printf "\e[0m\n";
```

### Outputs:
|`xterm`|`ghostty@main`|this PR|
|-|-|-|
|<img width="85" alt="image"
src="https://github.com/user-attachments/assets/a0aacff2-2103-4fff-9160-5e663d8a70a2"
/>|<img width="110" alt="image"
src="https://github.com/user-attachments/assets/0ad12e67-3f2c-46f3-b0ee-9230032d188a"
/>|<img width="110" alt="image"
src="https://github.com/user-attachments/assets/7477e3cf-7d27-419e-986b-8df581e52398"
/>|
2024-12-31 12:13:05 -08:00
Qwerasd
4543cdeac8 fix(terminal): correct SGR direct color parsing 2024-12-31 15:05:25 -05:00
Qwerasd
5ba8fee38a test/terminal: add failing sgr direct color parsing test
Behavior checked against xterm
2024-12-31 14:57:54 -05:00
Mitchell Hashimoto
47cf5cbb40 core: fix windows compile regression from #4021 (#4212) 2024-12-31 11:16:10 -08:00
Jeffrey C. Ollie
cf34ffa28e core: fix windows compile regression from #4021 2024-12-31 12:56:18 -06:00
Mitchell Hashimoto
eaa872216b write_*_file actions default to mode 0600 (#4201)
This commit changes the default filemode for the write actions so that
it is only readable and writable by the user running Ghostty.
2024-12-31 07:20:28 -08:00
Mitchell Hashimoto
d59a57e133 write_*_file actions default to mode 0600
This commit changes the default filemode for the write actions so that
it is only readable and writable by the user running Ghostty.
2024-12-31 07:16:43 -08:00
Mitchell Hashimoto
a30b2eda39 Handle short boolean flags in zsh/fish completions (#4039)
Closes: https://github.com/ghostty-org/ghostty/issues/2992
2024-12-31 06:45:44 -08:00
Maciej Bartczak
85ed9b626e explicitly handle bool values 2024-12-31 09:36:23 +01:00
Mitchell Hashimoto
ecfca17ad6 Update iTerm2 colorschemes (#4152)
Upstream revision:
e030599a6a
2024-12-30 21:40:28 -08:00
mitchellh
12a333dfb4 deps: Update iTerm2 color schemes 2024-12-31 05:40:18 +00:00
Mitchell Hashimoto
783a06689e config: fix segfault if font-family is reset via the CLI (#4151)
Fixes #4149
2024-12-30 21:37:23 -08:00
Mitchell Hashimoto
789e2024a5 config: fix segfault if font-family is reset via the CLI
Fixes #4149
2024-12-30 21:30:48 -08:00
Mitchell Hashimoto
d7c5017cd2 surface: don't issue mode 2031 DSR reports when colors are changed by a VT sequence (#3994)
#3965
2024-12-30 21:10:32 -08:00
Mitchell Hashimoto
413964774c input: parse triggers with codepoints that map to keys as translated (#4147)
Fixes #4146

This makes it so that keys such as `cmd+1` and `cmd+one` are identical.
2024-12-30 21:09:13 -08:00
Mitchell Hashimoto
aa81c16ba1 input: parse triggers with codepoints that map to keys as translated
Fixes #4146

This makes it so that keys such as `cmd+1` and `cmd+one` are identical.
2024-12-30 21:06:43 -08:00
Mitchell Hashimoto
fa4d4a38c1 gtk: make sure that window-decoration is honored on all paths (#4130)
Fix a regression from #4110 .
2024-12-30 19:22:26 -08:00
Jeffrey C. Ollie
f97f7e8a70 gtk: also add css window-decorated class when toggling window decorations 2024-12-30 19:40:13 -06:00
Mitchell Hashimoto
478fe3917c feat(config): generate default template when config file is not found (#3460)
Closes #3203
2024-12-30 14:32:05 -08:00
Leah Amelia Chen
98d77788f4 feat(config): generate default template when config file is not found
Closes #3203
2024-12-30 14:28:38 -08:00
Jeffrey C. Ollie
220d40e99a gtk: make sure that window-decoration is honored on all paths 2024-12-30 16:10:14 -06:00
Mitchell Hashimoto
d512f56005 macOS: weak self for event monitor to avoid retain cycle for controllers (#4128)
Fixes #3219

We were holding a reference cycle to the base terminal controller. This
was preventing the window from ever being fully deallocated.
2024-12-30 13:49:36 -08:00
Mitchell Hashimoto
dd41a9447d macOS: weak self for event monitor to avoid retain cycle for controllers
Fixes #3219

We were holding a reference cycle to the base terminal controller. This
was preventing the window from ever being fully deallocated.
2024-12-30 13:45:14 -08:00
Mitchell Hashimoto
d54817607c gtk: don't use gtk_window_set_titlebar if adwaita is enabled but it's older than 1.4.0 (#4110)
Fix #4097
2024-12-30 13:02:48 -08:00
Jeffrey C. Ollie
ffe1b7a872 gtk: don't use gtk_window_set_titlebar if adwaita is enabled but it's older than 1.4.0 2024-12-30 14:44:56 -06:00
Mitchell Hashimoto
0da8801dc9 Fix clipboard confirmation window typo (#4124)
uh just fixes a typo of appliclication instead of application for an
osc_52_read request
2024-12-30 12:41:40 -08:00
Mitchell Hashimoto
9204bb888f os: don't return stack memory (#4122)
A regression from
adcaff7137
2024-12-30 12:24:19 -08:00
kaizo
bdeb93fe87 Fix clipboard confirmation window typo 2024-12-30 15:23:16 -05:00
Mitchell Hashimoto
e9edd21bed os: don't return stack memory
A regression from adcaff7137
2024-12-30 12:21:28 -08:00
Mitchell Hashimoto
ef542c6e63 Enable bitmap font usage under CoreText (#4115)
macOS bitmap-only fonts are a poorly documented format, which are often
distributed as `.dfont` or `.dfon` files. They use a 'bhed' table in
place of the usual 'head', but the table format is byte-identical, so
enabling the use of bitmap-only fonts only requires us to properly fetch
this table while calculating metrics.

ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html

Reverts #3550 for obvious reasons, and may close issue #2168 because
this should now mean that bitmap fonts are properly supported under both
font backends due to #3837 - unless `otb` fonts are still not properly
supported under FreeType (they are not supported under CoreText because
CoreText does not know how to handle them).

I tested this change with the `.dfont` distribution of
[Cozette](https://github.com/slavfox/Cozette) v1.25.2 and saw no visual
issues.
2024-12-30 12:05:57 -08:00
Mitchell Hashimoto
41df2d9805 font/freetype: hardcode DPI in test to avoid variation between OS 2024-12-30 12:01:46 -08:00
Mitchell Hashimoto
d20446e4de feat: support for short hex colors in config (#4112)
closes #4111
2024-12-30 11:59:08 -08:00
acsetter
a6eec4cbe2 feat: support for short hex colors in config 2024-12-30 11:55:49 -08:00
Qwerasd
7a4215abd7 font/coretext: properly resolve metrics for bitmap-only fonts
macOS bitmap-only fonts are a poorly documented format, which are often
distributed as `.dfont` or `.dfon` files. They use a 'bhed' table in
place of the usual 'head', but the table format is byte-identical, so
enabling the use of bitmap-only fonts only requires us to properly fetch
this table while calculating metrics.

ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
2024-12-30 14:44:46 -05:00
Qwerasd
31f101c970 Revert "coretext: exclude bitmap fonts from discovery"
This reverts commit 322f166ca5.
2024-12-30 14:39:07 -05:00
Mitchell Hashimoto
2f6860fbc5 font/freetype: Enable bitmap glyphs for non-color faces (#3837)
This allows for crisp bitmap font rendering once again. Tested with
Terminus (TTF), and at both 1x and 2x DPI the font renders perfectly
(click for 1:1 size):

![Terminus (TTF), size 12, 96
DPI](https://github.com/user-attachments/assets/181ba561-ebe4-49df-aa41-68fc782f92b8)

<img alt="Terminus (TTF), size 12, 192 DPI"
src="https://github.com/user-attachments/assets/d34804fe-4966-42e8-bf9e-bd10570ad443"
width="384" height="50" />
2024-12-30 11:16:17 -08:00
Mitchell Hashimoto
f8c3dc1bbf fix: quick terminal focus-follows-mouse behaviour (#3814)
# Description

Quick Terminal now focuses on the surface under the mouse pointer when
`focus-follows-mouse` is enabled.

Fixes #3337
2024-12-30 11:10:38 -08:00
Damien Mehala
ade07c4c3c fix: quick terminal focus-follows-mouse behaviour
Quick Terminal now focuses on the surface under the mouse
pointer when `focus-follows-mouse` is enabled.

Fixes #3337
2024-12-30 11:04:21 -08:00
Mitchell Hashimoto
68318e2830 Set alpha component for fullscreen background colour (#3834)
This fixes a bug in the fullscreen behaviour on MacOS.

As per discussion #2840 native fullscreen on MacOS should set the
background to be opaque. Colours are incorrectly set due to the alpha
channel.

## Original behaviour:
When windowed:
<img width="810" alt="Screenshot 2024-12-28 at 9 02 09 PM"
src="https://github.com/user-attachments/assets/59bd4380-2744-42e6-99c7-6d7b19919206"
/>

When fullscreened:
<img width="1470" alt="Screenshot 2024-12-28 at 9 01 46 PM"
src="https://github.com/user-attachments/assets/a18f82ea-8cce-4d9b-8bb2-c279e2a753f0"
/>

## After the fix:
When fullscreened:
<img width="1470" alt="Screenshot 2024-12-28 at 9 13 17 PM"
src="https://github.com/user-attachments/assets/6c3f31ae-c206-4234-8bde-1886ce16c837"
/>
2024-12-30 10:55:48 -08:00
Mitchell Hashimoto
a15473d9bd fix(gtk): fix issue detecting preferred color scheme on older systems (#4035)
On my system (pop-os 22.04 LTS) with system theme and seperate light and
dark themes, ghostty always defaults to light mode. Switches between
light and dark mode are properly handled.

In the logs this error is reported:

error(gtk): unable to get current color scheme:
GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method
“ReadOne”

The spec notes that the other functions are "[Deprecated, use ReadOne
instead.](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html)"
so using ReadOne is cerainly the correct path.

I've managed to fix this on my system by checking for the error and
falling back to an implementation using the deprecated Read.

Discussion: https://github.com/ghostty-org/ghostty/discussions/3704
Issue: https://github.com/ghostty-org/ghostty/issues/4038
2024-12-30 10:53:32 -08:00
Mitchell Hashimoto
397bf41ec3 Ensure correct coordinate ordering in selection file write (#4078)
## Description

Fix an issue where `write_selection_file` would create empty files when
selecting multiple lines. This occurred because the selection
coordinates were not properly ordered when writing to file.

## Problem

When selecting multiple lines from bottom to top , the resulting file
would be empty. This happened because:

1. Selection coordinates were used directly without proper ordering

2. `start()` and `end()` don't guarantee top-to-bottom, left-to-right
ordering

## Solution

Use `topLeft()` and `bottomRight()` methods to ensure correct coordinate
ordering when writing selection to file.


## Testing

Tested the following scenarios:

- Selecting multiple lines from top to bottom 

- Selecting multiple lines from bottom to top 


## Known Behavior

When selecting a single word, the entire line containing that word is
written to the file. This is consistent with the current terminal
behavior as noted in the [original
discussion](https://github.com/ghostty-org/ghostty/discussions/3594).

Fixes ghostty-org/ghostty#3594
2024-12-30 10:51:38 -08:00
Bryan Lee
a1f7a95763 Add pin order assertion in Pin.pageIterator 2024-12-31 01:14:56 +08:00
Bryan Lee
c62f64866c Ensure correct coordinate ordering in selection file write
When writing selected text to file, use `topLeft` and `bottomRight` instead of
`start` and `end` to ensure correct coordinate ordering. This fixes an issue
where selection files could be empty when selecting text in reverse order.

- Use `terminal.Selection.topLeft()` for start coordinate
- Use `terminal.Selection.bottomRight()` for end coordinate
2024-12-31 01:14:56 +08:00
Mitchell Hashimoto
716848cb58 macos: always have terminal fullscreen style (#3831)
~~When processing toggle_fullscreen actions, multiple FullscreenStyle
derived objects would register observers on the same window and
subsequently remove them when the old style was deinited.~~

~~This change passes ownership of the fullscreenNotification observers
to TerminalController while still allowing FullscreenStyles access to
the window for style specific actions.~~

We need to have at least one fullscreen style (even if it is not being
used) to make sure that observers for fullscreen state changes are set
up. An alternative approach would be the first version of this PR (for
which my reasoning was wrong but the end change was correct) which
registers the fullscreen observers at either the TerminalController or
BaseTerminalController level.

Fixes #3553
2024-12-30 08:56:19 -08:00
Mitchell Hashimoto
a7c93cdfb1 apprt/gtk: fix the combination of gtk-titlebar=false and gtk-tabs-location=hidden (#3957)
Fixes: #3178
2024-12-30 08:55:22 -08:00
Mitchell Hashimoto
e17bb69645 config: edit opens AppSupport over XDG on macOS, prefers non-empty paths (#4004)
Fixes #3953
Fixes #3284

This fixes two issues. In fixing one issue, the other became apparent so
I fixed both in this one commit.

The first issue is that on macOS, the `open` command should take the
`-t` flag to open text files in a text editor. To do this, the `os.open`
function now takes a type hint that is used to better do the right
thing.

Second, the order of the paths that we attempt to open when editing a
config on macOS is wrong. Our priority when loading configs is well
documented:
https://ghostty.org/docs/config#macos-specific-path-(macos-only). But
open_config does the opposite. This makes it too easy for people to have
configs that are being overridden without them realizing it.

This commit changes the order of the paths to match the documented
order. If neither path exists, we prefer AppSupport.
2024-12-30 08:52:55 -08:00
Mitchell Hashimoto
318641f5a1 surface: handle hyperlinks more reliably (#3903)
We refresh the link hover state in two (generic) cases

1. When the modifiers change
2. When the cursor changes position

Each of these have additional state qualifiers. Modify the qualifiers
such that we refresh links under the following scenarios:

1. Modifiers change
  - Control is pressed (this is handled in the renderer)
  - Mouse reporting is off
    OR
    Mouse reporting is on AND shift is pressed AND we are NOT reporting
    shift to the terminal

2. Cursor changes position
  - Control is pressed (this is handled in the renderer)
  - We previously were over a link
  - The position changed (or we had no previous position)
  - Mouse reporting is off
    OR
    Mouse reporting is on AND shift is pressed AND we are NOT reporting
    shift to the terminal

This fixes a few issues with the previous implementation:

1. If mouse reporting was on and you were over a link, pressing ctrl
   would enable link hover state. If you moved your mouse, you would
   exit that state. The logic in the keyCallback and the
   cursorPosCallback was not the same. Now, they both check for the same
   set of conditions
2. If mouse reporting was off, you could hold control and move the mouse
   to discover links. If mouse reporting was on, holding control + shift
   would not allow you to discover links. You had to be hovering one
   when you pressed the modifiers. Previously, we only refreshed links
   if we *weren't* reporting the mouse event. Now, we refresh links even
   even if we report a mouse event (ie a mouse motion event with the
   shift modifier pressed *will* hover links and also report events)

## Old Behavior

Notice that the state of the hyperlink is erratic in `comlink`. When I
am over it and press ctrl the link is underlined and the url hint in the
lower left shown for one frame, but then the state is dropped.


https://github.com/user-attachments/assets/52d6a8c8-8459-4d67-85eb-5d91f9833771

## New Behavior

State is retained when holding ctrl+shift. And the link only underlines
if I press both ctrl+shift. If I move the mouse around while holding
these keys, I can discover new links.


https://github.com/user-attachments/assets/78fa8e97-eb0c-4618-bd96-fe40d6bc67ce
2024-12-30 08:50:35 -08:00
Mitchell Hashimoto
26b1888494 apprt/gtk: move some static CSS to the style.css file (#4011) 2024-12-30 08:47:55 -08:00
Mitchell Hashimoto
1a27ce0797 bash: improved 'sudo' command wrapper (#4080)
The previous approach to wrapping `sudo` had a few shortcomings:

1. We were (re)defining our 'sudo' function wrapper in the "precmd"
path. It only needs to be defined once in the shell session.
2. If there was an existing 'sudo' alias, the function definition would
conflict and result in a syntax error.

Fix (1) by hoisting the 'sudo' function into global scope. I also
considered only defining our wrapper if an executable `sudo` binary
could be found (e.g. `-x $(builtin command -v sudo)`, but let's keep the
existing behavior for now. This allows for a `sudo` command to be
installed later in the shell session and still be wrapped.

Address (2) by defining the wrapper function using `function sudo`
(instead of `sudo()`) syntax. An explicit function definition won't
clash with an existing 'sudo' alias, although the alias will continue to
take precedence (i.e. our wrapper won't be called). If the alias is
defined _after_ our 'sudo' function is defined, our function will call
the aliased command.

This ordering is relevant because it can result in different behaviors
depending on when a user defines their aliases relative to sourcing the
shell integration script. Our recommendation remains that users either
use automatic shell injection or manually source the shell integration
script _before_ other things in their `.bashrc`, so that aligns with the
expected behavior of the 'sudo' wrapper with regard to aliases. Given
that, I don't think we need any more explicit user-facing documentation
on this beyond the script-level comments.
2024-12-30 08:44:47 -08:00
Mitchell Hashimoto
adcaff7137 config: edit opens AppSupport over XDG on macOS, prefers non-empty paths
Fixes #3953
Fixes #3284

This fixes two issues. In fixing one issue, the other became apparent so
I fixed both in this one commit.

The first issue is that on macOS, the `open` command should take the
`-t` flag to open text files in a text editor. To do this, the `os.open`
function now takes a type hint that is used to better do the right
thing.

Second, the order of the paths that we attempt to open when editing a
config on macOS is wrong. Our priority when loading configs is well documented:
https://ghostty.org/docs/config#macos-specific-path-(macos-only). But
open_config does the opposite. This makes it too easy for people to have
configs that are being overridden without them realizing it.

This commit changes the order of the paths to match the documented
order. If neither path exists, we prefer AppSupport.
2024-12-30 08:43:59 -08:00
Mitchell Hashimoto
c2c578789b Correct comptime GTK atLeast() version comparison (#3977)
The comptime path of the GTK `atLeast()` version function fails to take
the proceeding portion of the version into account. For example version
5.1.0 is incorrectly marked as less than 4.16.7 due to the minor version
(1) being less than the minor we are comparing against (16).

This update required that the major versions be equal when comparing
minor versions and the major and minor versions be equal when comparing
micro versions.

For example, building against GTK 4.17.1:
Before: version.atLeast(4,16,2) -> false
After:  version.atLeast(4,16,2) -> true
2024-12-30 08:39:56 -08:00
Mitchell Hashimoto
1c2532c184 gtk: correct comptime adwaita.versionAtLeast() comparison (#4084)
Companion to #3977
2024-12-30 08:39:47 -08:00
Mitchell Hashimoto
ff50b5539e fix: quick terminal CPU spikes (#4055)
# Description

The following code is causing an infinite loop that causes a CPU spikes
until the quick terminal is displayed:

87bd0bb744/macos/Sources/Features/QuickTerminal/QuickTerminalController.swift (L314-L317)

## Reproduce steps
1. Open Ghostty.
2. Open the Quick Terminal.
3. Close the Quick Terminal.
4. Reload the configuration (Ghostty > Reload Configuration or
`shift+cmd+,`).
5. Observe CPU spike.

## Fix
Now, `syncAppearance` doesn't postpone the process until it can be
consumed, and the appearance is synchronized once the animation is done
and the quick terminal is visible.

Fixes #3998
2024-12-30 08:21:21 -08:00
Jeffrey C. Ollie
f2ac9b85e3 gtk: correct comptime adwaita.versionAtLeast() comparison 2024-12-30 09:52:21 -06:00
Damien Mehala
011c17da41 fix: quick terminal CPU spikes
Fixes #3998
2024-12-30 07:24:35 -08:00
Mitchell Hashimoto
da186fb9df bash: remove "request for experts" comment (#4081)
We now have a multiple folks who have pitched in to improve this script.
2024-12-30 07:22:25 -08:00
Jon Parise
0dc3ea35c0 bash: remove "request for experts" comment
We now have a multiple folks who have pitched in to improve this script.
2024-12-30 10:19:55 -05:00
Jon Parise
4e7982fc2b bash: improved 'sudo' command wrapper
The previous approach to wrapping `sudo` had a few shortcomings:

1. We were (re)defining our 'sudo' function wrapper in the "precmd"
   path. It only needs to be defined once in the shell session.
2. If there was an existing 'sudo' alias, the function definition would
   conflict and result in a syntax error.

Fix (1) by hoisting the 'sudo' function into global scope. I also
considered only defining our wrapper if an executable `sudo` binary
could be found (e.g. `-x $(builtin command -v sudo)`, but let's keep the
existing behavior for now. This allows for a `sudo` command to be
installed later in the shell session and still be wrapped.

Address (2) by defining the wrapper function using `function sudo`
(instead of `sudo()`) syntax. An explicit function definition won't
clash with an existing 'sudo' alias, although the alias will continue to
take precedence (i.e. our wrapper won't be called). If the alias is
defined _after_ our 'sudo' function is defined, our function will call
the aliased command.

This ordering is relevant because it can result in different behaviors
depending on when a user defines their aliases relative to sourcing the
shell integration script. Our recommendation remains that users either
use automatic shell injection or manually source the shell integration
script _before_ other things in their `.bashrc`, so that aligns with the
expected behavior of the 'sudo' wrapper with regard to aliases. Given
that, I don't think we need any more explicit user-facing documentation
on this beyond the script-level comments.
2024-12-30 10:15:04 -05:00
Iain H
4f2110bce0 Be more idiomatic in tests when comparing to booleans 2024-12-30 08:25:03 -06:00
Mitchell Hashimoto
89e5ca2fb4 macos: add generated Nvim files to app bundle (#4031)
resolves #3966

<img width="689" alt="image"
src="https://github.com/user-attachments/assets/a969e1ee-2302-44cf-857d-f71345d4840b"
/>
2024-12-30 06:23:44 -08:00
Maciej Bartczak
d01b2397f1 fish: handle short boolean flags 2024-12-30 10:39:43 +01:00
Maciej Bartczak
aed61b62ae zsh: handle short boolean flags 2024-12-30 10:29:28 +01:00
Leigh Oliver
e9bc033b88 fix(gtk): fix issue detecting preferred color scheme 2024-12-30 09:17:46 +00:00
Bryan Lee
c011c4622d macos: add generated Nvim files to app bundle 2024-12-30 17:01:48 +08:00
Tristan Partin
057dd3e209 apprt/gtk: move some static CSS to the style.css file
Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 23:51:29 -06:00
Mitchell Hashimoto
87bd0bb744 gtk: Install 1024x1024 icon (#4003)
Fixes #3967

Include the 1024x1024 icon in the files installed by `build.zig` when
building the package for Linux with the GTK app runtime.
2024-12-29 21:18:41 -08:00
Arvin Verain
cfeed2b7a2 gtk: Install 1024x1024 icon 2024-12-30 13:05:57 +08:00
Mitchell Hashimoto
d2d6f8b9f4 apprt/gtk: make window-decoration=false with gtk-titlebar=true look better (#3999)
Before this change, there seemed to be some artifacting in the window
corners due to the window border no longer outlining the content
properly. By detecting the situation, we can turn the window border
radius off.
2024-12-29 21:01:01 -08:00
Tristan Partin
31c9a2fe59 apprt/gtk: make window-decoration=false with gtk-titlebar=true look better
Before this change, there seemed to be some artifacting in the window
corners due to the window border no longer outlining the content
properly. By detecting the situation, we can turn the window border
radius off.

Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 22:57:48 -06:00
moni-dz
4d983a2083 surface: don't issue mode 2031 DSR reports when colors are changed by a VT sequence 2024-12-30 12:33:05 +08:00
Tristan Partin
0fd65035c5 apprt/gtk: fix the combination of gtk-titlebar=false and gtk-tabs-location=hidden
Fixes: #3178
Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 22:12:37 -06:00
Mitchell Hashimoto
3059d9036b macos: Help menu goes to website docs (#3990) 2024-12-29 19:32:13 -08:00
Mitchell Hashimoto
37c5f5a123 macos: Help menu goes to website docs 2024-12-29 19:27:22 -08:00
Mitchell Hashimoto
35eefd4240 Add back quotes for variables in the zsh shell integration to improve compatibility (#3989)
https://github.com/ghostty-org/ghostty/pull/3332#issuecomment-2564526118
According to this comment, using variables without quoting may not work
with `setopt SH_WORD_SPLIT`.

We don't need to quote variables in `[[ ]]` because it works
differently.
2024-12-29 19:26:09 -08:00
Caleb Norton
8607b1f844 macos: correctly save terminal fullscreen style 2024-12-29 21:24:48 -06:00
XiaoYan Li
a9c1a5c73c Add back quotes for variables in the zsh shell integration to improve compatibility
https://github.com/ghostty-org/ghostty/pull/3332#issuecomment-2564526118
According to this comment, using variables without quoting may not work with `setopt SH_WORD_SPLIT`.

We don't need to quote variables in `[[ ]]` because it works differently.
2024-12-30 11:24:39 +08:00
Mitchell Hashimoto
06389b280a macos: restore 0 blur-radius when reloading config (#3954)
If a blur radius config value was previously set but then removed or set
to 0, the new blur radius would not take effect on config reload due to
the early return clause.
2024-12-29 19:17:25 -08:00
Mitchell Hashimoto
cb96ce5dcc apprt/gtk: fix website link in about window (#3958) 2024-12-29 19:17:11 -08:00
Iain H
936d0c0d58 Add unit tests 2024-12-29 21:01:18 -06:00
Tristan Partin
7256ebe13d apprt/gtk: fix website link in about window 2024-12-29 16:47:44 -06:00
Caleb Norton
479e735e7f macos: restore 0 blur-radius when reloading config 2024-12-29 16:25:30 -06:00
Iain H
b3290f6887 Correct the comptime GTK atLeast() function
The comptime path of the GTK `atLeast()` version function fails to take
the preceeding portion of the version into account. For example version
5.1.0 is incorrectly marked as less than 4.16.7 due to the minor version
(1) being less than the minor we are comparing against (16).

For example, building against GTK 4.17.1:
Before: version.atLeast(4,16,2) -> false
After:  version.atLeast(4,16,2) -> true
2024-12-29 16:20:20 -06:00
Mitchell Hashimoto
d5703a57e7 Allow startup with $HOME read-only (#3949)
Fixes #3202

Two changes to get here:

1. Do not fail startup if crash reporting initialization fails. This is
a non-critical feature and should not prevent the terminal from
starting.

2. Avoid freeing Sentry transport on error path. This segfaults.
Upstream issue will be reported separately.
2024-12-29 14:05:15 -08:00
Mitchell Hashimoto
84a03aa202 Allow startup without $HOME writable
Fixes #3202

Two changes to get here:

1. Do not fail startup if crash reporting initialization fails. This is a
   non-critical feature and should not prevent the terminal from starting.

2. Avoid freeing Sentry transport on error path. This segfaults.
   Upstream issue will be reported separately.
2024-12-29 13:59:39 -08:00
Mitchell Hashimoto
b3925b83ae note source tarball name for 1.0.0 2024-12-29 13:39:11 -08:00
Mitchell Hashimoto
5698358c2f apprt/gtk: style the tab overview when window-theme=ghostty (#3920)
This requires libadwaita main (to be 1.8).

Signed-off-by: Tristan Partin <tristan@partin.io>


https://github.com/user-attachments/assets/057abec5-f85b-4fbb-980c-c58a20e7ddc7
2024-12-29 13:30:44 -08:00
Mitchell Hashimoto
ff6e60be56 Fix typo in list-themes browser (#3919)
This is a small PR to fix a tiny continuity typo I noticed in the
list-themes viewer.

#### Before and After


![image](https://github.com/user-attachments/assets/c95b23bb-01ad-4e82-8f9b-0b8559d006ba)
2024-12-29 13:29:41 -08:00
Mitchell Hashimoto
db0da9a273 Fix typo in config reference: window-decorations -> window-decoration (#3930)
I made the same PR to the autogenerated reference here:
https://github.com/ghostty-org/website/pull/168.
2024-12-29 13:28:44 -08:00
Mitchell Hashimoto
31ee48a355 apprt/gtk: create the tab overview even if gtk-titlebar=false (#3940)
self.isAdwWindow() obscures that check a bit.
2024-12-29 13:27:11 -08:00
Mitchell Hashimoto
b7dba0c5f5 macos: disable auto-updates for local builds (#3943)
The auto-update prompt isn't useful for local (source) builds. Disable
it by default by setting Sparkle's SUEnableAutomaticChecks Info.plist
key to NO (false) for all build configurations.

We then selectively re-enable it by deleting that Info.plist key from
our release workflows. We delete the key instead of setting its value to
YES (true) to give us Sparkle's default behavior of prompting the user
to enable update checks on the second application launch. (YES tells
Sparkle to skip that prompt and silently enable update checks.)

See also: https://sparkle-project.org/documentation/customization/

(This is a safer alternative to #3273.)

Fixes: #3179
2024-12-29 13:26:35 -08:00
Mitchell Hashimoto
3b8a0ed2b8 CI: Update release-tag.yml to include the version in the source archive and prefix within (#3490)
Continuing from #3043 I agree that it seems idiomatic to have an archive
with format <name>-<version>.tar.gz and matching prefix for packaging,
RPM and Debian packaging guides seem to assume this format and the
automated extract tooling assumes it too.

# Testing
I haven't tested running this workflow, and am unsure about the yaml
substitution at lines 105-106

# Breaking changes
This would break existing packaging scripts, not sure how we want to
version it
2024-12-29 13:25:28 -08:00
Mitchell Hashimoto
9c15d8de35 ci: keep old source tarballs as well 2024-12-29 13:24:28 -08:00
Jon Parise
074edd3065 macos: disable auto-updates for local builds
The auto-update prompt isn't useful for local (source) builds. Disable
it by default by setting Sparkle's SUEnableAutomaticChecks Info.plist
key to NO (false) for all build configurations.

We then selectively re-enable it by deleting that Info.plist key from
our release workflows. We delete the key instead of setting its value to
YES (true) to give us Sparkle's default behavior of prompting the user
to enable update checks on the second application launch. (YES tells
Sparkle to skip that prompt and silently enable update checks.)

See also: https://sparkle-project.org/documentation/customization/
2024-12-29 15:48:59 -05:00
Tristan Partin
27ddc2a9b2 apprt/gtk: create the tab overview even if gtk-titlebar=false
self.isAdwWindow() obscures that check a bit.

Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 14:45:45 -06:00
521337
7881bb2bf0 Fix typo in config reference: window-decorations -> window-decoration 2024-12-29 20:07:36 +01:00
Tristan Partin
c20fe23946 apprt/gtk: use CSS variables to deduplicate code
Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 12:11:26 -06:00
Tristan Partin
2e048c870c apprt/gtk: style the tab overview when window-theme=ghostty
This requires libadwaita main (to be 1.8).

Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 12:11:13 -06:00
goproslowyo
329c87a2b4 Merge branch 'main' into list-themes-typo 2024-12-29 17:54:30 +00:00
GoProSlowYo
c8c3fb2900 Fix typo in list-themes browser
Signed-off-by: GoProSlowYo <68455785+goproslowyo@users.noreply.github.com>
2024-12-29 09:39:46 -08:00
Mitchell Hashimoto
6508fec945 Revert "macos: disable auto-updates for local (source) builds (#3273)"
This reverts commit b39850fd8b, reversing
changes made to 64ea3a1a29.
2024-12-29 09:22:54 -08:00
Mitchell Hashimoto
b39850fd8b macos: disable auto-updates for local (source) builds (#3273)
The auto-update prompt isn't useful for local (source) builds so disable
both update checks and automatic downloads for the Debug and Release
configurations.

Fixes #3179
2024-12-29 09:20:07 -08:00
Mitchell Hashimoto
64ea3a1a29 renderer: track if fg/bg/cursor color is explicitly set by an OSC (#3228)
The renderer must track if the foreground, background, and cursor colors
are explicitly set by an OSC so that changes are not overridden when the
config file is reloaded.

Fixes: https://github.com/ghostty-org/ghostty/issues/2795
2024-12-29 09:05:37 -08:00
Mitchell Hashimoto
65a1c0df80 Redo: Handle execveZ failures in Command.zig tests (#3176)
https://github.com/ghostty-org/ghostty/pull/3130 do over. Github seems
to have lost track of the fork status for the previous work. (perhaps
permissions?). Rebased the PR to main as I can't see why it could
possibly fail.

<details>
  <summary>Previous description</summary>
Bit of a rabbit hole came up while trying to package ghostty for nixos.
zig build test would just hang when run as part of checkPhase in a nix
build. (rather than the nix develop ... command run in CI).

Here's what I understand so far:

/usr/bin/env does not exist in a nix build sandbox. This only works as a
test binary when running in nix develop as it shares its environment
with the user where this compatibility crutch exists.
When executing Command.zig tests that reference /usr/bin/env the
eventual call to fork+execevZ will duplicate the running test process as
execevZ returns rather than dissapearing into the new exec'd process.
Duplicating a test process via fork does unexepected things. zig build
test will hang for <reasons?>. A test binary created via -Demit-test-exe
will run two copies of the test suite producing two different failure
outputs for the same test. (or ~4 copies of the test framework, one
extra for each test that fails this way)

/bin/sh can be used and an alternative test target. It isn't amazing as
it's relying on stdenv creating /bin/sh and it just existing on user
systems. Other alternatives I can think of would require build flags or
some sort of contract with packaging around what binary will exist for
the Command.zig tests.
</details>
2024-12-29 09:05:05 -08:00
Mitchell Hashimoto
25a4a89ee3 config: add title_report (default false) to configure CSI 21 t (#3908) 2024-12-29 08:59:21 -08:00
Mitchell Hashimoto
5be77ded3a config: add title_report (default false) to configure CSI 21 t 2024-12-29 08:56:53 -08:00
Mitchell Hashimoto
02538bed3c fix: scrollback limit not being parsed correctly (#3906)
Change `scrollback-limit` type to a usize rather than a u32 to handle
parsing values larger than 4294967295.

Fixes https://github.com/ghostty-org/ghostty/issues/3900
2024-12-29 08:41:39 -08:00
Adam Wolf
a787e4b8fc fix: scrollback limit not being parsed correctly 2024-12-29 10:29:01 -06:00
Tim Culverhouse
e24f33ae6b surface: handle hyperlinks more reliably
We refresh the link hover state in two (generic) cases

1. When the modifiers change
2. When the cursor changes position

Each of these have additional state qualifiers. Modify the qualifiers
such that we refresh links under the following scenarios:

1. Modifiers change
  - Control is pressed (this is handled in the renderer)
  - Mouse reporting is off
    OR
    Mouse reporting is on AND shift is pressed AND we are NOT reporting
    shift to the terminal

2. Cursor changes position
  - Control is pressed (this is handled in the renderer)
  - We previously were over a link
  - The position changed (or we had no previous position)
  - Mouse reporting is off
    OR
    Mouse reporting is on AND shift is pressed AND we are NOT reporting
    shift to the terminal

This fixes a few issues with the previous implementation:

1. If mouse reporting was on and you were over a link, pressing ctrl
   would enable link hover state. If you moved your mouse, you would
   exit that state. The logic in the keyCallback and the
   cursorPosCallback was not the same. Now, they both check for the same
   set of conditions
2. If mouse reporting was off, you could hold control and move the mouse
   to discover links. If mouse reporting was on, holding control + shift
   would not allow you to discover links. You had to be hovering one
   when you pressed the modifiers. Previously, we only refreshed links
   if we *weren't* reporting the mouse event. Now, we refresh links even
   even if we report a mouse event (ie a mouse motion event with the
   shift modifier pressed *will* hover links and also report events)
2024-12-29 10:04:06 -06:00
Mitchell Hashimoto
64c393716a GTK: add delay before updating the title (#3746)
This pr fixes https://github.com/ghostty-org/ghostty/issues/2503 for
GTK.
The implementation is quite similar to what was done in
https://github.com/ghostty-org/ghostty/pull/2929 for the macOS version.

Before (i was able to reproduce the issue by just invoking `ls`):


https://github.com/user-attachments/assets/011acb1d-de71-46a1-8a14-45e8eb932183



After:


https://github.com/user-attachments/assets/b749cd1c-355e-47de-a976-62d98a56f966
2024-12-29 07:18:56 -08:00
Mitchell Hashimoto
2ab0376d80 misc: add desktop entry fields to support xdg-terminal-exec (#3853)
This PR adds fields to desktop entry that are standardized in [Proposal
for XDG Default Terminal Execution Specification
(xdg-terminal-exec)](https://github.com/Vladimir-csp/xdg-terminal-exec).
2024-12-29 07:16:41 -08:00
Mitchell Hashimoto
d359231e5c Add default Nix overlay (#3847)
Adding an overlay allows to easily change the version of `ghostty`
provided by `nixpkgs`:

```nix
pkgs = import nixpkgs {
  inherit system;
  overlays = [ ghostty.overlays.default ];
}
```

Then, all references to `pkgs.ghostty` would refer to this project's
package definition.
2024-12-29 07:14:12 -08:00
Sebastian Estrella
16f81353cc Add default Nix overlay 2024-12-29 07:12:59 -08:00
Mitchell Hashimoto
9bef43fd99 Fix markdown formatting for (blank) in docs (#3850)
I realize I'm rolling the dice by opening a PR without a pre-approved
issue, but I'll take that chance. Feel free to close if this isn't
desired; no hard feelings!

Currently, the docs for `cursor-style-blink` have two backticks side by
side in the docs, which end up rendering as actual backticks rather than
a code-formatted blank space:

<img width="721" alt="Screenshot 2024-12-28 at 11 19 02 PM"
src="https://github.com/user-attachments/assets/295369d6-624f-4efe-a7ea-495c9fd216bb"
/>

This change puts a space between the backticks so they render as a
code-formatted space.
2024-12-29 07:11:32 -08:00
Mitchell Hashimoto
56681741cb chore: Add milestone workflow to add milestone to merged PR and fixed issues (#3849)
## Description:

This workflow will do the following things:

- Add milestone to a merged PR automatically, example [action
detail](https://github.com/nushell/nushell/actions/runs/12530004607/job/34946134565)
- Add milestone to a closed issue that has a merged PR fix (if any),
example [action
detail](https://github.com/nushell/nushell/actions/runs/12515661380/job/34913564683)

If there is no opened milestone the action will stop. If there are
multiple opened milestones, the action will bind to the one whose due
date is closest to the PR merged date and fall back to the first one
sorted by the milestone created date.

We have use it in Nushell for a while, such as
[v0.101.0](https://github.com/nushell/nushell/issues?q=is%3Aclosed+milestone%3Av0.101.0)

Don't merge it if it's not a good fit
2024-12-29 07:08:57 -08:00
hustcer
c84fefc4ea chore: Add milestone workflow to add milestone to merged PR and fixed Issues 2024-12-29 07:06:47 -08:00
Mitchell Hashimoto
574407aacd gtk/x11: link directly to libX11, no more dlopen (#3857)
As a follow-up to #3477 and #3748, this eliminates the use of dlopen to
access `libX11` functions by directly linking `libX11` if X11 is
enabled. This should also fix problems with systems like NixOS and Void
Linux that have reported problems using Ghostty on X11 when using the
distribution packages.
2024-12-29 07:02:28 -08:00
Mitchell Hashimoto
579de8e491 apprt: add window-titlebar-{background,foreground} config options (#3806)
This gives people finer-grained control over the coloring of their
titlebars. Currently only implemented for GTK.
2024-12-29 06:24:58 -08:00
Mitchell Hashimoto
6ca64bc410 cgroup: change suffix to .scope (#3844)
I think it's more consistent with the name convention used by systemd
and other terminal emulators (e.g. gnome-terminal).

See also: https://systemd.io/CGROUP_DELEGATION/#systemds-unit-types
2024-12-29 06:24:21 -08:00
Maciej Bartczak
2b245c965c Invalidate the timer when the surface is destroyed 2024-12-29 09:27:59 +01:00
Tristan Partin
a92ed15baa apprt: add window-titlebar-{background,foreground} config options
This gives people finer-grained control over the coloring of their
window titlebars. Currently only implemented for GTK.

Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-29 01:17:47 -06:00
Jeffrey C. Ollie
b6e45d49a3 gtk/x11: link directly to libX11, no more dlopen 2024-12-29 01:15:01 -06:00
Misaki Kasumi
5c2fb580d0 misc: add desktop entry fields to support xdg-terminal-exec 2024-12-29 13:58:12 +08:00
sin-ack
5e14b8e501 font/freetype: Downgrade pixfmt conversion log to debug
This is an expected occurrence with bitmap glyphs and causes unnecessary
spam when using the terminal with one.
2024-12-29 04:56:17 +00:00
sin-ack
1a6d9590a2 font/freetype: Add test for crisp bitmap font rendering
Now we can be certain that bitmap fonts stay crisp. :^)
2024-12-29 04:55:29 +00:00
Richard Feldman
1ca25d3b5e Fix markdown formatting for (blank) in docs
It currently has two backticks side by side, which end up rendering as actual backticks rather than a code-formatted blank space.
2024-12-28 23:19:52 -05:00
Mitchell Hashimoto
c8950d376a Use premultiplied alpha for renderer clearColor (#3347)
Fixes #3324

---

I haven't tested it on Linux yet, but I believe it has the similar
problem and could be fixed by this PR as well.
2024-12-28 19:39:10 -08:00
XiaoYan Li
28bbd526f2 Fix the typing error when building on Linux
Co-authored-by: Gareth Widlansky <101901964+gerblesh@users.noreply.github.com>
2024-12-28 19:37:24 -08:00
Xiaoyan Li
2993d12de7 Use premultiplied alpha for renderer clearColor
Fixes #3324
2024-12-28 19:37:24 -08:00
Misaki Kasumi
02ca5bedac cgroup: change suffix to .scope 2024-12-29 11:25:45 +08:00
Mitchell Hashimoto
b50e087581 Add always in confirm-close-surface config option (#3700)
Fixes #3648
2024-12-28 19:22:47 -08:00
Mohammadi, Erfan
85fc49b22c confirm-close-surface option can be set to always to always require confirmation
Fixes #3648
The confirm-close-surface configuration can now be set to always
ensuring a confirmation dialog is shown before closing a surface, even
if shell integration indicates no running processes.
2024-12-28 19:16:36 -08:00
Mitchell Hashimoto
9fc1e4e91a ci: our GTK tests were installing Cachix twice 2024-12-28 18:53:13 -08:00
Mitchell Hashimoto
a2c446cb73 Add default keybinding cmd+backspace=esc:w for macOS (#3679)
regarding https://github.com/ghostty-org/ghostty/issues/3646
2024-12-28 18:46:40 -08:00
Mitchell Hashimoto
1783ec922d Update iTerm2 colorschemes (#3824)
Upstream revision:
08df2e8a72
2024-12-28 18:45:12 -08:00
Mitchell Hashimoto
b8a75c24a6 Update the pre-approved issue template 2024-12-28 18:37:17 -08:00
sin-ack
ea8fe9a4b0 font/freetype: Enable bitmap glyphs for non-color faces
This allows for crisp bitmap font rendering once again.
2024-12-29 02:27:56 +00:00
sin-ack
bfde326bcb font/freetype: Rewrite monoToGrayscale algorithm
The original version had issues converting properly and caused broken
glyphs. This version tries to be as simple as possible in order to make
it easy to understand. I haven't measured the performance but in
practice this will only happen during the first render of the glyph
after a face change (i.e. during launch or when changing font size).
2024-12-29 02:27:52 +00:00
Zein Hajj-Ali
fa83140585 Set alpha component for fullscreen background colour 2024-12-28 21:07:43 -05:00
mitchellh
a671ca43f8 deps: Update iTerm2 color schemes 2024-12-29 01:00:43 +00:00
Mitchell Hashimoto
5293fc9c2f gtk: add option to not link against libX11 (#3748)
Possible fix for #3477. Needs testing.
2024-12-28 14:44:55 -08:00
Mitchell Hashimoto
531a225b1e Clarify how to read logs on macOS (#3808)
I removed the sentence `See the Mac App section for more information.`
because I cannot find the relevant information. It would be great to
have a separate log file and indicate its location in the document.

For the record, I just spent 15 minute searching for the location of a
log file in the document, which is painful because the website does not
have a search button. Eventually I gave up and looked into the source
file, and finally found the following comment (btw do we really need
`sudo`?):


ca7c954712/src/main_ghostty.zig (L129-L132)

Hopefully, this commit can make other new users' lives easier!
2024-12-28 14:41:54 -08:00
Mitchell Hashimoto
a55bea3491 ci: install nix 2024-12-28 14:38:29 -08:00
Qingyao Sun
351a7c03a5 Clarify how to read logs on macOS 2024-12-28 17:35:32 -05:00
Mitchell Hashimoto
f95aa32965 run gtk matrix on small instances 2024-12-28 14:32:13 -08:00
Mitchell Hashimoto
63dad2fb10 prettier 2024-12-28 14:31:35 -08:00
Mitchell Hashimoto
c4ff873726 ci: test gtk via a matrix 2024-12-28 14:29:36 -08:00
Mitchell Hashimoto
ca7c954712 docs: Add info about adjustments to adjustment config options (#3672)
Fix for misunderstanding in
https://github.com/ghostty-org/ghostty/issues/3400

Adds info about the MetricModifier config options that they can be used
with percentages, and links to the config option `adjust-cell-width`
that has more info
2024-12-28 12:58:30 -08:00
Jelle Besseling
b22417630b docs: Add info about adjustments to adjustment config options 2024-12-28 12:58:21 -08:00
Mitchell Hashimoto
54679ff30e docs: add guide for setting quick terminal toggle keybind (#3647)
Added documentation to guide users on configuring a keybind to toggle
the quick terminal. By default there is no keybind and there is no
reference in docs also on how to set a keybind.

Related:
https://github.com/ghostty-org/website/pull/193#issuecomment-2564164801
2024-12-28 12:57:31 -08:00
Ronit Gandhi
38643ec4fe docs: add guide for setting quick terminal toggle keybind 2024-12-28 12:57:17 -08:00
Mitchell Hashimoto
040ce0e7df docs: update guides on handling number identifier (#3664)
Related:
https://github.com/ghostty-org/website/pull/170#discussion_r1898746444
2024-12-28 12:55:24 -08:00
Hisam Fahri
b00a4275f0 Update Config.zig 2024-12-28 12:55:04 -08:00
Mitchell Hashimoto
3bccb4f05e docs: correct default value of gtk-single-instance (#3793)
`desktop` is the default value of `gtk-single-instance`; `detect` is not
a valid value.
2024-12-28 12:51:44 -08:00
Mitchell Hashimoto
2a4e5e1c8e Add MDItemKeywords on macOS (#3745)
Surfaces Ghostty when searching for "terminal" in Spotlight / Finder.
2024-12-28 12:40:28 -08:00
Matt Mirus
a8261ec9f6 chore: undo unintentional change
I think this must have snuck in due to my fork falling behind the
upstream.
2024-12-28 15:40:25 -05:00
Mitchell Hashimoto
1e937cf2f1 fix(gtk): fix segfault/bus error on ctrl+d (#3694)
Fixes https://github.com/ghostty-org/ghostty/issues/3135 as per the fix
described here
https://github.com/ghostty-org/ghostty/issues/3135#issuecomment-2564273739
2024-12-28 12:39:34 -08:00
Mitchell Hashimoto
5340aa6c96 Use source in the example for sourcing Zsh shell integration (#3680)
A few people were copying that snippet and were facing issues such as
"permission denied", and (after a subsequent `chmod`) "syntax error".

-----
From the [CONTRIBUTING.md]:

> Pull requests should be associated with a previously accepted issue.

I decided to ignore this for a single-word change, since creating a
discussion then waiting for it to be promoted to an issue seemed
pointless for such a minor change.

[CONTRIBUTING.md]:
https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md#pull-requests-implement-an-issue
2024-12-28 12:36:21 -08:00
Koray08
95ee6c1633 Replace 'w' with '\x15' 2024-12-28 22:35:44 +02:00
Matt Mirus
e8a2950324 Merge branch 'ghostty-org:main' into patch-1 2024-12-28 15:35:25 -05:00
Mitchell Hashimoto
2362a67f68 deinit buffer (#3736)
successor of #3262
2024-12-28 12:34:55 -08:00
Matt Mirus
e00233bebf docs: correct default value of gtk-single-instance
`desktop` is the default value of `gtk-single-instance`; `detect` is not a valid value.
2024-12-28 15:33:59 -05:00
Mitchell Hashimoto
00d4610fa5 fix probable typo in Config.zig (#3714)
Based on both context and interaction with ghostty, "now" was intended
to be "not" in the paragraph describing the behavior of
`focus-follows-mouse` is set to `true`.
2024-12-28 12:32:57 -08:00
Jeffrey C. Ollie
8ecb11a602 gtk: add option to not link against libX11 2024-12-28 13:40:17 -06:00
Finn Voorhees
8cb13bcfad Add MDItemKeywords 2024-12-28 11:21:11 -05:00
Maciej Bartczak
4ed8306b02 Add delay before updating the title 2024-12-28 17:18:15 +01:00
Felix Salcher
9632a2b956 deinit buffer 2024-12-28 16:51:16 +01:00
AP Darkly
02b34f44f6 fix probable typo in Config.zig
Based on both context and observed behavior, "now" was intended to be "not" in the paragraph describing the behavior of `focus-follows-mouse` is set to `true`.
2024-12-28 07:32:26 -05:00
Leigh Oliver
034f79cfa2 fix(gtk): fix segfault/bus error on ctrl+d 2024-12-28 10:55:13 +00:00
Kat
e8054c41f5 Use source in the example for sourcing Zsh shell integration 2024-12-28 09:01:41 +00:00
Koray08
0160f8a0d9 Add 'command delete' default to macOS 2024-12-28 10:41:52 +02:00
Mitchell Hashimoto
6cbd69da78 Update docs generators to include proper edit on github links (#3651)
Ref: https://github.com/ghostty-org/website/pull/197

The "Edit on Github" links currently on
[ghostty.org](https://ghostty.org/) link to the downstream MDX files.
This has lead to multiple folks editing the downstream MDX files.

Preview:

https://website-git-edit-on-github-link-override-ghostty.vercel.app/docs/config/reference#auto-update-channel
2024-12-27 21:05:27 -08:00
Mitchell Hashimoto
1fa2e699d1 Modify cmd+9 behaviour (#3587)
Fixes #3528 

**Note:**
Not sure if this is what OP requested regarding displaying user defined
shortcuts as hints, but we currently relabel all tabs, but only consider
labels for tabs from 1 to 9. Applying this simple patch should allow for
displaying shortcut hints for tabs beyond 9
<details><summary>Patch</summary>

```diff
diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift
index 7fd1802d..242e5711 100644
--- a/macos/Sources/Features/Terminal/TerminalController.swift
+++ b/macos/Sources/Features/Terminal/TerminalController.swift
@@ -155,13 +155,6 @@ class TerminalController: BaseTerminalController {
         tabListenForFrame = windows.count > 1
 
         for (tab, window) in zip(1..., windows) {
-            // We need to clear any windows beyond this because they have had
-            // a keyEquivalent set previously.
-            guard tab <= 9 else {
-                window.keyEquivalent = ""
-                continue
-            }
-
             let action = "goto_tab:\(tab)"
             if let equiv = ghostty.config.keyEquivalent(for: action) {
                 window.keyEquivalent = "\(equiv)"
```

</details>
2024-12-27 21:00:42 -08:00
Brandon Romano
16e4529f69 Update docs generators to include proper edit on github links
Ref: https://github.com/ghostty-org/website/pull/197
2024-12-27 20:59:50 -08:00
Rohit-Bevinahally
2555f09d88 modify cmd+9 behaviour 2024-12-27 20:14:28 -08:00
Mitchell Hashimoto
bee2188014 apprt/gtk: color popovers when window-theme=ghostty (#3569)
This looks better than the regular dark color. It also happens to match
what Ptyxis does. It does not support non-libadwaita builds.

Before:

![image](https://github.com/user-attachments/assets/8c400bdf-05b3-4629-925d-fd8ce9554ae7)

After:

![image](https://github.com/user-attachments/assets/d958eb6a-102d-4d91-970b-fcaca7f2386c)

It will look even better whenever we fix the separator colors 😄
2024-12-27 13:39:41 -08:00
Tristan Partin
bf46ab8d2d apprt/gtk: color popovers when window-theme=ghostty
This looks better than the regular dark color. It also happens to match
what Ptyxis does. It does not support non-libadwaita builds.

Signed-off-by: Tristan Partin <tristan@partin.io>
2024-12-27 14:32:02 -06:00
Mitchell Hashimoto
8111f5b995 Fix DESTDIR handling for terminfo installation (#3426)
## Description:

Fix `DESTDIR` handling when installing terminfo database files by using
`install_path` instead of `install_prefix`. This ensures files are
correctly installed under `$DESTDIR/$prefix` during packaging.

## Changes:

- Replace `b.install_prefix` with `b.install_path` for terminfo database
installation paths

- This change properly respects the `DESTDIR` environment variable
during installation

## Testing:

I've verified this fix by:

1. Setting `DESTDIR=/tmp/ghostty`

2. Building with: 
```bash
DESTDIR=/tmp/ghostty \
zig build \
  --prefix /usr \
  --system /tmp/offline-cache/p \
  -Doptimize=ReleaseFast \
  -Dcpu=baseline
```

3. Confirming files are correctly installed to:

```
/tmp/ghostty/usr/share/terminfo/ghostty.terminfo
/tmp/ghostty/usr/share/terminfo/ghostty.termcap
```

The files are now properly installed under `$DESTDIR/$prefix` path
structure as expected.

cc @BratishkaErik - Since you suggested this fix in #3152, would you
mind reviewing this implementation?

Fixes #3152
2024-12-27 12:09:37 -08:00
Mitchell Hashimoto
590c2929e7 Fix typo in config docs (#3564)
This was originally fixed in ghostty-org/website#152, but was
accidentally reverted in
[ghostty-org/website#99795d7882a5ee47437454c7c106c2874e0406dc](99795d7882).

P.S., you might want an PR template to discourage folks from making
unsolicited PRs like this :)
P.P.S., you can write issue templates as yaml files to stop folks from
ignoring them (required fields)
2024-12-27 12:08:44 -08:00
Eli
27a5a50aa0 fix: typo
Original fix: ghostty-org/website#152
Reverted: ghostty-org/website#99795d7882a5ee47437454c7c106c2874e0406dc.
2024-12-27 14:02:25 -06:00
Mitchell Hashimoto
c8dca6c9a8 Fix building with -Dflatpak=true (#3440)
While running a Ghostty instance built with this option currently
crashes under Flatpak, at least it ensures we're able to build it again.
2024-12-27 11:58:39 -08:00
Mitchell Hashimoto
e2aee7f75b Handle an empty path to mean no pwd (#3399)
Fixes #3398
2024-12-27 11:55:36 -08:00
Mitchell Hashimoto
f0b77e8972 Fix notification handling from publishing while View is updating (#3177)
I was originally looking into this issue:
https://github.com/ghostty-org/ghostty/issues/3109

When running the logic and triggering a config reload, Xcode warns us
about updating publishable properties from within the notification
callback functions:
<img width="924" alt="Screenshot 2024-12-26 at 5 46 19 PM"
src="https://github.com/user-attachments/assets/38000a09-ffad-4dda-9e2d-a37e5283ff89"
/>

I believe this is because `SurfaceView` is being used as both a bridged
NSView (inside `Surface`) and also an `ObservableObject`, so it's
possible for the notification callback to happen while a SwiftUI render
loop is occurring. The notification delivery happens on whatever thread
posted the message. The better solution long-term is likely to separate
the `ObservableObject` logic into its own class to avoid mixing with the
View logic.

The solution here is to simply move the publishable mutation out of the
current loop via `DispatchQueue.main.async`. I confirmed the warning
goes away with this, and I didn't notice any odd behavior while
reloading config changes.
2024-12-27 11:54:50 -08:00
Mitchell Hashimoto
571d9b015d Fix zsh shell integration docs (#3332)
- There is no need to quote variables in zsh.
- The shell integration file is not executable; we should `source` it
instead.
2024-12-27 11:54:07 -08:00
Mitchell Hashimoto
66dbc48e6b fix elvish integration: remove erroneous call to "type" (#3386)
An exception is raised from the elvish integration module when
`TERMINFO` is set:

```
Exception: exec: "type": executable file not found in $PATH
  ghostty-integration.elv:120:60-71:   if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (eq file (type -t sudo))) {
  ghostty-integration.elv:38:1-123:1:
```

`type` is a builtin in bash and [in
fish](https://fishshell.com/docs/current/cmds/type.html) but it does not
exist in elvish. I suspect it is here as an accidental copy/paste from
the ghostty [fish
integration](https://github.com/ghostty-org/ghostty/blob/main/src/shell-integration/fish/vendor_conf.d/ghostty-shell-integration.fish#L71).

Maybe we can use the elvish
[`has-external`](https://elv.sh/ref/builtin.html#has-external) function
instead (or we could remove that check altogether).

Edit: I think this is in a merge-able state (and it would fix that
error!), but I have just converted this PR to a draft since I can see
other problems with the script. I'll give it a more thorough treatment
later and amend this PR (or also happy to merge this and do other fixes
in separate PRs).
2024-12-27 11:53:54 -08:00
Mitchell Hashimoto
1622263519 cli(list_themes): ignore .DS_Store (#3425)
macOS will create `.DS_Store` files in any directory it opens in Finder.
The `+list_themes` command would then list this file as a theme, and
attempt to preview it. `.DS_Store` is a binary file, and is silently
failing in the theme preview...I am on Linux and when I put a small
binary file in my user themes directory, I get a segfault. There is
something about the specific contents in `.DS_Store` that does not cause
this segfault, but lets us silently fail. We should investigate this
further - the issue is in `Config.loadFile` I believe.

In either case, we need to ignore `.DS_Store` so that it is not listed
as a theme.
2024-12-27 11:53:04 -08:00
Mitchell Hashimoto
93b6bfdefe Update doc inform toggle_quick_terminal macOS only (#3464)
From the 'CONTRIBUTING.md':
"Pull requests should be associated with a previously accepted issue. If
you open a pull request for something that wasn't previously discussed,
it may be closed or remain stale for an indefinite period of time. I'm
not saying it will never be accepted, but the odds are stacked against
you."

I understand this, and I make a PR without an issue because I feel like
this is actually binary.

On discord I've been informed the quick terminal is macOS only, and in
the documentation I don't think this is expressed, please correct me if
wrong and close this.

If it's correct and the documentation should contain it, then here's my
PR adding that information on the bottom of the section.
If the location of the added information does not fit the style
guidelines I can change it.
2024-12-27 11:52:32 -08:00
xaviduds
96753aa7e0 Update doc inform toggle_quick_terminal macOS only 2024-12-27 11:52:20 -08:00
Mitchell Hashimoto
09470ede55 gtk: equalize on double clicking the split handle (#3557) 2024-12-27 11:50:15 -08:00
Mitchell Hashimoto
6139cb00cf coretext: exclude bitmap fonts from discovery (#3550)
We do not currently support bitmap fonts in a real capacity, and they
often are missing some tables which we currently rely on for metrics,
and we don't handle the metrics calculations failing gracefully right
now.

This needs to be fixed for the fontconfig discovery mechanism as well,
so this does NOT close issue #2168 (it fixes the problem on macOS but
not linux).

This greatly alleviates the effect of #2991 since most cases I've seen
that be a problem have been the accidental loading of a bitmap font; but
the underlying issue still exists.
2024-12-27 11:48:44 -08:00
Mitchell Hashimoto
7e4a69e7df fix prettier 2024-12-27 11:45:57 -08:00
LuK1337
66ed72f486 gtk: equalize on double clicking the split handle 2024-12-27 20:45:13 +01:00
Mitchell Hashimoto
6f2a7d3c36 github: make things more clear 2024-12-27 11:34:09 -08:00
Mitchell Hashimoto
4b3c1b031e github: preapproved template needs to be md 2024-12-27 11:29:49 -08:00
Mitchell Hashimoto
83987968ac github: preapproved issue template 2024-12-27 11:22:25 -08:00
Mitchell Hashimoto
2998775152 github: issue template 2024-12-27 11:16:48 -08:00
Qwerasd
322f166ca5 coretext: exclude bitmap fonts from discovery
We do not currently support bitmap fonts in a real capacity, and they
often are missing some tables which we currently rely on for metrics,
and we don't handle the metrics calculations failing gracefully right
now.
2024-12-27 14:06:46 -05:00
Anthony
8335a31e45 Use github yaml substitution for yaml element 2024-12-28 04:04:55 +11:00
Anthony
e07f813a50 Merge branch 'main' of github.com:ghostty-org/ghostty 2024-12-28 03:33:45 +11:00
Anthony
b9128aded5 CI: Update release-tag.yml to include the version in the source archive name and prefix 2024-12-28 03:33:12 +11:00
Yorick Peterse
50f7632d81 Fix building with -Dflatpak=true
While running a Ghostty instance built with this option currently
crashes under Flatpak, at least it ensures we're able to build it again.
2024-12-27 16:27:19 +01:00
Bryan Lee
2114e0a613 Fix DESTDIR handling for terminfo installation
Use `install_path` instead of `install_prefix` when installing terminfo
database files to properly respect the `DESTDIR` environment variable.
This ensures files are correctly installed under `$DESTDIR/$prefix`
when packaging.

Fixes #3152
2024-12-27 23:12:42 +08:00
Tim Culverhouse
c2792a1811 cli(list_themes): ignore .DS_Store
macOS will create `.DS_Store` files in any directory it opens in Finder.
The `+list_themes` command would then list this file as a theme, and
attempt to preview it. `.DS_Store` is a binary file, and is silently
failing in the theme preview...I am on Linux and when I put a small
binary file in my user themes directory, I get a segfault. There is
something about the specific contents in `.DS_Store` that does not cause
this segfault, but lets us silently fail. We should investigate this
further - the issue is in `Config.loadFile` I believe.

In either case, we need to ignore `.DS_Store` so that it is not listed
as a theme.

Fixes: #3378
2024-12-27 09:00:08 -06:00
Jon Parise
2c3847c9af macos: disable Sparkle checks in Debug and Release
This approach uses Xcode's Info.plist preprocessing to conditionally set
`SUEnableAutomaticChecks=false` for the Debug and Release build schemes.
It is unset for the ReleaseLocal scheme.

When this Info.plist key is explicitly set to false (as it is for these
build schemes), we disable auto-updates at runtime. Otherwise, we apply
the behavior defined by our "auto-update" configuration.
2024-12-27 08:16:18 -05:00
David Leadbeater
9db02fd152 Handle an empty path to mean no pwd
Fixes #3398
2024-12-28 00:09:05 +11:00
Jon Parise
f7a461a85f macos: disable auto-updates for local (source) builds
The auto-update prompt isn't useful for local (source) builds so disable
both update checks and automatic downloads.

There are multiple ways we could check if we've been built for source,
but the easiest and least intrusive approach is to check the value of
the 'GhosttyCommit' Info.plist key. Because it is only set as part of
the release build process, an empty key implies that we've been build
from source.
2024-12-27 07:36:14 -05:00
Ankur Kothari
c320635c29 fix use of bash builtin in elvish integration
`type` is a bash builtin and should not be used in elvish.

```
Exception: exec: "type": executable file not found in $PATH
  ghostty-integration.elv:120:60-71:   if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (eq file (type -t sudo))) {
  ghostty-integration.elv:38:1-123:1:
```

We can use the elvish `has-external` function instead.
2024-12-27 23:10:06 +11:00
XiaoYan Li
a295c5e884 Fix zsh shell integration docs
- There is no need to quote variables in zsh.
- The shell integration file is not executable; we should `source` it instead.
2024-12-27 15:56:14 +08:00
Anund
600eea08cd testing: point Command.zig at ~more universal external binaries
The `Command.zig` tests reach outside the local source tree and look for
files on the host os machine. This introduces some portability issues
for the tests.

The nix build sandbox doesn't include `/usr/bin/env` making it error out
when `zig build test` runs `Command.zig` tests as part of a `nix build`.
Current ci and local development relies on `nix develop` sharing a host os
file system that includes `/usr/bin/env`.

Turns out `/tmp` and `/bin/sh` are available in the build sandbox in
nix so we swap these in to enable nixpkg builds to include testing
ghostty as part of any update cycle.
2024-12-27 17:07:24 +11:00
Mitchell Hashimoto
a8e5eef11c Fix certain paths in the docs containing spaces when the shouldn't (#3276)
This addresses issue #3275
2024-12-26 19:59:38 -08:00
Joris Guex
73367a55f6 Fix docs formatting 2024-12-27 11:45:42 +08:00
Mitchell Hashimoto
b5f70b834b updated logic for grouping actions (#3262)
Update logic for generating `webgen_actions`. 

Before the change, the following grouping was produced:
```md
## `copy_to_clipboard`
Copy and paste.


## `paste_from_clipboard`
## `paste_from_selection`
## `increase_font_size`
Increase/decrease the font size by a certain amount.
```

After the change, the following grouping is being produced
```md
## `copy_to_clipboard`
## `paste_from_clipboard`
## `paste_from_selection`
Copy and paste.

## `increase_font_size`
## `decrease_font_size`
Increase/decrease the font size by a certain amount.
```

Please note that this is my first time ever writing zig, so forgive me
violating zig best practices and feel free to make suggestions

Successor of [this](https://github.com/ghostty-org/website/pull/147) pr
in the website repo
2024-12-26 19:41:26 -08:00
Mitchell Hashimoto
869987277c Update help comment for backslash (#3173)
Such a little commit, but I banged my head on it for a few minutes!
2024-12-26 19:33:08 -08:00
Mitchell Hashimoto
aef90ffff1 Use doc comments for focus-follows-mouse (#3259)
Fixes the description of `focus-follows-mouse` not showing up in the
autogenerated docs and the website
2024-12-26 19:31:30 -08:00
Mitchell Hashimoto
729b7ba7ed Re-add nix-compat flake input (#3217)
Its entry in flake.lock is required for shell.nix to operate as it's
been written. Hash values are restored to where they last existed.

Fixes #3216.
2024-12-26 19:27:58 -08:00
Felix Salcher
5411c001c8 added doc comment 2024-12-27 04:23:17 +01:00
Felix Salcher
ebfa606c67 updated logic for grouping actions 2024-12-27 04:14:17 +01:00
Anmol Wadhwani
7aced21a8e Use doc comments for focus-follows-mouse 2024-12-27 08:29:35 +05:30
Gregory Anders
eef9664ef8 renderer: track if fg/bg/cursor color is explicitly set by an OSC
The renderer must track if the foreground, background, and cursor colors
are explicitly set by an OSC so that changes are not overridden when the
config file is reloaded.
2024-12-26 20:18:45 -06:00
Chip Bilbrey
cb5fbc1041 Re-add nix-compat flake input
Its entry in flake.lock is required for shell.nix to operate as it's
been written. Hash values are restored to where they last existed.
2024-12-26 16:21:18 -08:00
Anund
b2cb80dfbb testing: move cleanup of execveZ into the test code 2024-12-27 11:08:45 +11:00
Anund
184db2654c testing: handle execveZ failing during test execution
Duplicating a test process via fork does unexepected things.
zig build test will hang
A test binary created via -Demit-test-exe will run 2 copies of the test suite
2024-12-27 11:08:44 +11:00
Mitchell Hashimoto
35b9ceee21 up the version to 1.0.1 everywhere for dev 2024-12-26 15:21:50 -08:00
Lucas Crownover
6217dbebcf Update help comment for backslash 2024-12-26 14:58:43 -08:00
Trevor Elkins
c1f61b4344 one more 2024-12-26 17:48:14 -05:00
Trevor Elkins
5867fe425f Move notification handling outside of the current render loop 2024-12-26 17:44:06 -05:00
78 changed files with 1626 additions and 649 deletions

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Features, Bug Reports, Questions
url: https://github.com/ghostty-org/ghostty/discussions/new/choose
about: Our preferred starting point if you have any questions or suggestions about configuration, features or behavior.

9
.github/ISSUE_TEMPLATE/preapproved.md vendored Normal file
View File

@@ -0,0 +1,9 @@
---
name: Pre-Discussed and Approved Topics
about: |-
Only for topics already discussed and approved in the GitHub Discussions section.
---
**DO NOT OPEN A NEW ISSUE. PLEASE USE THE DISCUSSIONS SECTION.**
**I DIDN'T READ THE ABOVE LINE. PLEASE CLOSE THIS ISSUE.**

32
.github/workflows/milestone.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
# Description:
# - Add milestone to a merged PR automatically
# - Add milestone to a closed issue that has a merged PR fix (if any)
name: Milestone Action
on:
issues:
types: [closed]
pull_request_target:
types: [closed]
jobs:
update-milestone:
runs-on: namespace-profile-ghostty-sm
name: Milestone Update
steps:
- name: Set Milestone for PR
uses: hustcer/milestone-action@v2
if: github.event.pull_request.merged == true
with:
action: bind-pr # `bind-pr` is the default action
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Bind milestone to closed issue that has a merged PR fix
- name: Set Milestone for Issue
uses: hustcer/milestone-action@v2
if: github.event.issue.state == 'closed'
with:
action: bind-issue
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -110,6 +110,7 @@ jobs:
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
@@ -261,6 +262,7 @@ jobs:
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:

View File

@@ -74,6 +74,8 @@ jobs:
source-tarball:
runs-on: namespace-profile-ghostty-md
needs: [setup]
env:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
steps:
- uses: actions/checkout@v4
@@ -87,19 +89,24 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Create Tarball
run: git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
run: |
git archive --format=tgz --prefix="ghostty-${GHOSTTY_VERSION}/" -o "ghostty-${GHOSTTY_VERSION}.tar.gz" HEAD
git archive --format=tgz --prefix=ghostty-source/ -o ghostty-source.tar.gz HEAD
- name: Sign Tarball
run: |
echo -n "${{ secrets.MINISIGN_KEY }}" > minisign.key
echo -n "${{ secrets.MINISIGN_PASSWORD }}" > minisign.password
nix develop -c minisign -S -m ghostty-source.tar.gz -s minisign.key < minisign.password
nix develop -c minisign -S -m "ghostty-${GHOSTTY_VERSION}.tar.gz" -s minisign.key < minisign.password
nix develop -c minisign -S -m "ghostty-source.tar.gz" -s minisign.key < minisign.password
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: source-tarball
path: |-
ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz
ghostty-${{ env.GHOSTTY_VERSION }}.tar.gz.minisig
ghostty-source.tar.gz
ghostty-source.tar.gz.minisig
@@ -165,6 +172,7 @@ jobs:
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
@@ -352,6 +360,8 @@ jobs:
run: |
mkdir blob
mkdir -p blob/${GHOSTTY_VERSION}
mv "ghostty-${GHOSTTY_VERSION}.tar.gz" blob/${GHOSTTY_VERSION}/ghostty-${GHOSTTY_VERSION}.tar.gz
mv "ghostty-${GHOSTTY_VERSION}.tar.gz.minisig" blob/${GHOSTTY_VERSION}/ghostty-${GHOSTTY_VERSION}.tar.gz.minisig
mv ghostty-source.tar.gz blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz
mv ghostty-source.tar.gz.minisig blob/${GHOSTTY_VERSION}/ghostty-source.tar.gz.minisig
mv ghostty-macos-universal.zip blob/${GHOSTTY_VERSION}/ghostty-macos-universal.zip

View File

@@ -205,6 +205,7 @@ jobs:
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
@@ -419,6 +420,7 @@ jobs:
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
@@ -593,6 +595,7 @@ jobs:
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:

View File

@@ -329,9 +329,6 @@ jobs:
- name: Test GTK Build
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=true -Demit-docs
- name: Test GTK Build (No Libadwaita)
run: nix develop -c zig build -Dapp-runtime=gtk -Dgtk-adwaita=false -Demit-docs
- name: Test GLFW Build
run: nix develop -c zig build -Dapp-runtime=glfw
@@ -339,6 +336,46 @@ jobs:
- name: Test System Build
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
test-gtk:
strategy:
fail-fast: false
matrix:
adwaita: ["true", "false"]
x11: ["true", "false"]
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }}
runs-on: namespace-profile-ghostty-sm
needs: test
env:
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@v1.2.0
with:
path: |
/nix
/zig
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v30
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v15
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Test GTK Build
run: |
nix develop -c \
zig build \
-Dapp-runtime=gtk \
-Dgtk-adwaita=${{ matrix.adwaita }} \
-Dgtk-x11=${{ matrix.x11 }}
test-macos:
runs-on: namespace-profile-ghostty-macos
needs: test

View File

@@ -19,10 +19,17 @@ at `release.files.ghostty.org` in the following URL format where
`VERSION` is the version number with no prefix such as `1.0.0`:
```
https://release.files.ghostty.org/VERSION/ghostty-source.tar.gz
https://release.files.ghostty.org/VERSION/ghostty-source.tar.gz.minisig
https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz
https://release.files.ghostty.org/VERSION/ghostty-VERSION.tar.gz.minisig
```
> [!NOTE]
>
> **Version 1.0.0 the filename is `ghostty-source.tar.gz`.** Future
> versions will use the `ghostty-VERSION.tar.gz` format since it is more
> typical for source tarballs. But for version 1.0.0, the filename is
> `ghostty-source.tar.gz`.
Signature files are signed with
[minisign](https://jedisct1.github.io/minisign/)
using the following public key:

View File

@@ -43,7 +43,7 @@ comptime {
}
/// The version of the next release.
const app_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 0 };
const app_version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 1 };
pub fn build(b: *std.Build) !void {
const optimize = b.standardOptimizeOption(.{});
@@ -105,6 +105,53 @@ pub fn build(b: *std.Build) !void {
"Enables the use of Adwaita when using the GTK rendering backend.",
) orelse true;
config.x11 = b.option(
bool,
"gtk-x11",
"Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse x11: {
if (target.result.os.tag != .linux) break :x11 false;
var pkgconfig = std.process.Child.init(&.{ "pkg-config", "--variable=targets", "gtk4" }, b.allocator);
pkgconfig.stdout_behavior = .Pipe;
pkgconfig.stderr_behavior = .Pipe;
try pkgconfig.spawn();
const output_max_size = 50 * 1024;
var stdout = std.ArrayList(u8).init(b.allocator);
var stderr = std.ArrayList(u8).init(b.allocator);
defer {
stdout.deinit();
stderr.deinit();
}
try pkgconfig.collectOutput(&stdout, &stderr, output_max_size);
const term = try pkgconfig.wait();
if (stderr.items.len > 0) {
std.log.warn("pkg-config had errors:\n{s}", .{stderr.items});
}
switch (term) {
.Exited => |code| {
if (code == 0) {
if (std.mem.indexOf(u8, stdout.items, "x11")) |_| break :x11 true;
break :x11 false;
}
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
return error.Unexpected;
},
inline else => |code| {
std.log.warn("pkg-config: {s} with code {d}", .{ @tagName(term), code });
return error.Unexpected;
},
}
};
const pie = b.option(
bool,
"pie",
@@ -481,19 +528,25 @@ pub fn build(b: *std.Build) !void {
run_step.step.dependOn(&src_install.step);
{
// Use cp -R instead of Step.InstallDir because we need to preserve
// symlinks in the terminfo database. Zig's InstallDir step doesn't
// handle symlinks correctly yet.
const copy_step = RunStep.create(b, "copy terminfo db");
copy_step.addArgs(&.{ "cp", "-R" });
copy_step.addFileArg(path);
copy_step.addArg(b.fmt("{s}/share", .{b.install_prefix}));
copy_step.addArg(b.fmt("{s}/share", .{b.install_path}));
b.getInstallStep().dependOn(&copy_step.step);
}
if (target.result.os.tag == .macos and exe_ != null) {
// Use cp -R instead of Step.InstallDir because we need to preserve
// symlinks in the terminfo database. Zig's InstallDir step doesn't
// handle symlinks correctly yet.
const copy_step = RunStep.create(b, "copy terminfo db");
copy_step.addArgs(&.{ "cp", "-R" });
copy_step.addFileArg(path);
copy_step.addArg(
b.fmt("{s}/Ghostty.app/Contents/Resources", .{b.install_prefix}),
b.fmt("{s}/Ghostty.app/Contents/Resources", .{b.install_path}),
);
b.getInstallStep().dependOn(&copy_step.step);
}
@@ -604,11 +657,7 @@ pub fn build(b: *std.Build) !void {
// https://developer.gnome.org/documentation/guidelines/maintainer/integrating.html
// Desktop file so that we have an icon and other metadata
if (config.flatpak) {
b.installFile("dist/linux/app-flatpak.desktop", "share/applications/com.mitchellh.ghostty.desktop");
} else {
b.installFile("dist/linux/app.desktop", "share/applications/com.mitchellh.ghostty.desktop");
}
b.installFile("dist/linux/app.desktop", "share/applications/com.mitchellh.ghostty.desktop");
// Right click menu action for Plasma desktop
b.installFile("dist/linux/ghostty_dolphin.desktop", "share/kio/servicemenus/com.mitchellh.ghostty.desktop");
@@ -620,6 +669,7 @@ pub fn build(b: *std.Build) !void {
b.installFile("images/icons/icon_128.png", "share/icons/hicolor/128x128/apps/com.mitchellh.ghostty.png");
b.installFile("images/icons/icon_256.png", "share/icons/hicolor/256x256/apps/com.mitchellh.ghostty.png");
b.installFile("images/icons/icon_512.png", "share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png");
b.installFile("images/icons/icon_1024.png", "share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png");
b.installFile("images/icons/icon_16@2x.png", "share/icons/hicolor/16x16@2/apps/com.mitchellh.ghostty.png");
b.installFile("images/icons/icon_32@2x.png", "share/icons/hicolor/32x32@2/apps/com.mitchellh.ghostty.png");
b.installFile("images/icons/icon_128@2x.png", "share/icons/hicolor/128x128@2/apps/com.mitchellh.ghostty.png");
@@ -1378,6 +1428,7 @@ fn addDeps(
.gtk => {
step.linkSystemLibrary2("gtk4", dynamic_link_opts);
if (config.adwaita) step.linkSystemLibrary2("adwaita-1", dynamic_link_opts);
if (config.x11) step.linkSystemLibrary2("X11", dynamic_link_opts);
{
const gresource = @import("src/apprt/gtk/gresource.zig");

View File

@@ -1,6 +1,6 @@
.{
.name = "ghostty",
.version = "1.0.0",
.version = "1.0.1",
.paths = .{""},
.dependencies = .{
// Zig libs
@@ -49,8 +49,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/d6c42066b3045292e0b1154ad84ff22d6863ebf7.tar.gz",
.hash = "12204358b2848ffd993d3425055bff0a5ba9b1b60bead763a6dea0517965d7290a6c",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e030599a6a6e19fcd1ea047c7714021170129d56.tar.gz",
.hash = "1220cc25b537556a42b0948437c791214c229efb78b551c80b1e9b18d70bf0498620",
},
.vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",

View File

@@ -10,6 +10,11 @@ StartupNotify=true
Terminal=false
Actions=new-window;
X-GNOME-UsesNotifications=true
X-TerminalArgExec=-e
X-TerminalArgTitle=--title=
X-TerminalArgAppId=--class=
X-TerminalArgDir=--working-directory=
X-TerminalArgHold=--wait-after-command
[Desktop Action new-window]
Name=New Window

17
flake.lock generated
View File

@@ -1,5 +1,21 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
@@ -52,6 +68,7 @@
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs-stable": "nixpkgs-stable",
"nixpkgs-unstable": "nixpkgs-unstable",
"zig": "zig"

View File

@@ -9,6 +9,12 @@
# system glibc that the user is building for.
nixpkgs-stable.url = "github:nixos/nixpkgs/release-24.11";
# Used for shell.nix
flake-compat = {
url = "github:edolstra/flake-compat";
flake = false;
};
zig = {
url = "github:mitchellh/zig-overlay";
inputs = {
@@ -52,7 +58,12 @@
formatter.${system} = pkgs-stable.alejandra;
# Our supported systems are the same supported systems as the Zig binaries.
}) (builtins.attrNames zig.packages));
}) (builtins.attrNames zig.packages))
// {
overlays.default = final: prev: {
ghostty = self.packages.${prev.system}.default;
};
};
nixConfig = {
extra-substituters = ["https://ghostty.cachix.org"];

View File

@@ -51,6 +51,8 @@
<key>GHOSTTY_MAC_APP</key>
<string>1</string>
</dict>
<key>MDItemKeywords</key>
<string>Terminal</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSServices</key>
@@ -94,6 +96,8 @@
</array>
</dict>
</array>
<key>SUEnableAutomaticChecks</key>
<false/>
<key>SUPublicEDKey</key>
<string>wsNcGf5hirwtdXMVnYoxRIX/SqZQLMOsYlD3q3imeok=</string>
</dict>

View File

@@ -10,6 +10,7 @@
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
@@ -107,6 +108,7 @@
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
@@ -403,6 +405,7 @@
A5985CE52C33060F00C57AD3 /* man */,
A5A1F8842A489D6800D1E8BC /* terminfo */,
FC5218F92D10FFC7004C93E0 /* zsh */,
9351BE8E2D22937F003B3499 /* nvim */,
);
name = Resources;
sourceTree = "<group>";
@@ -579,6 +582,7 @@
A5985CE62C33060F00C57AD3 /* man in Resources */,
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */,
552964E62B34A9B400030505 /* vim in Resources */,
9351BE8E3D22937F003B3499 /* nvim in Resources */,
A5CBD05C2CA0C5C70017A1AE /* QuickTerminal.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -484,11 +484,18 @@ class AppDelegate: NSObject,
default: UserDefaults.standard.removeObject(forKey: "NSQuitAlwaysKeepsWindows")
}
// Sync our auto-update settings
updaterController.updater.automaticallyChecksForUpdates =
config.autoUpdate == .check || config.autoUpdate == .download
updaterController.updater.automaticallyDownloadsUpdates =
config.autoUpdate == .download
// Sync our auto-update settings. If SUEnableAutomaticChecks (in our Info.plist) is
// explicitly false (NO), auto-updates are disabled. Otherwise, we use the behavior
// defined by our "auto-update" configuration.
if Bundle.main.infoDictionary?["SUEnableAutomaticChecks"] as? Bool != false {
updaterController.updater.automaticallyChecksForUpdates =
config.autoUpdate == .check || config.autoUpdate == .download
updaterController.updater.automaticallyDownloadsUpdates =
config.autoUpdate == .download
} else {
updaterController.updater.automaticallyChecksForUpdates = false
updaterController.updater.automaticallyDownloadsUpdates = false
}
// Config could change keybindings, so update everything that depends on that
syncMenuShortcuts(config)
@@ -662,7 +669,7 @@ class AppDelegate: NSObject,
}
@IBAction func showHelp(_ sender: Any) {
guard let url = URL(string: "https://github.com/ghostty-org/ghostty") else { return }
guard let url = URL(string: "https://ghostty.org/docs") else { return }
NSWorkspace.shared.open(url)
}

View File

@@ -69,7 +69,7 @@ class QuickTerminalController: BaseTerminalController {
window.isRestorable = false
// Setup our configured appearance that we support.
syncAppearance(ghostty.config)
syncAppearance()
// Setup our initial size based on our configured position
position.setLoaded(window)
@@ -214,6 +214,10 @@ class QuickTerminalController: BaseTerminalController {
// If we canceled our animation in we do nothing
guard self.visible else { return }
// Now that the window is visible, sync our appearance. This function
// requires the window is visible.
self.syncAppearance()
// Once our animation is done, we must grab focus since we can't grab
// focus of a non-visible window.
self.makeWindowKey(window)
@@ -304,24 +308,18 @@ class QuickTerminalController: BaseTerminalController {
})
}
private func syncAppearance(_ config: Ghostty.Config) {
private func syncAppearance() {
guard let window else { return }
// If our window is not visible, then delay this. This is possible specifically
// during state restoration but probably in other scenarios as well. To delay,
// we just loop directly on the dispatch queue. We have to delay because some
// APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance(config) }
return
}
// If our window is not visible, then no need to sync the appearance yet.
// Some APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else { return }
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (config.windowColorspace) {
switch (self.derivedConfig.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
@@ -331,7 +329,7 @@ class QuickTerminalController: BaseTerminalController {
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (config.backgroundOpacity < 1) {
if (self.derivedConfig.backgroundOpacity < 1) {
window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that
@@ -391,24 +389,30 @@ class QuickTerminalController: BaseTerminalController {
// Update our derived config
self.derivedConfig = DerivedConfig(config)
syncAppearance(config)
syncAppearance()
}
private struct DerivedConfig {
let quickTerminalScreen: QuickTerminalScreen
let quickTerminalAnimationDuration: Double
let quickTerminalAutoHide: Bool
let windowColorspace: String
let backgroundOpacity: Double
init() {
self.quickTerminalScreen = .main
self.quickTerminalAnimationDuration = 0.2
self.quickTerminalAutoHide = true
self.windowColorspace = ""
self.backgroundOpacity = 1.0
}
init(_ config: Ghostty.Config) {
self.quickTerminalScreen = config.quickTerminalScreen
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
self.quickTerminalAutoHide = config.quickTerminalAutoHide
self.windowColorspace = config.windowColorspace
self.backgroundOpacity = config.backgroundOpacity
}
}
}

View File

@@ -45,6 +45,11 @@ class BaseTerminalController: NSWindowController,
didSet { surfaceTreeDidChange(from: oldValue, to: surfaceTree) }
}
/// Whether the terminal surface should focus when the mouse is over it.
var focusFollowsMouse: Bool {
self.derivedConfig.focusFollowsMouse
}
/// Non-nil when an alert is active so we don't overlap multiple.
private var alert: NSAlert? = nil
@@ -106,8 +111,8 @@ class BaseTerminalController: NSWindowController,
// Listen for local events that we need to know of outside of
// single surface handlers.
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
matching: [.flagsChanged],
handler: localEventHandler)
matching: [.flagsChanged]
) { [weak self] event in self?.localEventHandler(event) }
}
deinit {
@@ -155,7 +160,7 @@ class BaseTerminalController: NSWindowController,
}
// MARK: Notifications
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
// If we have a window that is visible and it is outside the bounds of the
// screen then we clamp it back to within the screen.
@@ -262,7 +267,6 @@ class BaseTerminalController: NSWindowController,
// Set the main window title
window.title = to
}
func pwdDidChange(to: URL?) {
@@ -309,11 +313,11 @@ class BaseTerminalController: NSWindowController,
// We consider our mode changed if the types change (obvious) but
// also if its nil (not obvious) because nil means that the style has
// likely changed but we don't support it.
if newStyle == nil || type(of: newStyle) != type(of: oldStyle) {
if newStyle == nil || type(of: newStyle!) != type(of: oldStyle) {
// Our mode changed. Exit fullscreen (since we're toggling anyways)
// and then unset the style so that we replace it next time.
// and then set the new style for future use
oldStyle.exit()
self.fullscreenStyle = nil
self.fullscreenStyle = newStyle
// We're done
return
@@ -604,15 +608,18 @@ class BaseTerminalController: NSWindowController,
private struct DerivedConfig {
let macosTitlebarProxyIcon: Ghostty.MacOSTitlebarProxyIcon
let windowStepResize: Bool
let focusFollowsMouse: Bool
init() {
self.macosTitlebarProxyIcon = .visible
self.windowStepResize = false
self.focusFollowsMouse = false
}
init(_ config: Ghostty.Config) {
self.macosTitlebarProxyIcon = config.macosTitlebarProxyIcon
self.windowStepResize = config.windowStepResize
self.focusFollowsMouse = config.focusFollowsMouse
}
}
}

View File

@@ -117,9 +117,6 @@ class TerminalController: BaseTerminalController {
// Update our derived config
self.derivedConfig = DerivedConfig(config)
guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = config.focusFollowsMouse
// If we have no surfaces in our window (is that possible?) then we update
// our window appearance based on the root config. If we have surfaces, we
// don't call this because the TODO
@@ -247,7 +244,7 @@ class TerminalController: BaseTerminalController {
let backgroundColor: OSColor
if let surfaceTree {
if let focusedSurface, surfaceTree.doesBorderTop(view: focusedSurface) {
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor)
backgroundColor = OSColor(focusedSurface.backgroundColor ?? surfaceConfig.backgroundColor).withAlphaComponent(0.0)
} else {
// We don't have a focused surface or our surface doesn't border the
// top. We choose to match the color of the top-left most surface.
@@ -422,8 +419,6 @@ class TerminalController: BaseTerminalController {
}
}
window.focusFollowsMouse = config.focusFollowsMouse
// Apply any additional appearance-related properties to the new window. We
// apply this based on the root config but change it later based on surface
// config (see focused surface change callback).

View File

@@ -69,7 +69,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
// The pwd of the focused surface as a URL
private var pwdURL: URL? {
guard let surfacePwd else { return nil }
guard let surfacePwd, surfacePwd != "" else { return nil }
return URL(fileURLWithPath: surfacePwd)
}

View File

@@ -414,8 +414,6 @@ class TerminalWindow: NSWindow {
}
}
var focusFollowsMouse: Bool = false
// Find the NSTextField responsible for displaying the titlebar's title.
private var titlebarTextField: NSTextField? {
guard let titlebarView = titlebarContainer?.subviews

View File

@@ -361,17 +361,23 @@ extension Ghostty {
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
guard let healthAny = notification.userInfo?["health"] else { return }
guard let health = healthAny as? ghostty_action_renderer_health_e else { return }
healthy = health == GHOSTTY_RENDERER_HEALTH_OK
DispatchQueue.main.async { [weak self] in
self?.healthy = health == GHOSTTY_RENDERER_HEALTH_OK
}
}
@objc private func ghosttyDidContinueKeySequence(notification: SwiftUI.Notification) {
guard let keyAny = notification.userInfo?[Ghostty.Notification.KeySequenceKey] else { return }
guard let key = keyAny as? Ghostty.KeyEquivalent else { return }
keySequence.append(key)
DispatchQueue.main.async { [weak self] in
self?.keySequence.append(key)
}
}
@objc private func ghosttyDidEndKeySequence(notification: SwiftUI.Notification) {
keySequence = []
DispatchQueue.main.async { [weak self] in
self?.keySequence = []
}
}
@objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) {
@@ -381,7 +387,9 @@ extension Ghostty {
] as? Ghostty.Config else { return }
// Update our derived config
self.derivedConfig = DerivedConfig(config)
DispatchQueue.main.async { [weak self] in
self?.derivedConfig = DerivedConfig(config)
}
}
@objc private func ghosttyColorDidChange(_ notification: SwiftUI.Notification) {
@@ -391,7 +399,9 @@ extension Ghostty {
switch (change.kind) {
case .background:
self.backgroundColor = change.color
DispatchQueue.main.async { [weak self] in
self?.backgroundColor = change.color
}
default:
// We don't do anything for the other colors yet.
@@ -413,7 +423,9 @@ extension Ghostty {
// We also just trigger a backing property change. Just in case the screen has
// a different scaling factor, this ensures that we update our content scale.
// Issue: https://github.com/ghostty-org/ghostty/issues/2731
viewDidChangeBackingProperties()
DispatchQueue.main.async { [weak self] in
self?.viewDidChangeBackingProperties()
}
}
// MARK: - NSView
@@ -605,11 +617,12 @@ extension Ghostty {
let mods = Ghostty.ghosttyMods(event.modifierFlags)
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
// If focus follows mouse is enabled then move focus to this surface.
if let window = self.window as? TerminalWindow,
window.isKeyWindow &&
window.focusFollowsMouse &&
!self.focused
// Handle focus-follows-mouse
if let window,
let controller = window.windowController as? BaseTerminalController,
(window.isKeyWindow &&
!self.focused &&
controller.focusFollowsMouse)
{
Ghostty.moveFocus(to: self)
}

View File

@@ -26,6 +26,7 @@
pandoc,
revision ? "dirty",
optimize ? "Debug",
x11 ? true,
}: let
# The Zig hook has no way to select the release type without actual
# overriding of the default flags.
@@ -110,7 +111,7 @@
in
stdenv.mkDerivation (finalAttrs: {
pname = "ghostty";
version = "1.0.0";
version = "1.0.1";
inherit src;
nativeBuildInputs = [
@@ -136,20 +137,21 @@ in
oniguruma
zlib
libX11
libXcursor
libXi
libXrandr
libadwaita
gtk4
glib
gsettings-desktop-schemas
]
++ lib.optionals x11 [
libX11
libXcursor
libXi
libXrandr
];
dontConfigure = true;
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix";
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString x11}";
preBuild = ''
rm -rf $ZIG_GLOBAL_CACHE_DIR
@@ -157,7 +159,12 @@ in
chmod u+rwX -R $ZIG_GLOBAL_CACHE_DIR
'';
outputs = ["out" "terminfo" "shell_integration" "vim"];
outputs = [
"out"
"terminfo"
"shell_integration"
"vim"
];
postInstall = ''
terminfo_src=${
@@ -183,14 +190,13 @@ in
echo "$vim" >> "$out/nix-support/propagated-user-env-packages"
'';
postFixup = ''
patchelf --add-rpath "${lib.makeLibraryPath [libX11]}" "$out/bin/.ghostty-wrapped"
'';
meta = {
homepage = "https://github.com/ghostty-org/ghostty";
license = lib.licenses.mit;
platforms = ["x86_64-linux" "aarch64-linux"];
platforms = [
"x86_64-linux"
"aarch64-linux"
];
mainProgram = "ghostty";
};
})

View File

@@ -1,3 +1,3 @@
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
# more details.
"sha256-lS5v5VdFCLnIyCq9mp7fd2pXhQmkkDFHHVdg4pf37PA="
"sha256-ot5onG1yq7EWQkNUgTNBuqvsnLuaoFs2UDS96IqgJmU="

View File

@@ -587,8 +587,8 @@ test "createNullDelimitedEnvMap" {
test "Command: pre exec" {
if (builtin.os.tag == .windows) return error.SkipZigTest;
var cmd: Command = .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "-v" },
.path = "/bin/sh",
.args = &.{ "/bin/sh", "-v" },
.pre_exec = (struct {
fn do(_: *Command) void {
// This runs in the child, so we can exit and it won't
@@ -598,7 +598,7 @@ test "Command: pre exec" {
}).do,
};
try cmd.start(testing.allocator);
try cmd.testingStart();
try testing.expect(cmd.pid != null);
const exit = try cmd.wait(true);
try testing.expect(exit == .Exited);
@@ -629,12 +629,12 @@ test "Command: redirect stdout to file" {
.args = &.{"C:\\Windows\\System32\\whoami.exe"},
.stdout = stdout,
} else .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "-v" },
.path = "/bin/sh",
.args = &.{ "/bin/sh", "-c", "echo hello" },
.stdout = stdout,
};
try cmd.start(testing.allocator);
try cmd.testingStart();
try testing.expect(cmd.pid != null);
const exit = try cmd.wait(true);
try testing.expect(exit == .Exited);
@@ -663,13 +663,13 @@ test "Command: custom env vars" {
.stdout = stdout,
.env = &env,
} else .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "sh", "-c", "echo $VALUE" },
.path = "/bin/sh",
.args = &.{ "/bin/sh", "-c", "echo $VALUE" },
.stdout = stdout,
.env = &env,
};
try cmd.start(testing.allocator);
try cmd.testingStart();
try testing.expect(cmd.pid != null);
const exit = try cmd.wait(true);
try testing.expect(exit == .Exited);
@@ -699,13 +699,13 @@ test "Command: custom working directory" {
.stdout = stdout,
.cwd = "C:\\Windows\\System32",
} else .{
.path = "/usr/bin/env",
.args = &.{ "/usr/bin/env", "sh", "-c", "pwd" },
.path = "/bin/sh",
.args = &.{ "/bin/sh", "-c", "pwd" },
.stdout = stdout,
.cwd = "/usr/bin",
.cwd = "/tmp",
};
try cmd.start(testing.allocator);
try cmd.testingStart();
try testing.expect(cmd.pid != null);
const exit = try cmd.wait(true);
try testing.expect(exit == .Exited);
@@ -718,7 +718,51 @@ test "Command: custom working directory" {
if (builtin.os.tag == .windows) {
try testing.expectEqualStrings("C:\\Windows\\System32\r\n", contents);
} else if (builtin.os.tag == .macos) {
try testing.expectEqualStrings("/private/tmp\n", contents);
} else {
try testing.expectEqualStrings("/usr/bin\n", contents);
try testing.expectEqualStrings("/tmp\n", contents);
}
}
// Test validate an execveZ failure correctly terminates when error.ExecFailedInChild is correctly handled
//
// Incorrectly handling an error.ExecFailedInChild results in a second copy of the test process running.
// Duplicating the test process leads to weird behavior
// zig build test will hang
// test binary created via -Demit-test-exe will run 2 copies of the test suite
test "Command: posix fork handles execveZ failure" {
if (builtin.os.tag == .windows) {
return error.SkipZigTest;
}
var td = try TempDir.init();
defer td.deinit();
var stdout = try createTestStdout(td.dir);
defer stdout.close();
var cmd: Command = .{
.path = "/not/a/binary",
.args = &.{ "/not/a/binary", "" },
.stdout = stdout,
.cwd = "/bin",
};
try cmd.testingStart();
try testing.expect(cmd.pid != null);
const exit = try cmd.wait(true);
try testing.expect(exit == .Exited);
try testing.expect(exit.Exited == 1);
}
// If cmd.start fails with error.ExecFailedInChild it's the _child_ process that is running. If it does not
// terminate in response to that error both the parent and child will continue as if they _are_ the test suite
// process.
fn testingStart(self: *Command) !void {
self.start(testing.allocator) catch |err| {
if (err == error.ExecFailedInChild) {
// I am a child process, I must not get confused and continue running the rest of the test suite.
posix.exit(1);
}
return err;
};
}

View File

@@ -236,7 +236,7 @@ const DerivedConfig = struct {
clipboard_paste_protection: bool,
clipboard_paste_bracketed_safe: bool,
copy_on_select: configpkg.CopyOnSelect,
confirm_close_surface: bool,
confirm_close_surface: configpkg.ConfirmCloseSurface,
cursor_click_to_move: bool,
desktop_notifications: bool,
font: font.SharedGridSet.DerivedConfig,
@@ -253,6 +253,7 @@ const DerivedConfig = struct {
window_padding_right: u32,
window_padding_balance: bool,
title: ?[:0]const u8,
title_report: bool,
links: []Link,
const Link = struct {
@@ -313,6 +314,7 @@ const DerivedConfig = struct {
.window_padding_right = config.@"window-padding-x".bottom_right,
.window_padding_balance = config.@"window-padding-balance",
.title = config.title,
.title_report = config.@"title-report",
.links = links,
// Assignments happen sequentially so we have to do this last
@@ -784,18 +786,20 @@ pub fn deactivateInspector(self: *Surface) void {
/// True if the surface requires confirmation to quit. This should be called
/// by apprt to determine if the surface should confirm before quitting.
pub fn needsConfirmQuit(self: *Surface) bool {
// If the child has exited then our process is certainly not alive.
// If the child has exited, then our process is certainly not alive.
// We check this first to avoid the locking overhead below.
if (self.child_exited) return false;
// If we are configured to not hold open surfaces explicitly, just
// always say there is nothing alive.
if (!self.config.confirm_close_surface) return false;
// We have to talk to the terminal.
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
return !self.io.terminal.cursorIsAtPrompt();
// Check the configuration for confirming close behavior.
return switch (self.config.confirm_close_surface) {
.always => true,
.false => false,
.true => true: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
break :true !self.io.terminal.cursorIsAtPrompt();
},
};
}
/// Called from the app thread to handle mailbox messages to our specific
@@ -822,7 +826,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
);
},
.report_title => |style| {
.report_title => |style| report_title: {
if (!self.config.title_report) {
log.info("report_title requested, but disabled via config", .{});
break :report_title;
}
const title: ?[:0]const u8 = self.rt_surface.getTitle();
const data = switch (style) {
.csi_21_t => try std.fmt.allocPrint(
@@ -844,11 +853,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
},
.color_change => |change| {
// On any color change, we have to report for mode 2031
// if it is enabled.
self.reportColorScheme(false);
// Notify our apprt
// Notify our apprt, but don't send a mode 2031 DSR report
// because VT sequences were used to change the color.
try self.rt_app.performAction(
.{ .surface = self },
.color_change,
@@ -1701,16 +1707,37 @@ pub fn keyCallback(
// Update our modifiers, this will update mouse mods too
self.modsChanged(event.mods);
// Refresh our link state
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
self.mouseRefreshLinks(
pos,
self.posToViewport(pos.x, pos.y),
self.mouse.over_link,
) catch |err| {
log.warn("failed to refresh links err={}", .{err});
break :mouse_mods;
};
// We only refresh links if
// 1. mouse reporting is off
// OR
// 2. mouse reporting is on and we are not reporting shift to the terminal
if (self.io.terminal.flags.mouse_event == .none or
(self.mouse.mods.shift and !self.mouseShiftCapture(false)))
{
// Refresh our link state
const pos = self.rt_surface.getCursorPos() catch break :mouse_mods;
self.mouseRefreshLinks(
pos,
self.posToViewport(pos.x, pos.y),
self.mouse.over_link,
) catch |err| {
log.warn("failed to refresh links err={}", .{err});
break :mouse_mods;
};
} else if (self.io.terminal.flags.mouse_event != .none and !self.mouse.mods.shift) {
// If we have mouse reports on and we don't have shift pressed, we reset state
try self.rt_app.performAction(
.{ .surface = self },
.mouse_shape,
self.io.terminal.mouse_shape,
);
try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
);
try self.queueRender();
}
}
// Process the cursor state logic. This will update the cursor shape if
@@ -3186,7 +3213,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
.trim = false,
});
defer self.alloc.free(str);
try internal_os.open(self.alloc, str);
try internal_os.open(self.alloc, .unknown, str);
},
._open_osc8 => {
@@ -3194,7 +3221,7 @@ fn processLinks(self: *Surface, pos: apprt.CursorPos) !bool {
log.warn("failed to get URI for OSC8 hyperlink", .{});
return false;
};
try internal_os.open(self.alloc, uri);
try internal_os.open(self.alloc, .unknown, uri);
},
}
@@ -3341,6 +3368,27 @@ pub fn cursorPosCallback(
try self.queueRender();
}
// Handle link hovering
// We refresh links when
// 1. we were previously over a link
// OR
// 2. the cursor position has changed (either we have no previous state, or the state has
// changed)
// AND
// 1. mouse reporting is off
// OR
// 2. mouse reporting is on and we are not reporting shift to the terminal
if ((over_link or
self.mouse.link_point == null or
(self.mouse.link_point != null and !self.mouse.link_point.?.eql(pos_vp))) and
(self.io.terminal.flags.mouse_event == .none or
(self.mouse.mods.shift and !self.mouseShiftCapture(false))))
{
// If we were previously over a link, we always update. We do this so that if the text
// changed underneath us, even if the mouse didn't move, we update the URL hints and state
try self.mouseRefreshLinks(pos, pos_vp, over_link);
}
// Do a mouse report
if (self.io.terminal.flags.mouse_event != .none) report: {
// Shift overrides mouse "grabbing" in the window, taken from Kitty.
@@ -3361,18 +3409,6 @@ pub fn cursorPosCallback(
try self.mouseReport(button, .motion, self.mouse.mods, pos);
// If we were previously over a link, we need to undo the link state.
// We also queue a render so the renderer can undo the rendered link
// state.
if (over_link) {
try self.rt_app.performAction(
.{ .surface = self },
.mouse_over_link,
.{ .url = "" },
);
try self.queueRender();
}
// If we're doing mouse motion tracking, we do not support text
// selection.
return;
@@ -3428,30 +3464,6 @@ pub fn cursorPosCallback(
return;
}
// Handle link hovering
if (self.mouse.link_point) |last_vp| {
// Mark the link's row as dirty.
if (over_link) {
self.renderer_state.terminal.screen.dirty.hyperlink_hover = true;
}
// If our last link viewport point is unchanged, then don't process
// links. This avoids constantly reprocessing regular expressions
// for every pixel change.
if (last_vp.eql(pos_vp)) {
// We have to restore old values that are always cleared
if (over_link) {
self.mouse.over_link = over_link;
self.renderer_state.mouse.point = pos_vp;
}
return;
}
}
// We can process new links.
try self.mouseRefreshLinks(pos, pos_vp, over_link);
}
/// Double-click dragging moves the selection one "word" at a time.
@@ -4230,7 +4242,13 @@ fn writeScreenFile(
const filename = try std.fmt.bufPrint(&filename_buf, "{s}.txt", .{@tagName(loc)});
// Open our scrollback file
var file = try tmp_dir.dir.createFile(filename, .{});
var file = try tmp_dir.dir.createFile(
filename,
switch (builtin.os.tag) {
.windows => .{},
else => .{ .mode = 0o600 },
},
);
defer file.close();
// Screen.dumpString writes byte-by-byte, so buffer it
@@ -4278,11 +4296,16 @@ fn writeScreenFile(
tmp_dir.deinit();
return;
};
// Use topLeft and bottomRight to ensure correct coordinate ordering
const tl = sel.topLeft(&self.io.terminal.screen);
const br = sel.bottomRight(&self.io.terminal.screen);
try self.io.terminal.screen.dumpString(
buf_writer.writer(),
.{
.tl = sel.start(),
.br = sel.end(),
.tl = tl,
.br = br,
.unwrap = true,
},
);
@@ -4294,7 +4317,7 @@ fn writeScreenFile(
const path = try tmp_dir.dir.realpath(filename, &path_buf);
switch (write_action) {
.open => try internal_os.open(self.alloc, path),
.open => try internal_os.open(self.alloc, .text, path),
.paste => self.io.queueMessage(try termio.Message.writeReq(
self.alloc,
path,

View File

@@ -1891,9 +1891,6 @@ pub const CAPI = struct {
// Do nothing if we don't have background transparency enabled
if (config.@"background-opacity" >= 1.0) return;
// Do nothing if our blur value is zero
if (config.@"background-blur-radius" == 0) return;
const nswindow = objc.Object.fromId(window);
_ = CGSSetWindowBackgroundBlurRadius(
CGSDefaultConnectionForThread(),

View File

@@ -15,6 +15,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../../build_config.zig");
const build_options = @import("build_options");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const input = @import("../../input.zig");
@@ -360,6 +361,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// keyboard state but the block does more than that (i.e. setting up
// WM_CLASS).
const x11_xkb: ?x11.Xkb = x11_xkb: {
if (comptime !build_options.x11) break :x11_xkb null;
if (!x11.is_display(display)) break :x11_xkb null;
// Set the X11 window class property (WM_CLASS) if are are on an X11
@@ -966,8 +968,8 @@ fn loadRuntimeCss(
const config: *const Config = &self.config;
const window_theme = config.@"window-theme";
const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background;
const headerbar_background = config.background;
const headerbar_foreground = config.foreground;
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 {{
@@ -985,9 +987,15 @@ fn loadRuntimeCss(
switch (window_theme) {
.ghostty => try writer.print(
\\:root {{
\\ --headerbar-fg-color: rgb({d},{d},{d});
\\ --headerbar-bg-color: rgb({d},{d},{d});
\\ --ghostty-fg: rgb({d},{d},{d});
\\ --ghostty-bg: rgb({d},{d},{d});
\\ --headerbar-fg-color: var(--ghostty-fg);
\\ --headerbar-bg-color: var(--ghostty-bg);
\\ --headerbar-backdrop-color: oklab(from var(--headerbar-bg-color) calc(l * 0.9) a b / alpha);
\\ --overview-fg-color: var(--ghostty-fg);
\\ --overview-bg-color: var(--ghostty-bg);
\\ --popover-fg-color: var(--ghostty-fg);
\\ --popover-bg-color: var(--ghostty-bg);
\\}}
\\windowhandle {{
\\ background-color: var(--headerbar-bg-color);
@@ -1392,7 +1400,15 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
null,
&err,
) orelse {
if (err) |e| log.err("unable to get current color scheme: {s}", .{e.message});
if (err) |e| {
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
if (e.code == 19) {
return self.getColorSchemeDeprecated();
}
// Otherwise, log the error and return .light
log.err("unable to get current color scheme: {s}", .{e.message});
}
return .light;
};
defer c.g_variant_unref(value);
@@ -1409,6 +1425,49 @@ pub fn getColorScheme(self: *App) apprt.ColorScheme {
return .light;
}
/// Call the deprecated D-Bus "Read" method to determine the current color scheme. If
/// there is any error at any point we'll log the error and return "light"
fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
var err: ?*c.GError = null;
defer if (err) |e| c.g_error_free(e);
const value = c.g_dbus_connection_call_sync(
dbus_connection,
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.Settings",
"Read",
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
c.G_VARIANT_TYPE("(v)"),
c.G_DBUS_CALL_FLAGS_NONE,
-1,
null,
&err,
) orelse {
if (err) |e| log.err("Read method failed: {s}", .{e.message});
return .light;
};
defer c.g_variant_unref(value);
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
var inner: ?*c.GVariant = null;
c.g_variant_get(value, "(v)", &inner);
defer if (inner) |i| c.g_variant_unref(i);
if (inner) |i| {
const child = c.g_variant_get_child_value(i, 0) orelse {
return .light;
};
defer c.g_variant_unref(child);
const val = c.g_variant_get_uint32(child);
return if (val == 1) .dark else .light;
}
}
return .light;
}
/// This will be called by D-Bus when the style changes between light & dark.
fn gtkNotifyColorScheme(
_: ?*c.GDBusConnection,

View File

@@ -238,7 +238,7 @@ fn promptText(req: apprt.ClipboardRequest) [:0]const u8 {
\\Pasting this text into the terminal may be dangerous as it looks like some commands may be executed.
,
.osc_52_read =>
\\An appliclication is attempting to read from the clipboard.
\\An application is attempting to read from the clipboard.
\\The current clipboard contents are shown below.
,
.osc_52_write =>

View File

@@ -111,6 +111,16 @@ pub fn init(
// Keep a long-lived reference, which we unref in destroy.
_ = c.g_object_ref(paned);
// Clicks
const gesture_click = c.gtk_gesture_click_new();
errdefer c.g_object_unref(gesture_click);
c.gtk_event_controller_set_propagation_phase(@ptrCast(gesture_click), c.GTK_PHASE_CAPTURE);
c.gtk_gesture_single_set_button(@ptrCast(gesture_click), 1);
c.gtk_widget_add_controller(paned, @ptrCast(gesture_click));
// Signals
_ = c.g_signal_connect_data(gesture_click, "pressed", c.G_CALLBACK(&gtkMouseDown), self, null, c.G_CONNECT_DEFAULT);
// Update all of our containers to point to the right place.
// The split has to point to where the sibling pointed to because
// we're inheriting its parent. The sibling points to its location
@@ -236,6 +246,19 @@ pub fn equalize(self: *Split) f64 {
return weight;
}
fn gtkMouseDown(
_: *c.GtkGestureClick,
n_press: c.gint,
_: c.gdouble,
_: c.gdouble,
ud: ?*anyopaque,
) callconv(.C) void {
if (n_press == 2) {
const self: *Split = @ptrCast(@alignCast(ud));
_ = equalize(self);
}
}
// maxPosition returns the maximum position of the GtkPaned, which is the
// "max-position" attribute.
fn maxPosition(self: *Split) f64 {

View File

@@ -6,6 +6,7 @@ const Surface = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const build_options = @import("build_options");
const configpkg = @import("../../config.zig");
const apprt = @import("../../apprt.zig");
const font = @import("../../font/main.zig");
@@ -346,6 +347,9 @@ cursor: ?*c.GdkCursor = null,
/// pass it to GTK.
title_text: ?[:0]const u8 = null,
/// The timer used to delay title updates in order to prevent flickering.
update_title_timer: ?c.guint = null,
/// The core surface backing this surface
core_surface: CoreSurface,
@@ -506,7 +510,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
var buf: [256]u8 = undefined;
const name = std.fmt.bufPrint(
&buf,
"surfaces/{X}.service",
"surfaces/{X}.scope",
.{@intFromPtr(self)},
) catch unreachable;
@@ -647,6 +651,7 @@ pub fn deinit(self: *Surface) void {
// and therefore the unfocused_overlay has been destroyed as well.
c.g_object_unref(self.im_context);
if (self.cursor) |cursor| c.g_object_unref(cursor);
if (self.update_title_timer) |timer| _ = c.g_source_remove(timer);
self.resize_overlay.deinit();
}
@@ -894,7 +899,23 @@ pub fn setTitle(self: *Surface, slice: [:0]const u8) !void {
if (self.title_text) |old| alloc.free(old);
self.title_text = copy;
// delay the title update to prevent flickering
if (self.update_title_timer) |timer| {
if (c.g_source_remove(timer) == c.FALSE) {
log.warn("unable to remove update title timer", .{});
}
self.update_title_timer = null;
}
self.update_title_timer = c.g_timeout_add(75, updateTitleTimerExpired, self);
}
fn updateTitleTimerExpired(ctx: ?*anyopaque) callconv(.C) c.gboolean {
const self: *Surface = @ptrCast(@alignCast(ctx));
self.updateTitleLabels();
self.update_title_timer = null;
return c.FALSE;
}
pub fn getTitle(self: *Surface) ?[:0]const u8 {
@@ -1183,7 +1204,7 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
@ptrCast(window.window),
&c.GRAPHENE_POINT_INIT(point.x, point.y),
@ptrCast(&point),
) == c.False) {
) == 0) {
log.warn("failed computing point for context menu", .{});
return;
}
@@ -1899,7 +1920,7 @@ pub fn dimSurface(self: *Surface) void {
// Don't dim surface if context menu is open.
// This means we got unfocused due to it opening.
const context_menu_open = c.gtk_widget_get_visible(window.context_menu);
if (context_menu_open == c.True) return;
if (context_menu_open == 1) return;
if (self.unfocused_widget != null) return;
self.unfocused_widget = c.gtk_drawing_area_new();

View File

@@ -83,7 +83,7 @@ pub fn init(self: *Window, app: *App) !void {
// Create the window
const window: *c.GtkWidget = window: {
if (self.isAdwWindow()) {
if ((comptime adwaita.versionAtLeast(0, 0, 0)) and adwaita.enabled(&self.app.config)) {
const window = c.adw_application_window_new(app.app);
c.gtk_widget_add_css_class(@ptrCast(window), "adw");
break :window window;
@@ -124,8 +124,8 @@ pub fn init(self: *Window, app: *App) !void {
// Setup our notebook
self.notebook = Notebook.create(self);
// If we are using an AdwWindow then we can support the tab overview.
self.tab_overview = if (self.isAdwWindow()) overview: {
// If we are using Adwaita, then we can support the tab overview.
self.tab_overview = if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&self.app.config) and adwaita.versionAtLeast(1, 4, 0)) overview: {
const tab_overview = c.adw_tab_overview_new();
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
@@ -156,6 +156,9 @@ pub fn init(self: *Window, app: *App) !void {
if (app.config.@"gtk-titlebar") {
const header = HeaderBar.init(self);
// If we are not decorated then we hide the titlebar.
header.setVisible(app.config.@"window-decoration");
{
const btn = c.gtk_menu_button_new();
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
@@ -167,7 +170,7 @@ pub fn init(self: *Window, app: *App) !void {
// If we're using an AdwWindow then we can support the tab overview.
if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
assert(self.isAdwWindow());
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
const btn = switch (app.config.@"gtk-tabs-location") {
.top, .bottom, .left, .right => btn: {
const btn = c.gtk_toggle_button_new();
@@ -209,6 +212,19 @@ pub fn init(self: *Window, app: *App) !void {
// If we are disabling decorations then disable them right away.
if (!app.config.@"window-decoration") {
c.gtk_window_set_decorated(gtk_window, 0);
// Fix any artifacting that may occur in window corners.
if (app.config.@"gtk-titlebar") {
c.gtk_widget_add_css_class(window, "without-window-decoration-and-with-titlebar");
}
}
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
// need to stick the headerbar into the content box.
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
if (self.header) |h| {
c.gtk_box_append(@ptrCast(box), h.asWidget());
}
}
// In debug we show a warning and apply the 'devel' class to the window.
@@ -250,14 +266,14 @@ pub fn init(self: *Window, app: *App) !void {
// If we have a tab overview then we can set it on our notebook.
if (self.tab_overview) |tab_overview| {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
if (comptime !adwaita.versionAtLeast(1, 3, 0)) unreachable;
assert(self.notebook == .adw_tab_view);
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
}
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
c.gtk_widget_set_parent(self.context_menu, window);
c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), c.False);
c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0);
c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START);
// If we are in fullscreen mode, new windows start fullscreen.
@@ -279,12 +295,13 @@ pub fn init(self: *Window, app: *App) !void {
// Our actions for the menu
initActions(self);
if (self.isAdwWindow()) {
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
const header_widget: *c.GtkWidget = self.header.?.asWidget();
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
if (self.header) |header| {
const header_widget = header.asWidget();
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
}
if (self.app.config.@"gtk-tabs-location" != .hidden) {
const tab_bar = c.adw_tab_bar_new();
@@ -310,28 +327,15 @@ pub fn init(self: *Window, app: *App) !void {
c.adw_toolbar_view_set_top_bar_style(toolbar_view, toolbar_style);
c.adw_toolbar_view_set_bottom_bar_style(toolbar_view, toolbar_style);
// If we are not decorated then we hide the titlebar.
if (!app.config.@"window-decoration") {
c.gtk_widget_set_visible(header_widget, 0);
}
// Set our application window content. The content depends on if
// we're using an AdwTabOverview or not.
if (self.tab_overview) |tab_overview| {
c.adw_tab_overview_set_child(
@ptrCast(tab_overview),
@ptrCast(@alignCast(toolbar_view)),
);
c.adw_application_window_set_content(
@ptrCast(gtk_window),
@ptrCast(@alignCast(tab_overview)),
);
} else {
c.adw_application_window_set_content(
@ptrCast(gtk_window),
@ptrCast(@alignCast(toolbar_view)),
);
}
// Set our application window content.
c.adw_tab_overview_set_child(
@ptrCast(self.tab_overview),
@ptrCast(@alignCast(toolbar_view)),
);
c.adw_application_window_set_content(
@ptrCast(gtk_window),
@ptrCast(@alignCast(self.tab_overview)),
);
} else tab_bar: {
switch (self.notebook) {
.adw_tab_view => |tab_view| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
@@ -365,8 +369,17 @@ pub fn init(self: *Window, app: *App) !void {
}
// The box is our main child
c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| c.gtk_window_set_titlebar(gtk_window, h.asWidget());
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
c.adw_application_window_set_content(
@ptrCast(gtk_window),
box,
);
} else {
c.gtk_window_set_child(gtk_window, box);
if (self.header) |h| {
c.gtk_window_set_titlebar(gtk_window, h.asWidget());
}
}
}
// Show the window
@@ -415,17 +428,6 @@ pub fn deinit(self: *Window) void {
}
}
/// Returns true if this window should use an Adwaita window.
///
/// This must be `inline` so that the comptime check noops conditional
/// paths that are not enabled.
inline fn isAdwWindow(self: *Window) bool {
return (comptime adwaita.versionAtLeast(1, 4, 0)) and
adwaita.enabled(&self.app.config) and
adwaita.versionAtLeast(1, 4, 0) and
self.app.config.@"gtk-titlebar";
}
/// Add a new tab to this window.
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
const alloc = self.app.core_app.alloc;
@@ -512,13 +514,19 @@ pub fn toggleWindowDecorations(self: *Window) void {
const new_decorated = !old_decorated;
c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));
// Fix any artifacting that may occur in window corners.
if (new_decorated) {
c.gtk_widget_add_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
} else {
c.gtk_widget_remove_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
}
// If we have a titlebar, then we also show/hide it depending on the
// decorated state. GTK tends to consider the titlebar part of the frame
// and hides it with decorations, but libadwaita doesn't. This makes it
// explicit.
if (self.header) |v| {
const widget = v.asWidget();
c.gtk_widget_set_visible(widget, @intFromBool(new_decorated));
if (self.header) |headerbar| {
headerbar.setVisible(new_decorated);
}
}
@@ -557,7 +565,7 @@ fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
/// because we need to return an AdwTabPage from this function.
fn gtkNewTabFromOverview(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) ?*c.AdwTabPage {
const self: *Window = userdataSelf(ud.?);
assert(self.isAdwWindow());
assert((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config));
const alloc = self.app.core_app.alloc;
const surface = self.actionSurface();
@@ -735,11 +743,11 @@ fn gtkActionAbout(
const name = "Ghostty";
const icon = "com.mitchellh.ghostty";
const website = "https://github.com/ghostty-org/ghostty";
const website = "https://ghostty.org";
if ((comptime adwaita.versionAtLeast(1, 5, 0)) and
adwaita.versionAtLeast(1, 5, 0) and
self.isAdwWindow())
adwaita.enabled(&self.app.config))
{
c.adw_show_about_dialog(
@ptrCast(self.window),

View File

@@ -25,7 +25,10 @@ pub inline fn enabled(config: *const Config) bool {
/// in the headers. If it is run in a runtime context, it will
/// check the actual version of the library we are linked against.
/// So generally you probably want to do both checks!
pub fn versionAtLeast(
///
/// This is inlined so that the comptime checks will disable the
/// runtime checks if the comptime checks fail.
pub inline fn versionAtLeast(
comptime major: u16,
comptime minor: u16,
comptime micro: u16,
@@ -37,8 +40,9 @@ pub fn versionAtLeast(
// compiling against unknown symbols and makes runtime checks
// very slightly faster.
if (comptime c.ADW_MAJOR_VERSION < major or
c.ADW_MINOR_VERSION < minor or
c.ADW_MICRO_VERSION < micro) return false;
(c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION < minor) or
(c.ADW_MAJOR_VERSION == major and c.ADW_MINOR_VERSION == minor and c.ADW_MICRO_VERSION < micro))
return false;
// If we're in comptime then we can't check the runtime version.
if (@inComptime()) return true;
@@ -56,3 +60,16 @@ pub fn versionAtLeast(
return false;
}
test "versionAtLeast" {
const testing = std.testing;
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(!versionAtLeast(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(!versionAtLeast(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(!versionAtLeast(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
try testing.expect(versionAtLeast(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
}

View File

@@ -1,15 +1,19 @@
const build_options = @import("build_options");
/// Imported C API directly from header files
pub const c = @cImport({
@cInclude("gtk/gtk.h");
if (@import("build_options").adwaita) {
if (build_options.adwaita) {
@cInclude("libadwaita-1/adwaita.h");
}
// Add in X11-specific GDK backend which we use for specific things
// (e.g. X11 window class).
@cInclude("gdk/x11/gdkx.h");
// Xkb for X11 state handling
@cInclude("X11/XKBlib.h");
if (build_options.x11) {
// Add in X11-specific GDK backend which we use for specific things
// (e.g. X11 window class).
@cInclude("gdk/x11/gdkx.h");
// Xkb for X11 state handling
@cInclude("X11/XKBlib.h");
}
// generated header files
@cInclude("ghostty_resources.h");

View File

@@ -47,6 +47,10 @@ const icons = [_]struct {
.alias = "512x512",
.source = "512",
},
.{
.alias = "1024x1024",
.source = "1024",
},
};
pub const gresource_xml = comptimeGenerateGResourceXML();

View File

@@ -30,6 +30,10 @@ pub const HeaderBar = union(enum) {
return .{ .gtk = @ptrCast(headerbar) };
}
pub fn setVisible(self: HeaderBar, visible: bool) void {
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
}
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
return switch (self) {
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),

View File

@@ -1,4 +1,5 @@
const std = @import("std");
const build_options = @import("build_options");
const input = @import("../../input.zig");
const c = @import("c.zig").c;
const x11 = @import("x11.zig");
@@ -111,21 +112,26 @@ pub fn eventMods(
x11_xkb: ?*x11.Xkb,
) input.Mods {
const device = c.gdk_event_get_device(event);
const display = c.gtk_widget_get_display(widget);
var mods = if (x11_xkb) |xkb|
var mods = mods: {
// Add any modifier state events from Xkb if we have them (X11
// only). Null back from the Xkb call means there was no modifier
// event to read. This likely means that the key event did not
// result in a modifier change and we can safely rely on the GDK
// state.
xkb.modifier_state_from_notify(display) orelse
translateMods(gtk_mods)
else
if (comptime build_options.x11) {
const display = c.gtk_widget_get_display(widget);
if (x11_xkb) |xkb| {
if (xkb.modifier_state_from_notify(display)) |x11_mods| break :mods x11_mods;
break :mods translateMods(gtk_mods);
}
}
// On Wayland, we have to use the GDK device because the mods sent
// to this event do not have the modifier key applied if it was
// presssed (i.e. left control).
translateMods(c.gdk_device_get_modifier_state(device));
break :mods translateMods(c.gdk_device_get_modifier_state(device));
};
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;

View File

@@ -339,7 +339,7 @@ pub const Notebook = union(enum) {
c.g_object_unref(tab.box);
}
c.gtk_window_destroy(tab.window.window);
c.gtk_window_destroy(window.window);
}
},
.gtk_notebook => |notebook| {

View File

@@ -33,6 +33,10 @@ label.size-overlay.hidden {
opacity: 0;
}
window.without-window-decoration-and-with-titlebar {
border-radius: 0 0;
}
.transparent {
background-color: transparent;
}

View File

@@ -19,8 +19,9 @@ pub inline fn atLeast(
// compiling against unknown symbols and makes runtime checks
// very slightly faster.
if (comptime c.GTK_MAJOR_VERSION < major or
c.GTK_MINOR_VERSION < minor or
c.GTK_MICRO_VERSION < micro) return false;
(c.GTK_MAJOR_VERSION == major and c.GTK_MINOR_VERSION < minor) or
(c.GTK_MAJOR_VERSION == major and c.GTK_MINOR_VERSION == minor and c.GTK_MICRO_VERSION < micro))
return false;
// If we're in comptime then we can't check the runtime version.
if (@inComptime()) return true;
@@ -38,3 +39,20 @@ pub inline fn atLeast(
return false;
}
test "atLeast" {
const std = @import("std");
const testing = std.testing;
try testing.expect(atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(!atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(!atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(!atLeast(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(atLeast(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
try testing.expect(atLeast(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
try testing.expect(atLeast(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
try testing.expect(atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
}

View File

@@ -1,5 +1,6 @@
/// Utility functions for X11 handling.
const std = @import("std");
const build_options = @import("build_options");
const c = @import("c.zig").c;
const input = @import("../../input.zig");
@@ -7,6 +8,7 @@ const log = std.log.scoped(.gtk_x11);
/// Returns true if the passed in display is an X11 display.
pub fn is_display(display: ?*c.GdkDisplay) bool {
if (comptime !build_options.x11) return false;
return c.g_type_check_instance_is_a(
@ptrCast(@alignCast(display orelse return false)),
c.gdk_x11_display_get_type(),
@@ -15,17 +17,19 @@ pub fn is_display(display: ?*c.GdkDisplay) bool {
/// Returns true if the app is running on X11
pub fn is_current_display_server() bool {
if (comptime !build_options.x11) return false;
const display = c.gdk_display_get_default();
return is_display(display);
}
pub const Xkb = struct {
base_event_code: c_int,
funcs: Funcs,
/// Initialize an Xkb struct, for the given GDK display. If the display
/// isn't backed by X then this will return null.
/// Initialize an Xkb struct for the given GDK display. If the display isn't
/// backed by X then this will return null.
pub fn init(display_: ?*c.GdkDisplay) !?Xkb {
if (comptime !build_options.x11) return null;
// Display should never be null but we just treat that as a non-X11
// display so that the caller can just ignore it and not unwrap it.
const display = display_ orelse return null;
@@ -37,7 +41,6 @@ pub const Xkb = struct {
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
var result: Xkb = .{
.base_event_code = 0,
.funcs = try Funcs.init(),
};
log.debug("Xkb.init: running XkbQueryExtension", .{});
@@ -45,7 +48,7 @@ pub const Xkb = struct {
var base_error_code: c_int = 0;
var major = c.XkbMajorVersion;
var minor = c.XkbMinorVersion;
if (result.funcs.XkbQueryExtension(
if (c.XkbQueryExtension(
xdisplay,
&opcode,
&result.base_event_code,
@@ -58,7 +61,7 @@ pub const Xkb = struct {
}
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
if (result.funcs.XkbSelectEventDetails(
if (c.XkbSelectEventDetails(
xdisplay,
c.XkbUseCoreKbd,
c.XkbStateNotify,
@@ -83,15 +86,17 @@ pub const Xkb = struct {
/// back to the standard GDK modifier state (this likely means the key
/// event did not result in a modifier change).
pub fn modifier_state_from_notify(self: Xkb, display_: ?*c.GdkDisplay) ?input.Mods {
if (comptime !build_options.x11) return null;
const display = display_ orelse return null;
// Shoutout to Mozilla for figuring out a clean way to do this, this is
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
if (self.funcs.XEventsQueued(xdisplay, c.QueuedAfterReading) == 0) return null;
if (c.XEventsQueued(xdisplay, c.QueuedAfterReading) == 0) return null;
var nextEvent: c.XEvent = undefined;
_ = self.funcs.XPeekEvent(xdisplay, &nextEvent);
_ = c.XPeekEvent(xdisplay, &nextEvent);
if (nextEvent.type != self.base_event_code) return null;
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
@@ -112,39 +117,3 @@ pub const Xkb = struct {
return mods;
}
};
/// The functions that we load dynamically from libX11.so.
const Funcs = struct {
XkbQueryExtension: XkbQueryExtensionType,
XkbSelectEventDetails: XkbSelectEventDetailsType,
XEventsQueued: XEventsQueuedType,
XPeekEvent: XPeekEventType,
const XkbQueryExtensionType = *const fn (?*c.struct__XDisplay, [*c]c_int, [*c]c_int, [*c]c_int, [*c]c_int, [*c]c_int) callconv(.C) c_int;
const XkbSelectEventDetailsType = *const fn (?*c.struct__XDisplay, c_uint, c_uint, c_ulong, c_ulong) callconv(.C) c_int;
const XEventsQueuedType = *const fn (?*c.struct__XDisplay, c_int) callconv(.C) c_int;
const XPeekEventType = *const fn (?*c.struct__XDisplay, [*c]c.union__XEvent) callconv(.C) c_int;
pub fn init() !Funcs {
var libX11 = try std.DynLib.open("libX11.so");
defer libX11.close();
var result: Funcs = undefined;
inline for (@typeInfo(Funcs).Struct.fields) |field| {
const name = comptime name: {
const null_term = field.name ++ .{0};
break :name null_term[0..field.name.len :0];
};
@field(result, field.name) = libX11.lookup(
field.type,
name,
) orelse {
log.err(" error dynamic loading libX11: missing symbol {s}", .{field.name});
return error.XkbInitializationError;
};
}
return result;
}
};

View File

@@ -56,7 +56,7 @@ fn writeFishCompletions(writer: anytype) !void {
else {
try writer.writeAll(if (field.type != Config.RepeatablePath) " -f" else " -F");
switch (@typeInfo(field.type)) {
.Bool => try writer.writeAll(" -a \"true false\""),
.Bool => {},
.Enum => |info| {
try writer.writeAll(" -a \"");
for (info.fields, 0..) |f, i| {
@@ -114,7 +114,7 @@ fn writeFishCompletions(writer: anytype) !void {
} else try writer.writeAll(" -f");
switch (@typeInfo(opt.type)) {
.Bool => try writer.writeAll(" -a \"true false\""),
.Bool => {},
.Enum => |info| {
try writer.writeAll(" -a \"");
for (info.fields, 0..) |f, i| {

View File

@@ -71,7 +71,7 @@ If your configuration file has any errors, Ghostty does its best to ignore
them and move on. Configuration errors currently show up in the log. The log
is written directly to stderr, so it is up to you to figure out how to access
that for your system (for now). On macOS, you can also use the system `log` CLI
utility. See the Mac App section for more information.
utility with `log stream --level debug --predicate 'subsystem=="com.mitchellh.ghostty"'`.
## Debugging Configuration

View File

@@ -13,6 +13,7 @@ pub fn genKeybindActions(writer: anytype) !void {
\\---
\\title: Keybinding Action Reference
\\description: Reference of all Ghostty keybinding actions.
\\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/input/Binding.zig
\\---
\\
\\This is a reference of all Ghostty keybinding actions.
@@ -21,10 +22,22 @@ pub fn genKeybindActions(writer: anytype) !void {
);
@setEvalBranchQuota(5_000);
var buffer = std.ArrayList(u8).init(std.heap.page_allocator);
defer buffer.deinit();
const fields = @typeInfo(KeybindAction).Union.fields;
inline for (fields) |field| {
if (field.name[0] == '_') continue;
// Write previously stored doc comment below all related actions
if (@hasDecl(help_strings.KeybindAction, field.name)) {
try writer.writeAll(buffer.items);
try writer.writeAll("\n");
buffer.clearRetainingCapacity();
}
// Write the field name.
try writer.writeAll("## `");
try writer.writeAll(field.name);
@@ -37,10 +50,9 @@ pub fn genKeybindActions(writer: anytype) !void {
'\n',
);
while (iter.next()) |s| {
try writer.writeAll(s);
try writer.writeAll("\n");
try buffer.appendSlice(s);
try buffer.appendSlice("\n");
}
try writer.writeAll("\n\n");
}
}
}

View File

@@ -13,6 +13,7 @@ pub fn genConfig(writer: anytype) !void {
\\---
\\title: Reference
\\description: Reference of all Ghostty configuration options.
\\editOnGithubLink: https://github.com/ghostty-org/ghostty/edit/main/src/config/Config.zig
\\---
\\
\\This is a reference of all Ghostty configuration options. These

View File

@@ -7,6 +7,8 @@ const Action = @import("../cli/action.zig").Action;
/// and options.
pub const zsh_completions = comptimeGenerateZshCompletions();
const equals_required = "=-:::";
fn comptimeGenerateZshCompletions() []const u8 {
comptime {
@setEvalBranchQuota(50000);
@@ -47,34 +49,42 @@ fn writeZshCompletions(writer: anytype) !void {
if (field.name[0] == '_') continue;
try writer.writeAll(" \"--");
try writer.writeAll(field.name);
try writer.writeAll("=-:::");
if (std.mem.startsWith(u8, field.name, "font-family"))
try writer.writeAll("_fonts")
else if (std.mem.eql(u8, "theme", field.name))
try writer.writeAll("_themes")
else if (std.mem.eql(u8, "working-directory", field.name))
try writer.writeAll("{_files -/}")
else if (field.type == Config.RepeatablePath)
try writer.writeAll("_files") // todo check if this is needed
else {
try writer.writeAll("(");
if (std.mem.startsWith(u8, field.name, "font-family")) {
try writer.writeAll(equals_required);
try writer.writeAll("_fonts");
} else if (std.mem.eql(u8, "theme", field.name)) {
try writer.writeAll(equals_required);
try writer.writeAll("_themes");
} else if (std.mem.eql(u8, "working-directory", field.name)) {
try writer.writeAll(equals_required);
try writer.writeAll("{_files -/}");
} else if (field.type == Config.RepeatablePath) {
try writer.writeAll(equals_required);
try writer.writeAll("_files"); // todo check if this is needed
} else {
switch (@typeInfo(field.type)) {
.Bool => try writer.writeAll("true false"),
.Bool => {},
.Enum => |info| {
try writer.writeAll(equals_required);
try writer.writeAll("(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
}
try writer.writeAll(")");
},
.Struct => |info| {
try writer.writeAll(equals_required);
if (!@hasDecl(field.type, "parseCLI") and info.layout == .@"packed") {
try writer.writeAll("(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
try writer.writeAll(f.name);
try writer.writeAll(" no-");
try writer.writeAll(f.name);
}
try writer.writeAll(")");
} else {
//resize-overlay-duration
//keybind
@@ -85,12 +95,14 @@ fn writeZshCompletions(writer: anytype) !void {
//foreground
//font-variation*
//font-feature
try writer.writeAll(" ");
try writer.writeAll("( )");
}
},
else => try writer.writeAll(" "),
else => {
try writer.writeAll(equals_required);
try writer.writeAll("( )");
},
}
try writer.writeAll(")");
}
try writer.writeAll("\" \\\n");
@@ -170,10 +182,11 @@ fn writeZshCompletions(writer: anytype) !void {
try writer.writeAll(padding ++ " '--");
try writer.writeAll(opt.name);
try writer.writeAll("=-:::");
switch (@typeInfo(opt.type)) {
.Bool => try writer.writeAll("(true false)"),
.Bool => {},
.Enum => |info| {
try writer.writeAll(equals_required);
try writer.writeAll("(");
for (info.fields, 0..) |f, i| {
if (i > 0) try writer.writeAll(" ");
@@ -182,6 +195,7 @@ fn writeZshCompletions(writer: anytype) !void {
try writer.writeAll(")");
},
.Optional => |optional| {
try writer.writeAll(equals_required);
switch (@typeInfo(optional.child)) {
.Enum => |info| {
try writer.writeAll("(");
@@ -199,11 +213,13 @@ fn writeZshCompletions(writer: anytype) !void {
}
},
else => {
try writer.writeAll(equals_required);
if (std.mem.eql(u8, "config-file", opt.name)) {
try writer.writeAll("_files");
} else try writer.writeAll("( )");
},
}
try writer.writeAll("' \\\n");
}
try writer.writeAll(padding ++ ";;\n");

View File

@@ -22,6 +22,7 @@ pub const BuildConfig = struct {
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
flatpak: bool = false,
adwaita: bool = false,
x11: bool = false,
app_runtime: apprt.Runtime = .none,
renderer: rendererpkg.Impl = .opengl,
font_backend: font.Backend = .freetype,
@@ -41,6 +42,7 @@ pub const BuildConfig = struct {
// support all types.
step.addOption(bool, "flatpak", self.flatpak);
step.addOption(bool, "adwaita", self.adwaita);
step.addOption(bool, "x11", self.x11);
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
step.addOption(font.Backend, "font_backend", self.font_backend);
step.addOption(rendererpkg.Impl, "renderer", self.renderer);

View File

@@ -73,11 +73,11 @@ const ThemeListElement = struct {
///
/// The second directory is the `themes` subdirectory of the Ghostty resources
/// directory. Ghostty ships with a multitude of themes that will be installed
/// into this directory. On macOS, this directory is the `Ghostty.app/Contents/
/// Resources/ghostty/themes`. On Linux, this directory is the `share/ghostty/
/// themes` (wherever you installed the Ghostty "share" directory). If you're
/// running Ghostty from the source, this is the `zig-out/share/ghostty/themes`
/// directory.
/// into this directory. On macOS, this directory is the
/// `Ghostty.app/Contents/Resources/ghostty/themes`. On Linux, this directory
/// is the `share/ghostty/themes` (wherever you installed the Ghostty "share"
/// directory). If you're running Ghostty from the source, this is the
/// `zig-out/share/ghostty/themes` directory.
///
/// You can also set the `GHOSTTY_RESOURCES_DIR` environment variable to point
/// to the resources directory.
@@ -127,6 +127,8 @@ pub fn run(gpa_alloc: std.mem.Allocator) !u8 {
while (try walker.next()) |entry| {
switch (entry.kind) {
.file, .sym_link => {
if (std.mem.eql(u8, entry.name, ".DS_Store"))
continue;
count += 1;
try themes.append(.{
.location = loc.location,
@@ -1100,7 +1102,7 @@ const Preview = struct {
},
.{
.text = "ziggzag.zig",
.text = "ziggzagg.zig",
.style = bold,
},
},

View File

@@ -61,6 +61,11 @@ pub fn run(alloc: Allocator) !u8 {
} else {
try stdout.print(" - libadwaita : disabled\n", .{});
}
if (comptime build_options.x11) {
try stdout.print(" - libX11 : enabled\n", .{});
} else {
try stdout.print(" - libX11 : disabled\n", .{});
}
}
return 0;
}

View File

@@ -14,6 +14,7 @@ pub const formatEntry = formatter.formatEntry;
// Field types
pub const ClipboardAccess = Config.ClipboardAccess;
pub const ConfirmCloseSurface = Config.ConfirmCloseSurface;
pub const CopyOnSelect = Config.CopyOnSelect;
pub const CustomShaderAnimation = Config.CustomShaderAnimation;
pub const FontSyntheticStyle = Config.FontSyntheticStyle;

View File

@@ -255,30 +255,40 @@ const c = @cImport({
/// that things like status lines continue to look aligned.
@"adjust-cell-width": ?MetricModifier = null,
@"adjust-cell-height": ?MetricModifier = null,
/// Distance in pixels from the bottom of the cell to the text baseline.
/// Distance in pixels or percentage adjustment from the bottom of the cell to the text baseline.
/// Increase to move baseline UP, decrease to move baseline DOWN.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-font-baseline": ?MetricModifier = null,
/// Distance in pixels from the top of the cell to the top of the underline.
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the underline.
/// Increase to move underline DOWN, decrease to move underline UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-underline-position": ?MetricModifier = null,
/// Thickness in pixels of the underline.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-underline-thickness": ?MetricModifier = null,
/// Distance in pixels from the top of the cell to the top of the strikethrough.
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the strikethrough.
/// Increase to move strikethrough DOWN, decrease to move underline UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-strikethrough-position": ?MetricModifier = null,
/// Thickness in pixels of the strikethrough.
/// Thickness in pixels or percentage adjustment of the strikethrough.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-strikethrough-thickness": ?MetricModifier = null,
/// Distance in pixels from the top of the cell to the top of the overline.
/// Distance in pixels or percentage adjustment from the top of the cell to the top of the overline.
/// Increase to move overline DOWN, decrease to move underline UP.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-overline-position": ?MetricModifier = null,
/// Thickness in pixels of the overline.
/// Thickness in pixels or percentage adjustment of the overline.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-overline-thickness": ?MetricModifier = null,
/// Thickness in pixels of the bar cursor and outlined rect cursor.
/// Thickness in pixels or percentage adjustment of the bar cursor and outlined rect cursor.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-cursor-thickness": ?MetricModifier = null,
/// Height in pixels of the cursor. Currently applies to all cursor types:
/// Height in pixels or percentage adjustment of the cursor. Currently applies to all cursor types:
/// bar, rect, and outlined rect.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-cursor-height": ?MetricModifier = null,
/// Thickness in pixels of box drawing characters.
/// Thickness in pixels or percentage adjustment of box drawing characters.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-box-thickness": ?MetricModifier = null,
/// The method to use for calculating the cell width of a grapheme cluster.
@@ -351,10 +361,10 @@ const c = @cImport({
///
/// The second directory is the `themes` subdirectory of the Ghostty resources
/// directory. Ghostty ships with a multitude of themes that will be installed
/// into this directory. On macOS, this list is in the `Ghostty.app/Contents/
/// Resources/ghostty/themes` directory. On Linux, this list is in the `share/
/// ghostty/themes` directory (wherever you installed the Ghostty "share"
/// directory.
/// into this directory. On macOS, this list is in the
/// `Ghostty.app/Contents/Resources/ghostty/themes` directory. On Linux, this
/// list is in the `share/ghostty/themes` directory (wherever you installed the
/// Ghostty "share" directory.
///
/// To see a list of available themes, run `ghostty +list-themes`.
///
@@ -475,7 +485,7 @@ palette: Palette = .{},
///
/// Valid values are:
///
/// * `` (blank)
/// * ` ` (blank)
/// * `true`
/// * `false`
///
@@ -628,7 +638,7 @@ command: ?[]const u8 = null,
/// (i.e. by wrapping your command in a shell, setting env vars, etc.).
/// This is a safety measure to prevent unexpected behavior. If you want
/// shell integration with a `-e`-executed command, you must either
/// name your binary appopriately or source the shell integration script
/// name your binary appropriately or source the shell integration script
/// manually.
///
@"initial-command": ?[]const u8 = null,
@@ -669,7 +679,7 @@ command: ?[]const u8 = null,
/// This is a future planned feature.
///
/// This can be changed at runtime but will only affect new terminal surfaces.
@"scrollback-limit": u32 = 10_000_000, // 10MB
@"scrollback-limit": usize = 10_000_000, // 10MB
/// Match a regular expression against the terminal text and associate clicking
/// it with an action. This can be used to match URLs, file paths, etc. Actions
@@ -769,7 +779,25 @@ class: ?[:0]const u8 = null,
/// the documentation or using the `ghostty +list-actions` command.
///
/// Trigger: `+`-separated list of keys and modifiers. Example: `ctrl+a`,
/// `ctrl+shift+b`, `up`. Some notes:
/// `ctrl+shift+b`, `up`.
///
/// Valid keys are currently only listed in the
/// [Ghostty source code](https://github.com/ghostty-org/ghostty/blob/d6e76858164d52cff460fedc61ddf2e560912d71/src/input/key.zig#L255).
/// This is a documentation limitation and we will improve this in the future.
/// A common gotcha is that numeric keys are written as words: i.e. `one`,
/// `two`, `three`, etc. and not `1`, `2`, `3`. This will also be improved in
/// the future.
///
/// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`,
/// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier
/// or the alias. When debugging keybinds, the non-aliased modifier will always
/// be used in output.
///
/// Note: The fn or "globe" key on keyboards are not supported as a
/// modifier. This is a limitation of the operating systems and GUI toolkits
/// that Ghostty uses.
///
/// Some additional notes for triggers:
///
/// * modifiers cannot repeat, `ctrl+ctrl+a` is invalid.
///
@@ -783,15 +811,6 @@ class: ?[:0]const u8 = null,
/// mapping responds to the hardware keycode and not the keycode
/// translated by any system keyboard layouts. Example: "ctrl+physical:a"
///
/// Valid modifiers are `shift`, `ctrl` (alias: `control`), `alt` (alias: `opt`,
/// `option`), and `super` (alias: `cmd`, `command`). You may use the modifier
/// or the alias. When debugging keybinds, the non-aliased modifier will always
/// be used in output.
///
/// Note: The fn or "globe" key on keyboards are not supported as a
/// modifier. This is a limitation of the operating systems and GUI toolkits
/// that Ghostty uses.
///
/// You may also specify multiple triggers separated by `>` to require a
/// sequence of triggers to activate the action. For example,
/// `ctrl+a>n=new_window` will only trigger the `new_window` action if the
@@ -1079,7 +1098,7 @@ keybind: Keybinds = .{},
/// BUG: On Linux with GTK, the calculated window size will not properly take
/// into account window decorations. As a result, the grid dimensions will not
/// exactly match this configuration. If window decorations are disabled (see
/// window-decorations), then this will work as expected.
/// `window-decoration`), then this will work as expected.
///
/// Windows smaller than 10 wide by 4 high are not allowed.
@"window-height": u32 = 0,
@@ -1130,6 +1149,16 @@ keybind: Keybinds = .{},
/// * `end` - Insert the new tab at the end of the tab list.
@"window-new-tab-position": WindowNewTabPosition = .current,
/// Background color for the window titlebar. This only takes effect if
/// window-theme is set to ghostty. Currently only supported in the GTK app
/// runtime.
@"window-titlebar-background": ?Color = null,
/// Foreground color for the window titlebar. This only takes effect if
/// window-theme is set to ghostty. Currently only supported in the GTK app
/// runtime.
@"window-titlebar-foreground": ?Color = null,
/// This controls when resize overlays are shown. Resize overlays are a
/// transient popup that shows the size of the terminal while the surfaces are
/// being resized. The possible options are:
@@ -1189,12 +1218,12 @@ keybind: Keybinds = .{},
/// value larger than this will be clamped to the maximum value.
@"resize-overlay-duration": Duration = .{ .duration = 750 * std.time.ns_per_ms },
// If true, when there are multiple split panes, the mouse selects the pane
// that is focused. This only applies to the currently focused window; i.e.
// mousing over a split in an unfocused window will now focus that split
// and bring the window to front.
//
// Default is false.
/// If true, when there are multiple split panes, the mouse selects the pane
/// that is focused. This only applies to the currently focused window; i.e.
/// mousing over a split in an unfocused window will not focus that split
/// and bring the window to front.
///
/// Default is false.
@"focus-follows-mouse": bool = false,
/// Whether to allow programs running in the terminal to read/write to the
@@ -1225,6 +1254,15 @@ keybind: Keybinds = .{},
/// program, not the terminal emulator).
@"clipboard-paste-bracketed-safe": bool = true,
/// Enables or disabled title reporting (CSI 21 t). This escape sequence
/// allows the running program to query the terminal title. This is a common
/// security issue and is disabled by default.
///
/// Warning: This can expose sensitive information at best and enable
/// arbitrary code execution at worst (with a maliciously crafted title
/// and a minor amount of user interaction).
@"title-report": bool = false,
/// The total amount of bytes that can be used for image data (i.e. the Kitty
/// image protocol) per terminal screen. The maximum value is 4,294,967,295
/// (4GiB). The default is 320MB. If this is set to zero, then all image
@@ -1304,9 +1342,13 @@ keybind: Keybinds = .{},
/// This configuration can only be set via CLI arguments.
@"config-default-files": bool = true,
/// Confirms that a surface should be closed before closing it. This defaults to
/// true. If set to false, surfaces will close without any confirmation.
@"confirm-close-surface": bool = true,
/// Confirms that a surface should be closed before closing it.
///
/// This defaults to `true`. If set to `false`, surfaces will close without
/// any confirmation. This can also be set to `always`, which will always
/// confirm closing a surface, even if shell integration says a process isn't
/// running.
@"confirm-close-surface": ConfirmCloseSurface = .true,
/// Whether or not to quit after the last surface is closed.
///
@@ -1380,6 +1422,9 @@ keybind: Keybinds = .{},
/// * `center` - Terminal appears at the center of the screen.
///
/// Changing this configuration requires restarting Ghostty completely.
///
/// Note: There is no default keybind for toggling the quick terminal.
/// To enable this feature, bind the `toggle_quick_terminal` action to a key.
@"quick-terminal-position": QuickTerminalPosition = .top,
/// The screen where the quick terminal should show up.
@@ -1812,7 +1857,7 @@ keybind: Keybinds = .{},
///
/// If `false`, each new ghostty process will launch a separate application.
///
/// The default value is `detect` which will default to `true` if Ghostty
/// The default value is `desktop` which will default to `true` if Ghostty
/// detects that it was launched from the `.desktop` file such as an app
/// launcher (like Gnome Shell) or by D-Bus activation. If Ghostty is launched
/// from the command line, it will default to `false`.
@@ -2303,18 +2348,18 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
);
}
{
// On macOS we default to super but everywhere else
// is alt.
const mods: inputpkg.Mods = if (builtin.target.isDarwin())
.{ .super = true }
else
.{ .alt = true };
// Cmd+N for goto tab N
const start = @intFromEnum(inputpkg.Key.one);
const end = @intFromEnum(inputpkg.Key.nine);
const end = @intFromEnum(inputpkg.Key.eight);
var i: usize = start;
while (i <= end) : (i += 1) {
// On macOS we default to super but everywhere else
// is alt.
const mods: inputpkg.Mods = if (builtin.target.isDarwin())
.{ .super = true }
else
.{ .alt = true };
try result.keybind.set.put(
alloc,
.{
@@ -2333,6 +2378,17 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .goto_tab = (i - start) + 1 },
);
}
try result.keybind.set.put(
alloc,
.{
.key = if (comptime builtin.target.isDarwin())
.{ .physical = .nine }
else
.{ .translated = .nine },
.mods = mods,
},
.{ .last_tab = {} },
);
}
// Toggle fullscreen
@@ -2437,11 +2493,6 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .{ .translated = .right_bracket }, .mods = .{ .super = true, .shift = true } },
.{ .next_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .{ .physical = inputpkg.Key.zero }, .mods = .{ .super = true } },
.{ .last_tab = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .d }, .mods = .{ .super = true } },
@@ -2549,6 +2600,11 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .{ .translated = .left }, .mods = .{ .super = true } },
.{ .text = "\\x01" },
);
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .backspace }, .mods = .{ .super = true } },
.{ .esc = "\x15" },
);
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .left }, .mods = .{ .alt = true } },
@@ -2612,18 +2668,40 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
try self.expandPaths(std.fs.path.dirname(path).?);
}
pub const OptionalFileAction = enum { loaded, not_found, @"error" };
/// Load optional configuration file from `path`. All errors are ignored.
pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void {
self.loadFile(alloc, path) catch |err| switch (err) {
error.FileNotFound => std.log.info(
"optional config file not found, not loading path={s}",
.{path},
),
else => std.log.warn(
"error reading optional config file, not loading err={} path={s}",
.{ err, path },
),
};
///
/// Returns the action that was taken.
pub fn loadOptionalFile(
self: *Config,
alloc: Allocator,
path: []const u8,
) OptionalFileAction {
if (self.loadFile(alloc, path)) {
return .loaded;
} else |err| switch (err) {
error.FileNotFound => return .not_found,
else => {
std.log.warn(
"error reading optional config file, not loading err={} path={s}",
.{ err, path },
);
return .@"error";
},
}
}
fn writeConfigTemplate(path: []const u8) !void {
log.info("creating template config file: path={s}", .{path});
const file = try std.fs.createFileAbsolute(path, .{});
defer file.close();
try std.fmt.format(
file.writer(),
@embedFile("./config-template"),
.{ .path = path },
);
}
/// Load configurations from the default configuration files. The default
@@ -2632,14 +2710,30 @@ pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void
/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config`
/// is also loaded.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
// Load XDG first
const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
defer alloc.free(xdg_path);
self.loadOptionalFile(alloc, xdg_path);
const xdg_action = self.loadOptionalFile(alloc, xdg_path);
// On macOS load the app support directory as well
if (comptime builtin.os.tag == .macos) {
const app_support_path = try internal_os.macos.appSupportDir(alloc, "config");
defer alloc.free(app_support_path);
self.loadOptionalFile(alloc, app_support_path);
const app_support_action = self.loadOptionalFile(alloc, app_support_path);
// If both files are not found, then we create a template file.
// For macOS, we only create the template file in the app support
if (app_support_action == .not_found and xdg_action == .not_found) {
writeConfigTemplate(app_support_path) catch |err| {
log.warn("error creating template config file err={}", .{err});
};
}
} else {
if (xdg_action == .not_found) {
writeConfigTemplate(xdg_path) catch |err| {
log.warn("error creating template config file err={}", .{err});
};
}
}
}
@@ -2749,17 +2843,21 @@ pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
// replace the entire list with the new list.
inline for (fields, 0..) |field, i| {
const v = &@field(self, field);
const len = v.list.items.len - counter[i];
if (len > 0) {
// Note: we don't have to worry about freeing the memory
// that we overwrite or cut off here because its all in
// an arena.
v.list.replaceRangeAssumeCapacity(
0,
len,
v.list.items[counter[i]..],
);
v.list.items.len = len;
// The list can be empty if it was reset, i.e. --font-family=""
if (v.list.items.len > 0) {
const len = v.list.items.len - counter[i];
if (len > 0) {
// Note: we don't have to worry about freeing the memory
// that we overwrite or cut off here because its all in
// an arena.
v.list.replaceRangeAssumeCapacity(
0,
len,
v.list.items[counter[i]..],
);
v.list.items.len = len;
}
}
}
}
@@ -3630,6 +3728,15 @@ const Replay = struct {
}
};
/// Valid values for confirm-close-surface
/// c_int because it needs to be extern compatible
/// If this is changed, you must also update ghostty.h
pub const ConfirmCloseSurface = enum(c_int) {
false,
true,
always,
};
/// Valid values for custom-shader-animation
/// c_int because it needs to be extern compatible
/// If this is changed, you must also update ghostty.h
@@ -3732,17 +3839,22 @@ pub const Color = struct {
pub fn fromHex(input: []const u8) !Color {
// Trim the beginning '#' if it exists
const trimmed = if (input.len != 0 and input[0] == '#') input[1..] else input;
if (trimmed.len != 6 and trimmed.len != 3) return error.InvalidValue;
// We expect exactly 6 for RRGGBB
if (trimmed.len != 6) return error.InvalidValue;
// Expand short hex values to full hex values
const rgb: []const u8 = if (trimmed.len == 3) &.{
trimmed[0], trimmed[0],
trimmed[1], trimmed[1],
trimmed[2], trimmed[2],
} else trimmed;
// Parse the colors two at a time.
var result: Color = undefined;
comptime var i: usize = 0;
inline while (i < 6) : (i += 2) {
const v: u8 =
((try std.fmt.charToDigit(trimmed[i], 16)) * 16) +
try std.fmt.charToDigit(trimmed[i + 1], 16);
((try std.fmt.charToDigit(rgb[i], 16)) * 16) +
try std.fmt.charToDigit(rgb[i + 1], 16);
@field(result, switch (i) {
0 => "r",
@@ -3762,6 +3874,8 @@ pub const Color = struct {
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("#0A0B0C"));
try testing.expectEqual(Color{ .r = 10, .g = 11, .b = 12 }, try Color.fromHex("0A0B0C"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFFFFF"));
try testing.expectEqual(Color{ .r = 255, .g = 255, .b = 255 }, try Color.fromHex("FFF"));
try testing.expectEqual(Color{ .r = 51, .g = 68, .b = 85 }, try Color.fromHex("#345"));
}
test "parseCLI from name" {
@@ -4636,9 +4750,11 @@ pub const Keybinds = struct {
try list.parseCLI(alloc, "ctrl+z>2=goto_tab:2");
try list.formatEntry(formatterpkg.entryFormatter("keybind", buf.writer()));
// Note they turn into translated keys because they match
// their ASCII mapping.
const want =
\\keybind = ctrl+z>1=goto_tab:1
\\keybind = ctrl+z>2=goto_tab:2
\\keybind = ctrl+z>two=goto_tab:2
\\keybind = ctrl+z>one=goto_tab:1
\\
;
try std.testing.expectEqualStrings(want, buf.items);

View File

@@ -0,0 +1,43 @@
# This is the configuration file for Ghostty.
#
# This template file has been automatically created at the following
# path since Ghostty couldn't find any existing config files on your system:
#
# {[path]s}
#
# The template does not set any default options, since Ghostty ships
# with sensible defaults for all options. Users should only need to set
# options that they want to change from the default.
#
# Run `ghostty +show-config --default --docs` to view a list of
# all available config options and their default values.
#
# Additionally, each config option is also explained in detail
# on Ghostty's website, at https://ghostty.org/docs/config.
# Config syntax crash course
# ==========================
# # The config file consists of simple key-value pairs,
# # separated by equals signs.
# font-family = Iosevka
# window-padding-x = 2
#
# # Spacing around the equals sign does not matter.
# # All of these are identical:
# key=value
# key= value
# key =value
# key = value
#
# # Any line beginning with a # is a comment. It's not possible to put
# # a comment after a config option, since it would be interpreted as a
# # part of the value. For example, this will have a value of "#123abc":
# background = #123abc
#
# # Empty values are used to reset config keys to default.
# key =
#
# # Some config options have unique syntaxes for their value,
# # which is explained in the docs for that config option.
# # Just for example:
# resize-overlay-duration = 4s 200ms

View File

@@ -1,31 +1,29 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const internal_os = @import("../os/main.zig");
/// Open the configuration in the OS default editor according to the default
/// paths the main config file could be in.
///
/// On Linux, this will open the file at the XDG config path. This is the
/// only valid path for Linux so we don't need to check for other paths.
///
/// On macOS, both XDG and AppSupport paths are valid. Because Ghostty
/// prioritizes AppSupport over XDG, we will open AppSupport if it exists,
/// followed by XDG if it exists, and finally AppSupport if neither exist.
/// For the existence check, we also prefer non-empty files over empty
/// files.
pub fn open(alloc_gpa: Allocator) !void {
// default location
const config_path = config_path: {
const xdg_config_path = try internal_os.xdg.config(alloc_gpa, .{ .subdir = "ghostty/config" });
// Use an arena to make memory management easier in here.
var arena = ArenaAllocator.init(alloc_gpa);
defer arena.deinit();
const alloc = arena.allocator();
if (comptime builtin.os.tag == .macos) macos: {
// On macOS, use the application support path if the XDG path doesn't exists.
if (std.fs.accessAbsolute(xdg_config_path, .{})) {
break :macos;
} else |err| switch (err) {
error.BadPathName, error.FileNotFound => {},
else => break :macos,
}
alloc_gpa.free(xdg_config_path);
break :config_path try internal_os.macos.appSupportDir(alloc_gpa, "config");
}
break :config_path xdg_config_path;
};
defer alloc_gpa.free(config_path);
// Get the path we should open
const config_path = try configPath(alloc);
// Create config directory recursively.
if (std.fs.path.dirname(config_path)) |config_dir| {
@@ -43,5 +41,67 @@ pub fn open(alloc_gpa: Allocator) !void {
}
};
try internal_os.open(alloc_gpa, config_path);
try internal_os.open(alloc, .text, config_path);
}
/// Returns the config path to use for open for the current OS.
///
/// The allocator must be an arena allocator. No memory is freed by this
/// function and the resulting path is not all the memory that is allocated.
fn configPath(alloc_arena: Allocator) ![]const u8 {
const paths: []const []const u8 = try configPathCandidates(alloc_arena);
assert(paths.len > 0);
// Find the first path that exists and is non-empty. If no paths are
// non-empty but at least one exists, we will return the first path that
// exists.
var exists: ?[]const u8 = null;
for (paths) |path| {
const f = std.fs.openFileAbsolute(path, .{}) catch |err| {
switch (err) {
// File doesn't exist, continue.
error.BadPathName, error.FileNotFound => continue,
// Some other error, assume it exists and return it.
else => return err,
}
};
defer f.close();
// We expect stat to succeed because we just opened the file.
const stat = try f.stat();
// If the file is non-empty, return it.
if (stat.size > 0) return path;
// If the file is empty, remember it exists.
if (exists == null) exists = path;
}
// No paths are non-empty, return the first path that exists.
if (exists) |v| return v;
// No paths are non-empty or exist, return the first path.
return paths[0];
}
/// Returns a const list of possible paths the main config file could be
/// in for the current OS.
fn configPathCandidates(alloc_arena: Allocator) ![]const []const u8 {
var paths = try std.ArrayList([]const u8).initCapacity(alloc_arena, 2);
errdefer paths.deinit();
if (comptime builtin.os.tag == .macos) {
paths.appendAssumeCapacity(try internal_os.macos.appSupportDir(
alloc_arena,
"config",
));
}
paths.appendAssumeCapacity(try internal_os.xdg.config(
alloc_arena,
.{ .subdir = "ghostty/config" },
));
return paths.items;
}

View File

@@ -81,7 +81,11 @@ fn initThread(gpa: Allocator) !void {
const alloc = arena.allocator();
const transport = sentry.Transport.init(&Transport.send);
errdefer transport.deinit();
// This will crash if the transport was never used so we avoid
// that for now. This probably leaks some memory but it'd be very
// small and a one time cost. Once this is fixed upstream we can
// remove this.
//errdefer transport.deinit();
const opts = sentry.c.sentry_options_new();
errdefer sentry.c.sentry_options_free(opts);

View File

@@ -34,3 +34,6 @@ pub const cozette = @embedFile("res/CozetteVector.ttf");
/// Monaspace has weird ligature behaviors we want to test in our shapers
/// so we embed it here.
pub const monaspace_neon = @embedFile("res/MonaspaceNeon-Regular.otf");
/// Terminus TTF is a scalable font with bitmap glyphs at various sizes.
pub const terminus_ttf = @embedFile("res/TerminusTTF-Regular.ttf");

View File

@@ -515,8 +515,17 @@ pub const Face = struct {
fn calcMetrics(ct_font: *macos.text.Font) CalcMetricsError!font.face.Metrics {
// Read the 'head' table out of the font data.
const head: opentype.Head = head: {
const tag = macos.text.FontTableTag.init("head");
const data = ct_font.copyTable(tag) orelse return error.CopyTableError;
// macOS bitmap-only fonts use a 'bhed' tag rather than 'head', but
// the table format is byte-identical to the 'head' table, so if we
// can't find 'head' we try 'bhed' instead before failing.
//
// ref: https://fontforge.org/docs/techref/bitmaponlysfnt.html
const head_tag = macos.text.FontTableTag.init("head");
const bhed_tag = macos.text.FontTableTag.init("bhed");
const data =
ct_font.copyTable(head_tag) orelse
ct_font.copyTable(bhed_tag) orelse
return error.CopyTableError;
defer data.release();
const ptr = data.getPointer();
const len = data.getLength();

View File

@@ -288,7 +288,6 @@ pub const Face = struct {
self.face.loadGlyph(glyph_id, .{
.render = true,
.color = self.face.hasColor(),
.no_bitmap = !self.face.hasColor(),
}) catch return false;
// If the glyph is SVG we assume colorized
@@ -323,14 +322,6 @@ pub const Face = struct {
// glyph properties before render so we don't render here.
.render = !self.synthetic.bold,
// Disable bitmap strikes for now since it causes issues with
// our cell metrics and rasterization. In the future, this is
// all fixable so we can enable it.
//
// This must be enabled for color faces though because those are
// often colored bitmaps, which we support.
.no_bitmap = !self.face.hasColor(),
// use options from config
.no_hinting = !self.load_flags.hinting,
.force_autohint = !self.load_flags.@"force-autohint",
@@ -385,7 +376,7 @@ pub const Face = struct {
return error.UnsupportedPixelMode;
};
log.warn("converting from pixel_mode={} to atlas_format={}", .{
log.debug("converting from pixel_mode={} to atlas_format={}", .{
bitmap_ft.pixel_mode,
atlas.format,
});
@@ -1005,3 +996,59 @@ test "svg font table" {
try testing.expectEqual(430, table.len);
}
const terminus_i =
\\........
\\........
\\...#....
\\...#....
\\........
\\..##....
\\...#....
\\...#....
\\...#....
\\...#....
\\...#....
\\..###...
\\........
\\........
\\........
\\........
;
// Including the newline
const terminus_i_pitch = 9;
test "bitmap glyph" {
const alloc = testing.allocator;
const testFont = font.embedded.terminus_ttf;
var lib = try Library.init();
defer lib.deinit();
var atlas = try font.Atlas.init(alloc, 512, .grayscale);
defer atlas.deinit(alloc);
// Any glyph at 12pt @ 96 DPI is a bitmap
var ft_font = try Face.init(lib, testFont, .{ .size = .{
.points = 12,
.xdpi = 96,
.ydpi = 96,
} });
defer ft_font.deinit();
// glyph 77 = 'i'
const glyph = try ft_font.renderGlyph(alloc, &atlas, 77, .{});
// should render crisp
try testing.expectEqual(8, glyph.width);
try testing.expectEqual(16, glyph.height);
for (0..glyph.height) |y| {
for (0..glyph.width) |x| {
const pixel = terminus_i[y * terminus_i_pitch + x];
try testing.expectEqual(
@as(u8, if (pixel == '#') 255 else 0),
atlas.data[(glyph.atlas_y + y) * atlas.size + (glyph.atlas_x + x)],
);
}
}
}

View File

@@ -43,26 +43,14 @@ pub fn monoToGrayscale(alloc: Allocator, bm: Bitmap) Allocator.Error!Bitmap {
var buf = try alloc.alloc(u8, bm.width * bm.rows);
errdefer alloc.free(buf);
// width divided by 8 because each byte has 8 pixels. This is therefore
// the number of bytes in each row.
const bytes_per_row = bm.width >> 3;
var source_i: usize = 0;
var target_i: usize = 0;
var i: usize = bm.rows;
while (i > 0) : (i -= 1) {
var j: usize = bytes_per_row;
while (j > 0) : (j -= 1) {
var bit: u4 = 8;
while (bit > 0) : (bit -= 1) {
const mask = @as(u8, 1) << @as(u3, @intCast(bit - 1));
const bitval: u8 = if (bm.buffer[source_i + (j - 1)] & mask > 0) 0xFF else 0;
buf[target_i] = bitval;
target_i += 1;
}
for (0..bm.rows) |y| {
const row_offset = y * @as(usize, @intCast(bm.pitch));
for (0..bm.width) |x| {
const byte_offset = row_offset + @divTrunc(x, 8);
const mask = @as(u8, 1) << @intCast(7 - (x % 8));
const bit: u8 = @intFromBool((bm.buffer[byte_offset] & mask) != 0);
buf[y * bm.width + x] = bit * 255;
}
source_i += @intCast(bm.pitch);
}
var copy = bm;

View File

@@ -25,6 +25,9 @@ This project uses several fonts which fall under the SIL Open Font License (OFL-
- [Copyright 2013 Google LLC](https://github.com/googlefonts/noto-emoji/blob/main/LICENSE)
- Cozette (MIT)
- [Copyright (c) 2020, Slavfox](https://github.com/slavfox/Cozette/blob/main/LICENSE)
- Terminus TTF (OFL-1.1)
- [Copyright (c) 2010-2020 Dimitar Toshkov Zhekov with Reserved Font Name "Terminus Font"](https://sourceforge.net/projects/terminus-font/)
- [Copyright (c) 2011-2023 Tilman Blumenbach with Reserved Font Name "Terminus (TTF)"](https://files.ax86.net/terminus-ttf/)
A full copy of the OFL license can be found at [OFL.txt](./OFL.txt).
An accompanying FAQ is also available at <https://openfontlicense.org/>.

Binary file not shown.

View File

@@ -127,7 +127,12 @@ pub const GlobalState = struct {
internal_os.fixMaxFiles();
// Initialize our crash reporting.
try crash.init(self.alloc);
crash.init(self.alloc) catch |err| {
std.log.warn(
"sentry init failed, no crash capture available err={}",
.{err},
);
};
// const sentrylib = @import("sentry");
// if (sentrylib.captureEvent(sentrylib.Value.initMessageEvent(

View File

@@ -380,10 +380,17 @@ pub const Action = union(enum) {
/// is preserved between appearances, so you can always press the keybinding
/// to bring it back up.
///
/// To enable the quick terminally globally so that Ghostty doesn't
/// have to be focused, prefix your keybind with `global`. Example:
///
/// ```ini
/// keybind = global:cmd+grave_accent=toggle_quick_terminal
/// ```
///
/// The quick terminal has some limitations:
///
/// - It is a singleton; only one instance can exist at a time.
/// - It does not support tabs.
/// - It does not support tabs, but it does support splits.
/// - It will not be restored when the application is restarted
/// (for systems that support window restoration).
/// - It supports fullscreen, but fullscreen will always be a non-native
@@ -393,6 +400,8 @@ pub const Action = union(enum) {
///
/// See the various configurations for the quick terminal in the
/// configuration file to customize its behavior.
///
/// This currently only works on macOS.
toggle_quick_terminal: void,
/// Show/hide all windows. If all windows become shown, we also ensure
@@ -1010,6 +1019,14 @@ pub const Trigger = struct {
const cp = it.nextCodepoint() orelse break :unicode;
if (it.nextCodepoint() != null) break :unicode;
// If this is ASCII and we have a translated key, set that.
if (std.math.cast(u8, cp)) |ascii| {
if (key.Key.fromASCII(ascii)) |k| {
result.key = .{ .translated = k };
continue :loop;
}
}
result.key = .{ .unicode = cp };
continue :loop;
}
@@ -1545,6 +1562,19 @@ test "parse: triggers" {
try parseSingle("a=ignore"),
);
// unicode keys that map to translated
try testing.expectEqual(Binding{
.trigger = .{ .key = .{ .translated = .one } },
.action = .{ .ignore = {} },
}, try parseSingle("1=ignore"));
try testing.expectEqual(Binding{
.trigger = .{
.mods = .{ .super = true },
.key = .{ .translated = .period },
},
.action = .{ .ignore = {} },
}, try parseSingle("cmd+.=ignore"));
// single modifier
try testing.expectEqual(Binding{
.trigger = .{

View File

@@ -308,7 +308,7 @@ pub const Key = enum(c_int) {
equal,
left_bracket, // [
right_bracket, // ]
backslash, // /
backslash, // \
// control
up,
@@ -729,7 +729,9 @@ pub const Key = enum(c_int) {
.{ '\t', .tab },
// Keypad entries. We just assume keypad with the kp_ prefix
// so that has some special meaning. These must also always be last.
// so that has some special meaning. These must also always be last,
// so that our `fromASCII` function doesn't accidentally map them
// over normal numerics and other keys.
.{ '0', .kp_0 },
.{ '1', .kp_1 },
.{ '2', .kp_2 },

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const posix = std.posix;
const log = std.log.scoped(.flatpak);
@@ -26,7 +27,7 @@ pub fn isFlatpak() bool {
///
/// Requires GIO, GLib to be available and linked.
pub const FlatpakHostCommand = struct {
const fd_t = std.os.fd_t;
const fd_t = posix.fd_t;
const EnvMap = std.process.EnvMap;
const c = @cImport({
@cInclude("gio/gio.h");

View File

@@ -24,7 +24,7 @@ pub const AppSupportDirError = Allocator.Error || error{AppleAPIFailed};
pub fn appSupportDir(
alloc: Allocator,
sub_path: []const u8,
) AppSupportDirError![]u8 {
) AppSupportDirError![]const u8 {
comptime assert(builtin.target.isDarwin());
const NSFileManager = objc.getClass("NSFileManager").?;

View File

@@ -36,9 +36,11 @@ pub const fixMaxFiles = file.fixMaxFiles;
pub const allocTmpDir = file.allocTmpDir;
pub const freeTmpDir = file.freeTmpDir;
pub const isFlatpak = flatpak.isFlatpak;
pub const FlatpakHostCommand = flatpak.FlatpakHostCommand;
pub const home = homedir.home;
pub const ensureLocale = locale.ensureLocale;
pub const clickInterval = mouse.clickInterval;
pub const open = openpkg.open;
pub const OpenType = openpkg.Type;
pub const pipe = pipepkg.pipe;
pub const resourcesDir = resourcesdir.resourcesDir;

View File

@@ -2,25 +2,50 @@ const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
/// The type of the data at the URL to open. This is used as a hint
/// to potentially open the URL in a different way.
pub const Type = enum {
text,
unknown,
};
/// Open a URL in the default handling application.
///
/// Any output on stderr is logged as a warning in the application logs.
/// Output on stdout is ignored.
pub fn open(alloc: Allocator, url: []const u8) !void {
// Some opener commands terminate after opening (macOS open) and some do not
// (xdg-open). For those which do not terminate, we do not want to wait for
// the process to exit to collect stderr.
const argv, const wait = switch (builtin.os.tag) {
.linux => .{ &.{ "xdg-open", url }, false },
.macos => .{ &.{ "open", url }, true },
.windows => .{ &.{ "rundll32", "url.dll,FileProtocolHandler", url }, false },
pub fn open(
alloc: Allocator,
typ: Type,
url: []const u8,
) !void {
const cmd: OpenCommand = switch (builtin.os.tag) {
.linux => .{ .child = std.process.Child.init(
&.{ "xdg-open", url },
alloc,
) },
.windows => .{ .child = std.process.Child.init(
&.{ "rundll32", "url.dll,FileProtocolHandler", url },
alloc,
) },
.macos => .{
.child = std.process.Child.init(
switch (typ) {
.text => &.{ "open", "-t", url },
.unknown => &.{ "open", url },
},
alloc,
),
.wait = true,
},
.ios => return error.Unimplemented,
else => @compileError("unsupported OS"),
};
var exe = std.process.Child.init(argv, alloc);
if (comptime wait) {
var exe = cmd.child;
if (cmd.wait) {
// Pipe stdout/stderr so we can collect output from the command
exe.stdout_behavior = .Pipe;
exe.stderr_behavior = .Pipe;
@@ -28,7 +53,7 @@ pub fn open(alloc: Allocator, url: []const u8) !void {
try exe.spawn();
if (comptime wait) {
if (cmd.wait) {
// 50 KiB is the default value used by std.process.Child.run
const output_max_size = 50 * 1024;
@@ -47,3 +72,8 @@ pub fn open(alloc: Allocator, url: []const u8) !void {
if (stderr.items.len > 0) std.log.err("open stderr={s}", .{stderr.items});
}
}
const OpenCommand = struct {
child: std.process.Child,
wait: bool = false,
};

View File

@@ -4,6 +4,7 @@ const internal_os = @import("main.zig");
const build_config = @import("../build_config.zig");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const posix = std.posix;
const log = std.log.scoped(.passwd);
@@ -88,13 +89,13 @@ pub fn get(alloc: Allocator) !Entry {
// Once started, we can close the child side. We do this after
// wait right now but that is fine too. This lets us read the
// parent and detect EOF.
_ = std.os.close(pty.slave);
_ = posix.close(pty.slave);
// Read all of our output
const output = output: {
var output: std.ArrayListUnmanaged(u8) = .{};
while (true) {
const n = std.os.read(pty.master, &buf) catch |err| {
const n = posix.read(pty.master, &buf) catch |err| {
switch (err) {
// EIO is triggered at the end since we closed our
// child side. This is just EOF for this. I'm not sure

View File

@@ -76,18 +76,30 @@ size: renderer.Size,
/// True if the window is focused
focused: bool,
/// The actual foreground color. May differ from the config foreground color if
/// changed by a terminal application
foreground_color: terminal.color.RGB,
/// The foreground color set by an OSC 10 sequence. If unset then
/// default_foreground_color is used.
foreground_color: ?terminal.color.RGB,
/// The actual background color. May differ from the config background color if
/// changed by a terminal application
background_color: terminal.color.RGB,
/// Foreground color set in the user's config file.
default_foreground_color: terminal.color.RGB,
/// The actual cursor color. May differ from the config cursor color if changed
/// by a terminal application
/// The background color set by an OSC 11 sequence. If unset then
/// default_background_color is used.
background_color: ?terminal.color.RGB,
/// Background color set in the user's config file.
default_background_color: terminal.color.RGB,
/// The cursor color set by an OSC 12 sequence. If unset then
/// default_cursor_color is used.
cursor_color: ?terminal.color.RGB,
/// Default cursor color when no color is set explicitly by an OSC 12 command.
/// This is cursor color as set in the user's config, if any. If no cursor color
/// is set in the user's config, then the cursor color is determined by the
/// current foreground color.
default_cursor_color: ?terminal.color.RGB,
/// When `cursor_color` is null, swap the foreground and background colors of
/// the cell under the cursor for the cursor color. Otherwise, use the default
/// foreground color as the cursor color.
@@ -629,9 +641,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
.grid_metrics = font_critical.metrics,
.size = options.size,
.focused = true,
.foreground_color = options.config.foreground,
.background_color = options.config.background,
.cursor_color = options.config.cursor_color,
.foreground_color = null,
.default_foreground_color = options.config.foreground,
.background_color = null,
.default_background_color = options.config.background,
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.cursor_invert = options.config.cursor_invert,
.current_background_color = options.config.background,
@@ -919,15 +934,34 @@ pub fn updateFrame(
}
// Swap bg/fg if the terminal is reversed
const bg = self.background_color;
const fg = self.foreground_color;
const bg = self.background_color orelse self.default_background_color;
const fg = self.foreground_color orelse self.default_foreground_color;
defer {
self.background_color = bg;
self.foreground_color = fg;
if (self.background_color) |*c| {
c.* = bg;
} else {
self.default_background_color = bg;
}
if (self.foreground_color) |*c| {
c.* = fg;
} else {
self.default_foreground_color = fg;
}
}
if (state.terminal.modes.get(.reverse_colors)) {
self.background_color = fg;
self.foreground_color = bg;
if (self.background_color) |*c| {
c.* = fg;
} else {
self.default_background_color = fg;
}
if (self.foreground_color) |*c| {
c.* = bg;
} else {
self.default_foreground_color = bg;
}
}
// If our terminal screen size doesn't match our expected renderer
@@ -1029,7 +1063,7 @@ pub fn updateFrame(
}
break :critical .{
.bg = self.background_color,
.bg = self.background_color orelse self.default_background_color,
.screen = screen_copy,
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
@@ -1186,9 +1220,9 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
attachment.setProperty("storeAction", @intFromEnum(mtl.MTLStoreAction.store));
attachment.setProperty("texture", screen_texture.value);
attachment.setProperty("clearColor", mtl.MTLClearColor{
.red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255,
.green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255,
.blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255,
.red = @as(f32, @floatFromInt(self.current_background_color.r)) / 255 * self.config.background_opacity,
.green = @as(f32, @floatFromInt(self.current_background_color.g)) / 255 * self.config.background_opacity,
.blue = @as(f32, @floatFromInt(self.current_background_color.b)) / 255 * self.config.background_opacity,
.alpha = self.config.background_opacity,
});
}
@@ -1957,10 +1991,10 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
self.uniforms.min_contrast = config.min_contrast;
// Set our new colors
self.background_color = config.background;
self.foreground_color = config.foreground;
self.default_background_color = config.background;
self.default_foreground_color = config.foreground;
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
self.cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.config.deinit();
self.config = config.*;
@@ -2246,12 +2280,12 @@ fn rebuildCells(
.extend => if (y == 0) {
self.uniforms.padding_extend.up = !row.neverExtendBg(
color_palette,
self.background_color,
self.background_color orelse self.default_background_color,
);
} else if (y == self.cells.size.rows - 1) {
self.uniforms.padding_extend.down = !row.neverExtendBg(
color_palette,
self.background_color,
self.background_color orelse self.default_background_color,
);
},
}
@@ -2360,7 +2394,7 @@ fn rebuildCells(
false;
const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color;
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
// The final background color for the cell.
const bg = bg: {
@@ -2380,7 +2414,7 @@ fn rebuildCells(
// If we don't have invert selection fg/bg set then we
// just use the selection background if set, otherwise
// the default fg color.
break :bg self.config.selection_background orelse self.foreground_color;
break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
}
// Not selected
@@ -2402,7 +2436,7 @@ fn rebuildCells(
// If we don't have invert selection fg/bg set
// then we just use the selection foreground if
// set, otherwise the default bg color.
break :fg self.config.selection_foreground orelse self.background_color;
break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
}
// Whether we need to use the bg color as our fg color:
@@ -2411,7 +2445,7 @@ fn rebuildCells(
// Note: if selected then invert sel fg / bg must be
// false since we separately handle it if true above.
break :fg if (style.flags.inverse != selected)
bg_style orelse self.background_color
bg_style orelse self.background_color orelse self.default_background_color
else
fg_style;
};
@@ -2438,7 +2472,7 @@ fn rebuildCells(
// If we have a background and its not the default background
// then we apply background opacity
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color)) {
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
break :bg_alpha default;
}
@@ -2601,12 +2635,12 @@ fn rebuildCells(
// Prepare the cursor cell contents.
const style = cursor_style_ orelse break :cursor;
const cursor_color = self.cursor_color orelse color: {
const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
if (self.cursor_invert) {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :color sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color;
break :color sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
} else {
break :color self.foreground_color;
break :color self.foreground_color orelse self.default_foreground_color;
}
};
@@ -2634,11 +2668,11 @@ fn rebuildCells(
const uniform_color = if (self.cursor_invert) blk: {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color;
break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
} else if (self.config.cursor_text) |txt|
txt
else
self.background_color;
self.background_color orelse self.default_background_color;
self.uniforms.cursor_color = .{
uniform_color.r,
@@ -2928,8 +2962,8 @@ fn addPreeditCell(
coord: terminal.Coordinate,
) !void {
// Preedit is rendered inverted
const bg = self.foreground_color;
const fg = self.background_color;
const bg = self.foreground_color orelse self.default_foreground_color;
const fg = self.background_color orelse self.default_background_color;
// Render the glyph for our preedit text
const render_ = self.font_grid.renderCodepoint(

View File

@@ -89,18 +89,30 @@ texture_color_resized: usize = 0,
/// True if the window is focused
focused: bool,
/// The actual foreground color. May differ from the config foreground color if
/// changed by a terminal application
foreground_color: terminal.color.RGB,
/// The foreground color set by an OSC 10 sequence. If unset then the default
/// value from the config file is used.
foreground_color: ?terminal.color.RGB,
/// The actual background color. May differ from the config background color if
/// changed by a terminal application
background_color: terminal.color.RGB,
/// Foreground color set in the user's config file.
default_foreground_color: terminal.color.RGB,
/// The actual cursor color. May differ from the config cursor color if changed
/// by a terminal application
/// The background color set by an OSC 11 sequence. If unset then the default
/// value from the config file is used.
background_color: ?terminal.color.RGB,
/// Background color set in the user's config file.
default_background_color: terminal.color.RGB,
/// The cursor color set by an OSC 12 sequence. If unset then
/// default_cursor_color is used.
cursor_color: ?terminal.color.RGB,
/// Default cursor color when no color is set explicitly by an OSC 12 command.
/// This is cursor color as set in the user's config, if any. If no cursor color
/// is set in the user's config, then the cursor color is determined by the
/// current foreground color.
default_cursor_color: ?terminal.color.RGB,
/// When `cursor_color` is null, swap the foreground and background colors of
/// the cell under the cursor for the cursor color. Otherwise, use the default
/// foreground color as the cursor color.
@@ -386,9 +398,12 @@ pub fn init(alloc: Allocator, options: renderer.Options) !OpenGL {
.font_shaper_cache = font.ShaperCache.init(),
.draw_background = options.config.background,
.focused = true,
.foreground_color = options.config.foreground,
.background_color = options.config.background,
.cursor_color = options.config.cursor_color,
.foreground_color = null,
.default_foreground_color = options.config.foreground,
.background_color = null,
.default_background_color = options.config.background,
.cursor_color = null,
.default_cursor_color = options.config.cursor_color,
.cursor_invert = options.config.cursor_invert,
.surface_mailbox = options.surface_mailbox,
.deferred_font_size = .{ .metrics = grid.metrics },
@@ -701,15 +716,34 @@ pub fn updateFrame(
}
// Swap bg/fg if the terminal is reversed
const bg = self.background_color;
const fg = self.foreground_color;
const bg = self.background_color orelse self.default_background_color;
const fg = self.foreground_color orelse self.default_foreground_color;
defer {
self.background_color = bg;
self.foreground_color = fg;
if (self.background_color) |*c| {
c.* = bg;
} else {
self.default_background_color = bg;
}
if (self.foreground_color) |*c| {
c.* = fg;
} else {
self.default_foreground_color = fg;
}
}
if (state.terminal.modes.get(.reverse_colors)) {
self.background_color = fg;
self.foreground_color = bg;
if (self.background_color) |*c| {
c.* = fg;
} else {
self.default_background_color = fg;
}
if (self.foreground_color) |*c| {
c.* = bg;
} else {
self.default_foreground_color = bg;
}
}
// If our terminal screen size doesn't match our expected renderer
@@ -820,7 +854,7 @@ pub fn updateFrame(
break :critical .{
.full_rebuild = full_rebuild,
.gl_bg = self.background_color,
.gl_bg = self.background_color orelse self.default_background_color,
.screen = screen_copy,
.screen_type = state.terminal.active_screen,
.mouse = state.mouse,
@@ -1298,12 +1332,12 @@ pub fn rebuildCells(
.extend => if (y == 0) {
self.padding_extend_top = !row.neverExtendBg(
color_palette,
self.background_color,
self.background_color orelse self.default_background_color,
);
} else if (y == self.size.grid().rows - 1) {
self.padding_extend_bottom = !row.neverExtendBg(
color_palette,
self.background_color,
self.background_color orelse self.default_background_color,
);
},
}
@@ -1412,7 +1446,7 @@ pub fn rebuildCells(
false;
const bg_style = style.bg(cell, color_palette);
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color;
const fg_style = style.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
// The final background color for the cell.
const bg = bg: {
@@ -1432,7 +1466,7 @@ pub fn rebuildCells(
// If we don't have invert selection fg/bg set then we
// just use the selection background if set, otherwise
// the default fg color.
break :bg self.config.selection_background orelse self.foreground_color;
break :bg self.config.selection_background orelse self.foreground_color orelse self.default_foreground_color;
}
// Not selected
@@ -1454,7 +1488,7 @@ pub fn rebuildCells(
// If we don't have invert selection fg/bg set
// then we just use the selection foreground if
// set, otherwise the default bg color.
break :fg self.config.selection_foreground orelse self.background_color;
break :fg self.config.selection_foreground orelse self.background_color orelse self.default_background_color;
}
// Whether we need to use the bg color as our fg color:
@@ -1463,7 +1497,7 @@ pub fn rebuildCells(
// Note: if selected then invert sel fg / bg must be
// false since we separately handle it if true above.
break :fg if (style.flags.inverse != selected)
bg_style orelse self.background_color
bg_style orelse self.background_color orelse self.default_background_color
else
fg_style;
};
@@ -1490,7 +1524,7 @@ pub fn rebuildCells(
// If we have a background and its not the default background
// then we apply background opacity
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color)) {
if (style.bg(cell, color_palette) != null and !rgb.eql(self.background_color orelse self.default_background_color)) {
break :bg_alpha default;
}
@@ -1699,12 +1733,12 @@ pub fn rebuildCells(
break :cursor_style;
}
const cursor_color = self.cursor_color orelse color: {
const cursor_color = self.cursor_color orelse self.default_cursor_color orelse color: {
if (self.cursor_invert) {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :color sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color;
break :color sty.fg(color_palette, self.config.bold_is_bright) orelse self.foreground_color orelse self.default_foreground_color;
} else {
break :color self.foreground_color;
break :color self.foreground_color orelse self.default_foreground_color;
}
};
@@ -1713,11 +1747,11 @@ pub fn rebuildCells(
if (cell.mode.isFg() and cell.mode != .fg_color) {
const cell_color = if (self.cursor_invert) blk: {
const sty = screen.cursor.page_pin.style(screen.cursor.page_cell);
break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color;
break :blk sty.bg(screen.cursor.page_cell, color_palette) orelse self.background_color orelse self.default_background_color;
} else if (self.config.cursor_text) |txt|
txt
else
self.background_color;
self.background_color orelse self.default_background_color;
cell.r = cell_color.r;
cell.g = cell_color.g;
@@ -1742,8 +1776,8 @@ fn addPreeditCell(
y: usize,
) !void {
// Preedit is rendered inverted
const bg = self.foreground_color;
const fg = self.background_color;
const bg = self.foreground_color orelse self.default_foreground_color;
const fg = self.background_color orelse self.default_background_color;
// Render the glyph for our preedit text
const render_ = self.font_grid.renderCodepoint(
@@ -2122,10 +2156,10 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
self.font_shaper_cache = font_shaper_cache;
// Set our new colors
self.background_color = config.background;
self.foreground_color = config.foreground;
self.default_background_color = config.background;
self.default_foreground_color = config.foreground;
self.default_cursor_color = if (!config.cursor_invert) config.cursor_color else null;
self.cursor_invert = config.cursor_invert;
self.cursor_color = if (!config.cursor_invert) config.cursor_color else null;
// Update our uniforms
self.deferred_config = .{};
@@ -2344,9 +2378,9 @@ fn drawCellProgram(
// Clear the surface
gl.clearColor(
@as(f32, @floatFromInt(self.draw_background.r)) / 255,
@as(f32, @floatFromInt(self.draw_background.g)) / 255,
@as(f32, @floatFromInt(self.draw_background.b)) / 255,
@floatCast(@as(f32, @floatFromInt(self.draw_background.r)) / 255 * self.config.background_opacity),
@floatCast(@as(f32, @floatFromInt(self.draw_background.g)) / 255 * self.config.background_opacity),
@floatCast(@as(f32, @floatFromInt(self.draw_background.b)) / 255 * self.config.background_opacity),
@floatCast(self.config.background_opacity),
);
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);

View File

@@ -42,13 +42,11 @@ pub const Message = union(enum) {
old_key: font.SharedGridSet.Key,
},
/// Change the foreground color. This can be done separately from changing
/// the config file in response to an OSC 10 command.
foreground_color: terminal.color.RGB,
/// Change the foreground color as set by an OSC 10 command, if any.
foreground_color: ?terminal.color.RGB,
/// Change the background color. This can be done separately from changing
/// the config file in response to an OSC 11 command.
background_color: terminal.color.RGB,
/// Change the background color as set by an OSC 11 command, if any.
background_color: ?terminal.color.RGB,
/// Change the cursor color. This can be done separately from changing the
/// config file in response to an OSC 12 command.

View File

@@ -73,6 +73,6 @@ sequence occurs.
```bash
if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
"$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
fi
```

View File

@@ -1,10 +1,6 @@
# This is originally based on the recommended bash integration from
# the semantic prompts proposal as well as some logic from Kitty's
# bash integration.
#
# I'm not a bash expert so this probably has some major issues but for
# my simple bash usage this is working. If a bash expert wants to
# improve this please do!
# We need to be in interactive mode and we need to have the Ghostty
# resources dir set which also tells us we're running in Ghostty.
@@ -72,6 +68,34 @@ if [ -n "$GHOSTTY_BASH_INJECT" ]; then
builtin unset ghostty_bash_inject rcfile
fi
# Sudo
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" && -n "$TERMINFO" ]]; then
# Wrap `sudo` command to ensure Ghostty terminfo is preserved.
#
# This approach supports wrapping a `sudo` alias, but the alias definition
# must come _after_ this function is defined. Otherwise, the alias expansion
# will take precedence over this function, and it won't be wrapped.
function sudo {
builtin local sudo_has_sudoedit_flags="no"
for arg in "$@"; do
# Check if argument is '-e' or '--edit' (sudoedit flags)
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
sudo_has_sudoedit_flags="yes"
builtin break
fi
# Check if argument is neither an option nor a key-value pair
if [[ "$arg" != -* && "$arg" != *=* ]]; then
builtin break
fi
done
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
builtin command sudo "$@";
else
builtin command sudo TERMINFO="$TERMINFO" "$@";
fi
}
fi
# Import bash-preexec, safe to do multiple times
builtin source "$GHOSTTY_RESOURCES_DIR/shell-integration/bash/bash-preexec.sh"
@@ -113,31 +137,6 @@ function __ghostty_precmd() {
PS0=$PS0'\[\e[0 q\]'
fi
# Sudo
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_SUDO" != "1" ]] && [[ -n "$TERMINFO" ]]; then
# Wrap `sudo` command to ensure Ghostty terminfo is preserved
# shellcheck disable=SC2317
sudo() {
builtin local sudo_has_sudoedit_flags="no"
for arg in "$@"; do
# Check if argument is '-e' or '--edit' (sudoedit flags)
if [[ "$arg" == "-e" || $arg == "--edit" ]]; then
sudo_has_sudoedit_flags="yes"
builtin break
fi
# Check if argument is neither an option nor a key-value pair
if [[ "$arg" != -* && "$arg" != *=* ]]; then
builtin break
fi
done
if [[ "$sudo_has_sudoedit_flags" == "yes" ]]; then
builtin command sudo "$@";
else
builtin command sudo TERMINFO="$TERMINFO" "$@";
fi
}
fi
if [[ "$GHOSTTY_SHELL_INTEGRATION_NO_TITLE" != 1 ]]; then
# Command and working directory
# shellcheck disable=SC2016

View File

@@ -117,7 +117,7 @@
set edit:before-readline = (conj $edit:before-readline $beam~)
set edit:after-readline = (conj $edit:after-readline {|_| block })
}
if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (eq file (type -t sudo))) {
if (and (not $no-sudo) (not-eq "" $E:TERMINFO) (has-external sudo)) {
edit:add-var sudo~ $sudo-with-terminfo~
}
}

View File

@@ -25,7 +25,7 @@
# Ghostty in all shells should add the following lines to their .zshrc:
#
# if [[ -n $GHOSTTY_RESOURCES_DIR ]]; then
# "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
# source "$GHOSTTY_RESOURCES_DIR"/shell-integration/zsh/ghostty-integration
# fi
#
# Implementation note: We can assume that alias expansion is disabled in this

View File

@@ -3413,6 +3413,16 @@ pub const Pin = struct {
direction: Direction,
limit: ?Pin,
) PageIterator {
if (build_config.slow_runtime_safety) {
if (limit) |l| {
// Check the order according to the iteration direction.
switch (direction) {
.right_down => assert(self.eql(l) or self.before(l)),
.left_up => assert(self.eql(l) or l.before(self)),
}
}
}
return .{
.row = self,
.limit = if (limit) |p| .{ .row = p } else .{ .none = {} },

View File

@@ -189,26 +189,39 @@ pub const Parser = struct {
.@"8_fg" = @enumFromInt(slice[0] - 30),
},
38 => if (slice.len >= 5 and slice[1] == 2) {
self.idx += 4;
38 => if (slice.len >= 2) switch (slice[1]) {
// `2` indicates direct-color (r, g, b).
// We need at least 3 more params for this to make sense.
2 => if (slice.len >= 5) {
self.idx += 4;
// When a colon separator is used, there may or may not be
// a color space identifier as the third param, which we
// need to ignore (it has no standardized behavior).
const rgb = if (slice.len == 5 or !self.colon)
slice[2..5]
else rgb: {
self.idx += 1;
break :rgb slice[3..6];
};
// In the 6-len form, ignore the 3rd param.
const rgb = slice[2..5];
// We use @truncate because the value should be 0 to 255. If
// it isn't, the behavior is undefined so we just... truncate it.
return Attribute{
.direct_color_fg = .{
.r = @truncate(rgb[0]),
.g = @truncate(rgb[1]),
.b = @truncate(rgb[2]),
},
};
} else if (slice.len >= 3 and slice[1] == 5) {
self.idx += 2;
return Attribute{
.@"256_fg" = @truncate(slice[2]),
};
// We use @truncate because the value should be 0 to 255. If
// it isn't, the behavior is undefined so we just... truncate it.
return Attribute{
.direct_color_fg = .{
.r = @truncate(rgb[0]),
.g = @truncate(rgb[1]),
.b = @truncate(rgb[2]),
},
};
},
// `5` indicates indexed color.
5 => if (slice.len >= 3) {
self.idx += 2;
return Attribute{
.@"256_fg" = @truncate(slice[2]),
};
},
else => {},
},
39 => return Attribute{ .reset_fg = {} },
@@ -217,26 +230,39 @@ pub const Parser = struct {
.@"8_bg" = @enumFromInt(slice[0] - 40),
},
48 => if (slice.len >= 5 and slice[1] == 2) {
self.idx += 4;
48 => if (slice.len >= 2) switch (slice[1]) {
// `2` indicates direct-color (r, g, b).
// We need at least 3 more params for this to make sense.
2 => if (slice.len >= 5) {
self.idx += 4;
// When a colon separator is used, there may or may not be
// a color space identifier as the third param, which we
// need to ignore (it has no standardized behavior).
const rgb = if (slice.len == 5 or !self.colon)
slice[2..5]
else rgb: {
self.idx += 1;
break :rgb slice[3..6];
};
// We only support the 5-len form.
const rgb = slice[2..5];
// We use @truncate because the value should be 0 to 255. If
// it isn't, the behavior is undefined so we just... truncate it.
return Attribute{
.direct_color_bg = .{
.r = @truncate(rgb[0]),
.g = @truncate(rgb[1]),
.b = @truncate(rgb[2]),
},
};
} else if (slice.len >= 3 and slice[1] == 5) {
self.idx += 2;
return Attribute{
.@"256_bg" = @truncate(slice[2]),
};
// We use @truncate because the value should be 0 to 255. If
// it isn't, the behavior is undefined so we just... truncate it.
return Attribute{
.direct_color_bg = .{
.r = @truncate(rgb[0]),
.g = @truncate(rgb[1]),
.b = @truncate(rgb[2]),
},
};
},
// `5` indicates indexed color.
5 => if (slice.len >= 3) {
self.idx += 2;
return Attribute{
.@"256_bg" = @truncate(slice[2]),
};
},
else => {},
},
49 => return Attribute{ .reset_bg = {} },
@@ -244,30 +270,39 @@ pub const Parser = struct {
53 => return Attribute{ .overline = {} },
55 => return Attribute{ .reset_overline = {} },
58 => if (slice.len >= 5 and slice[1] == 2) {
self.idx += 4;
58 => if (slice.len >= 2) switch (slice[1]) {
// `2` indicates direct-color (r, g, b).
// We need at least 3 more params for this to make sense.
2 => if (slice.len >= 5) {
self.idx += 4;
// When a colon separator is used, there may or may not be
// a color space identifier as the third param, which we
// need to ignore (it has no standardized behavior).
const rgb = if (slice.len == 5 or !self.colon)
slice[2..5]
else rgb: {
self.idx += 1;
break :rgb slice[3..6];
};
// In the 6-len form, ignore the 3rd param. Otherwise, use it.
const rgb = if (slice.len == 5) slice[2..5] else rgb: {
// Consume one more element
self.idx += 1;
break :rgb slice[3..6];
};
// We use @truncate because the value should be 0 to 255. If
// it isn't, the behavior is undefined so we just... truncate it.
return Attribute{
.underline_color = .{
.r = @truncate(rgb[0]),
.g = @truncate(rgb[1]),
.b = @truncate(rgb[2]),
},
};
} else if (slice.len >= 3 and slice[1] == 5) {
self.idx += 2;
return Attribute{
.@"256_underline_color" = @truncate(slice[2]),
};
// We use @truncate because the value should be 0 to 255. If
// it isn't, the behavior is undefined so we just... truncate it.
return Attribute{
.underline_color = .{
.r = @truncate(rgb[0]),
.g = @truncate(rgb[1]),
.b = @truncate(rgb[2]),
},
};
},
// `5` indicates indexed color.
5 => if (slice.len >= 3) {
self.idx += 2;
return Attribute{
.@"256_underline_color" = @truncate(slice[2]),
};
},
else => {},
},
59 => return Attribute{ .reset_underline_color = {} },
@@ -566,3 +601,59 @@ test "sgr: direct color bg missing color" {
var p: Parser = .{ .params = &[_]u16{ 48, 5 }, .colon = false };
while (p.next()) |_| {}
}
test "sgr: direct fg/bg/underline ignore optional color space" {
// These behaviors have been verified against xterm.
// Colon version should skip the optional color space identifier
{
// 3 8 : 2 : Pi : Pr : Pg : Pb
const v = testParseColon(&[_]u16{ 38, 2, 0, 1, 2, 3, 4 });
try testing.expect(v == .direct_color_fg);
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.r);
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.g);
try testing.expectEqual(@as(u8, 3), v.direct_color_fg.b);
}
{
// 4 8 : 2 : Pi : Pr : Pg : Pb
const v = testParseColon(&[_]u16{ 48, 2, 0, 1, 2, 3, 4 });
try testing.expect(v == .direct_color_bg);
try testing.expectEqual(@as(u8, 1), v.direct_color_bg.r);
try testing.expectEqual(@as(u8, 2), v.direct_color_bg.g);
try testing.expectEqual(@as(u8, 3), v.direct_color_bg.b);
}
{
// 5 8 : 2 : Pi : Pr : Pg : Pb
const v = testParseColon(&[_]u16{ 58, 2, 0, 1, 2, 3, 4 });
try testing.expect(v == .underline_color);
try testing.expectEqual(@as(u8, 1), v.underline_color.r);
try testing.expectEqual(@as(u8, 2), v.underline_color.g);
try testing.expectEqual(@as(u8, 3), v.underline_color.b);
}
// Semicolon version should not parse optional color space identifier
{
// 3 8 ; 2 ; Pr ; Pg ; Pb
const v = testParse(&[_]u16{ 38, 2, 0, 1, 2, 3, 4 });
try testing.expect(v == .direct_color_fg);
try testing.expectEqual(@as(u8, 0), v.direct_color_fg.r);
try testing.expectEqual(@as(u8, 1), v.direct_color_fg.g);
try testing.expectEqual(@as(u8, 2), v.direct_color_fg.b);
}
{
// 4 8 ; 2 ; Pr ; Pg ; Pb
const v = testParse(&[_]u16{ 48, 2, 0, 1, 2, 3, 4 });
try testing.expect(v == .direct_color_bg);
try testing.expectEqual(@as(u8, 0), v.direct_color_bg.r);
try testing.expectEqual(@as(u8, 1), v.direct_color_bg.g);
try testing.expectEqual(@as(u8, 2), v.direct_color_bg.b);
}
{
// 5 8 ; 2 ; Pr ; Pg ; Pb
const v = testParse(&[_]u16{ 58, 2, 0, 1, 2, 3, 4 });
try testing.expect(v == .underline_color);
try testing.expectEqual(@as(u8, 0), v.underline_color.r);
try testing.expectEqual(@as(u8, 1), v.underline_color.g);
try testing.expectEqual(@as(u8, 2), v.underline_color.b);
}
}

View File

@@ -1347,7 +1347,7 @@ pub const StreamHandler = struct {
.foreground => {
self.foreground_color = null;
_ = self.renderer_mailbox.push(.{
.foreground_color = self.default_foreground_color,
.foreground_color = self.foreground_color,
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
@@ -1358,7 +1358,7 @@ pub const StreamHandler = struct {
.background => {
self.background_color = null;
_ = self.renderer_mailbox.push(.{
.background_color = self.default_background_color,
.background_color = self.background_color,
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
@@ -1370,7 +1370,7 @@ pub const StreamHandler = struct {
self.cursor_color = null;
_ = self.renderer_mailbox.push(.{
.cursor_color = self.default_cursor_color,
.cursor_color = self.cursor_color,
}, .{ .forever = {} });
if (self.default_cursor_color) |color| {
@@ -1490,15 +1490,15 @@ pub const StreamHandler = struct {
const msg: renderer.Message = switch (special) {
.foreground => msg: {
self.foreground_color = null;
break :msg .{ .foreground_color = self.default_foreground_color };
break :msg .{ .foreground_color = self.foreground_color };
},
.background => msg: {
self.background_color = null;
break :msg .{ .background_color = self.default_background_color };
break :msg .{ .background_color = self.background_color };
},
.cursor => msg: {
self.cursor_color = null;
break :msg .{ .cursor_color = self.default_cursor_color };
break :msg .{ .cursor_color = self.cursor_color };
},
else => {
log.warn(