mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-07 11:58:19 +00:00
Compare commits
227 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3f7c3afaf9 | ||
![]() |
a857d56fb6 | ||
![]() |
df0620afe9 | ||
![]() |
4543cdeac8 | ||
![]() |
5ba8fee38a | ||
![]() |
47cf5cbb40 | ||
![]() |
cf34ffa28e | ||
![]() |
eaa872216b | ||
![]() |
d59a57e133 | ||
![]() |
a30b2eda39 | ||
![]() |
85ed9b626e | ||
![]() |
ecfca17ad6 | ||
![]() |
12a333dfb4 | ||
![]() |
783a06689e | ||
![]() |
789e2024a5 | ||
![]() |
d7c5017cd2 | ||
![]() |
413964774c | ||
![]() |
aa81c16ba1 | ||
![]() |
fa4d4a38c1 | ||
![]() |
f97f7e8a70 | ||
![]() |
478fe3917c | ||
![]() |
98d77788f4 | ||
![]() |
220d40e99a | ||
![]() |
d512f56005 | ||
![]() |
dd41a9447d | ||
![]() |
d54817607c | ||
![]() |
ffe1b7a872 | ||
![]() |
0da8801dc9 | ||
![]() |
9204bb888f | ||
![]() |
bdeb93fe87 | ||
![]() |
e9edd21bed | ||
![]() |
ef542c6e63 | ||
![]() |
41df2d9805 | ||
![]() |
d20446e4de | ||
![]() |
a6eec4cbe2 | ||
![]() |
7a4215abd7 | ||
![]() |
31f101c970 | ||
![]() |
2f6860fbc5 | ||
![]() |
f8c3dc1bbf | ||
![]() |
ade07c4c3c | ||
![]() |
68318e2830 | ||
![]() |
a15473d9bd | ||
![]() |
397bf41ec3 | ||
![]() |
a1f7a95763 | ||
![]() |
c62f64866c | ||
![]() |
716848cb58 | ||
![]() |
a7c93cdfb1 | ||
![]() |
e17bb69645 | ||
![]() |
318641f5a1 | ||
![]() |
26b1888494 | ||
![]() |
1a27ce0797 | ||
![]() |
adcaff7137 | ||
![]() |
c2c578789b | ||
![]() |
1c2532c184 | ||
![]() |
ff50b5539e | ||
![]() |
f2ac9b85e3 | ||
![]() |
011c17da41 | ||
![]() |
da186fb9df | ||
![]() |
0dc3ea35c0 | ||
![]() |
4e7982fc2b | ||
![]() |
4f2110bce0 | ||
![]() |
89e5ca2fb4 | ||
![]() |
d01b2397f1 | ||
![]() |
aed61b62ae | ||
![]() |
e9bc033b88 | ||
![]() |
c011c4622d | ||
![]() |
057dd3e209 | ||
![]() |
87bd0bb744 | ||
![]() |
cfeed2b7a2 | ||
![]() |
d2d6f8b9f4 | ||
![]() |
31c9a2fe59 | ||
![]() |
4d983a2083 | ||
![]() |
0fd65035c5 | ||
![]() |
3059d9036b | ||
![]() |
37c5f5a123 | ||
![]() |
35eefd4240 | ||
![]() |
8607b1f844 | ||
![]() |
a9c1a5c73c | ||
![]() |
06389b280a | ||
![]() |
cb96ce5dcc | ||
![]() |
936d0c0d58 | ||
![]() |
7256ebe13d | ||
![]() |
479e735e7f | ||
![]() |
b3290f6887 | ||
![]() |
d5703a57e7 | ||
![]() |
84a03aa202 | ||
![]() |
b3925b83ae | ||
![]() |
5698358c2f | ||
![]() |
ff6e60be56 | ||
![]() |
db0da9a273 | ||
![]() |
31ee48a355 | ||
![]() |
b7dba0c5f5 | ||
![]() |
3b8a0ed2b8 | ||
![]() |
9c15d8de35 | ||
![]() |
074edd3065 | ||
![]() |
27ddc2a9b2 | ||
![]() |
7881bb2bf0 | ||
![]() |
c20fe23946 | ||
![]() |
2e048c870c | ||
![]() |
329c87a2b4 | ||
![]() |
c8c3fb2900 | ||
![]() |
6508fec945 | ||
![]() |
b39850fd8b | ||
![]() |
64ea3a1a29 | ||
![]() |
65a1c0df80 | ||
![]() |
25a4a89ee3 | ||
![]() |
5be77ded3a | ||
![]() |
02538bed3c | ||
![]() |
a787e4b8fc | ||
![]() |
e24f33ae6b | ||
![]() |
64c393716a | ||
![]() |
2ab0376d80 | ||
![]() |
d359231e5c | ||
![]() |
16f81353cc | ||
![]() |
9bef43fd99 | ||
![]() |
56681741cb | ||
![]() |
c84fefc4ea | ||
![]() |
574407aacd | ||
![]() |
579de8e491 | ||
![]() |
6ca64bc410 | ||
![]() |
2b245c965c | ||
![]() |
a92ed15baa | ||
![]() |
b6e45d49a3 | ||
![]() |
5c2fb580d0 | ||
![]() |
5e14b8e501 | ||
![]() |
1a6d9590a2 | ||
![]() |
1ca25d3b5e | ||
![]() |
c8950d376a | ||
![]() |
28bbd526f2 | ||
![]() |
2993d12de7 | ||
![]() |
02ca5bedac | ||
![]() |
b50e087581 | ||
![]() |
85fc49b22c | ||
![]() |
9fc1e4e91a | ||
![]() |
a2c446cb73 | ||
![]() |
1783ec922d | ||
![]() |
b8a75c24a6 | ||
![]() |
ea8fe9a4b0 | ||
![]() |
bfde326bcb | ||
![]() |
fa83140585 | ||
![]() |
a671ca43f8 | ||
![]() |
5293fc9c2f | ||
![]() |
531a225b1e | ||
![]() |
a55bea3491 | ||
![]() |
351a7c03a5 | ||
![]() |
f95aa32965 | ||
![]() |
63dad2fb10 | ||
![]() |
c4ff873726 | ||
![]() |
ca7c954712 | ||
![]() |
b22417630b | ||
![]() |
54679ff30e | ||
![]() |
38643ec4fe | ||
![]() |
040ce0e7df | ||
![]() |
b00a4275f0 | ||
![]() |
3bccb4f05e | ||
![]() |
2a4e5e1c8e | ||
![]() |
a8261ec9f6 | ||
![]() |
1e937cf2f1 | ||
![]() |
5340aa6c96 | ||
![]() |
95ee6c1633 | ||
![]() |
e8a2950324 | ||
![]() |
2362a67f68 | ||
![]() |
e00233bebf | ||
![]() |
00d4610fa5 | ||
![]() |
8ecb11a602 | ||
![]() |
8cb13bcfad | ||
![]() |
4ed8306b02 | ||
![]() |
9632a2b956 | ||
![]() |
02b34f44f6 | ||
![]() |
034f79cfa2 | ||
![]() |
e8054c41f5 | ||
![]() |
0160f8a0d9 | ||
![]() |
6cbd69da78 | ||
![]() |
1fa2e699d1 | ||
![]() |
16e4529f69 | ||
![]() |
2555f09d88 | ||
![]() |
bee2188014 | ||
![]() |
bf46ab8d2d | ||
![]() |
8111f5b995 | ||
![]() |
590c2929e7 | ||
![]() |
27a5a50aa0 | ||
![]() |
c8dca6c9a8 | ||
![]() |
e2aee7f75b | ||
![]() |
f0b77e8972 | ||
![]() |
571d9b015d | ||
![]() |
66dbc48e6b | ||
![]() |
1622263519 | ||
![]() |
93b6bfdefe | ||
![]() |
96753aa7e0 | ||
![]() |
09470ede55 | ||
![]() |
6139cb00cf | ||
![]() |
7e4a69e7df | ||
![]() |
66ed72f486 | ||
![]() |
6f2a7d3c36 | ||
![]() |
4b3c1b031e | ||
![]() |
83987968ac | ||
![]() |
2998775152 | ||
![]() |
322f166ca5 | ||
![]() |
8335a31e45 | ||
![]() |
e07f813a50 | ||
![]() |
b9128aded5 | ||
![]() |
50f7632d81 | ||
![]() |
2114e0a613 | ||
![]() |
c2792a1811 | ||
![]() |
2c3847c9af | ||
![]() |
9db02fd152 | ||
![]() |
f7a461a85f | ||
![]() |
c320635c29 | ||
![]() |
a295c5e884 | ||
![]() |
600eea08cd | ||
![]() |
a8e5eef11c | ||
![]() |
73367a55f6 | ||
![]() |
b5f70b834b | ||
![]() |
869987277c | ||
![]() |
aef90ffff1 | ||
![]() |
729b7ba7ed | ||
![]() |
5411c001c8 | ||
![]() |
ebfa606c67 | ||
![]() |
7aced21a8e | ||
![]() |
eef9664ef8 | ||
![]() |
cb5fbc1041 | ||
![]() |
b2cb80dfbb | ||
![]() |
184db2654c | ||
![]() |
35b9ceee21 | ||
![]() |
6217dbebcf | ||
![]() |
c1f61b4344 | ||
![]() |
5867fe425f |
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal 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
9
.github/ISSUE_TEMPLATE/preapproved.md
vendored
Normal 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
32
.github/workflows/milestone.yml
vendored
Normal 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 }}
|
2
.github/workflows/release-pr.yml
vendored
2
.github/workflows/release-pr.yml
vendored
@@ -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:
|
||||
|
14
.github/workflows/release-tag.yml
vendored
14
.github/workflows/release-tag.yml
vendored
@@ -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
|
||||
|
3
.github/workflows/release-tip.yml
vendored
3
.github/workflows/release-tip.yml
vendored
@@ -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:
|
||||
|
43
.github/workflows/test.yml
vendored
43
.github/workflows/test.yml
vendored
@@ -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
|
||||
|
11
PACKAGING.md
11
PACKAGING.md
@@ -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:
|
||||
|
67
build.zig
67
build.zig
@@ -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(©_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(©_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");
|
||||
|
@@ -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",
|
||||
|
5
dist/linux/app.desktop
vendored
5
dist/linux/app.desktop
vendored
@@ -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
17
flake.lock
generated
@@ -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"
|
||||
|
13
flake.nix
13
flake.nix
@@ -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"];
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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).
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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";
|
||||
};
|
||||
})
|
||||
|
@@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-lS5v5VdFCLnIyCq9mp7fd2pXhQmkkDFHHVdg4pf37PA="
|
||||
"sha256-ot5onG1yq7EWQkNUgTNBuqvsnLuaoFs2UDS96IqgJmU="
|
||||
|
@@ -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;
|
||||
};
|
||||
}
|
||||
|
159
src/Surface.zig
159
src/Surface.zig
@@ -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,
|
||||
|
@@ -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(),
|
||||
|
@@ -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,
|
||||
|
@@ -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 =>
|
||||
|
@@ -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(>kMouseDown), 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 {
|
||||
|
@@ -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();
|
||||
|
@@ -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),
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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");
|
||||
|
@@ -47,6 +47,10 @@ const icons = [_]struct {
|
||||
.alias = "512x512",
|
||||
.source = "512",
|
||||
},
|
||||
.{
|
||||
.alias = "1024x1024",
|
||||
.source = "1024",
|
||||
},
|
||||
};
|
||||
|
||||
pub const gresource_xml = comptimeGenerateGResourceXML();
|
||||
|
@@ -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)),
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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| {
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
};
|
||||
|
@@ -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| {
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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");
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
},
|
||||
},
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
43
src/config/config-template
Normal file
43
src/config/config-template
Normal 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
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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");
|
||||
|
@@ -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();
|
||||
|
@@ -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)],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -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/>.
|
||||
|
BIN
src/font/res/TerminusTTF-Regular.ttf
Normal file
BIN
src/font/res/TerminusTTF-Regular.ttf
Normal file
Binary file not shown.
@@ -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(
|
||||
|
@@ -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 = .{
|
||||
|
@@ -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 },
|
||||
|
@@ -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");
|
||||
|
@@ -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").?;
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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);
|
||||
|
@@ -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.
|
||||
|
@@ -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
|
||||
```
|
||||
|
@@ -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
|
||||
|
@@ -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~
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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 = {} },
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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(
|
||||
|
Reference in New Issue
Block a user