Merge remote-tracking branch 'upstream/main' into jacob/uucode

pull/8757/head
Jacob Sandlund 2025-08-12 09:38:19 -04:00
commit 563cfb94ba
63 changed files with 5789 additions and 686 deletions

View File

@ -36,7 +36,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix

View File

@ -83,7 +83,7 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -279,7 +279,7 @@ jobs:
curl -sL https://sentry.io/get-cli/ | bash
- name: Download macOS Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: macos
@ -302,7 +302,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Download macOS Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: macos
@ -350,17 +350,17 @@ jobs:
GHOSTTY_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Download macOS Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: macos
- name: Download Sparkle Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: sparkle
- name: Download Source Tarball Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: source-tarball

View File

@ -107,7 +107,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix

View File

@ -70,7 +70,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -101,7 +101,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -137,7 +137,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -166,7 +166,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -199,7 +199,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -243,7 +243,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -406,7 +406,7 @@ jobs:
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
steps:
- name: Download Source Tarball Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: source-tarball
- name: Extract tarball
@ -414,7 +414,7 @@ jobs:
mkdir dist
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -509,7 +509,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -554,7 +554,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -603,7 +603,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -651,7 +651,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -706,7 +706,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -734,7 +734,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -761,7 +761,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -788,7 +788,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -815,7 +815,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -842,7 +842,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -876,7 +876,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -903,7 +903,7 @@ jobs:
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -938,7 +938,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix
@ -969,7 +969,7 @@ jobs:
uses: namespacelabs/nscloud-setup-buildx-action@01628ae51ea5d6b0c90109c7dccbf511953aff29 # v0.0.18
- name: Download Source Tarball Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: source-tarball
@ -996,7 +996,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix

View File

@ -22,7 +22,7 @@ jobs:
fetch-depth: 0
- name: Setup Cache
uses: namespacelabs/nscloud-cache-action@9ff6d4004df1c3fd97cecafe010c874d77c48599 # v1.2.13
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
with:
path: |
/nix

2
.gitignore vendored
View File

@ -21,3 +21,5 @@ glad.zip
/Box_test.ppm
/Box_test_diff.ppm
/ghostty.qcow2
vgcore.*

View File

@ -27,7 +27,7 @@ pub fn build(b: *std.Build) !void {
// Ghostty resources like terminfo, shell integration, themes, etc.
const resources = try buildpkg.GhosttyResources.init(b, &config);
const i18n = try buildpkg.GhosttyI18n.init(b, &config);
const i18n = if (config.i18n) try buildpkg.GhosttyI18n.init(b, &config) else null;
// Ghostty dependencies used by many artifacts.
const deps = try buildpkg.SharedDeps.init(b, &config);
@ -79,7 +79,7 @@ pub fn build(b: *std.Build) !void {
if (config.app_runtime != .none) {
exe.install();
resources.install();
i18n.install();
if (i18n) |v| v.install();
} else {
// Libghostty
//
@ -112,7 +112,7 @@ pub fn build(b: *std.Build) !void {
// The xcframework build always installs resources because our
// macOS xcode project contains references to them.
resources.install();
i18n.install();
if (i18n) |v| v.install();
}
// Ghostty macOS app
@ -122,7 +122,7 @@ pub fn build(b: *std.Build) !void {
.{
.xcframework = &xcframework,
.docs = &docs,
.i18n = &i18n,
.i18n = if (i18n) |v| &v else null,
.resources = &resources,
},
);
@ -166,7 +166,7 @@ pub fn build(b: *std.Build) !void {
.{
.xcframework = &xcframework_native,
.docs = &docs,
.i18n = &i18n,
.i18n = if (i18n) |v| &v else null,
.resources = &resources,
},
);
@ -204,5 +204,9 @@ pub fn build(b: *std.Build) !void {
// update-translations does what it sounds like and updates the "pot"
// files. These should be committed to the repo.
translations_step.dependOn(i18n.update_step);
if (i18n) |v| {
translations_step.dependOn(v.update_step);
} else {
try translations_step.addError("cannot update translations when i18n is disabled", .{});
}
}

View File

@ -63,8 +63,8 @@
.gobject = .{
// https://github.com/jcollie/ghostty-gobject based on zig_gobject
// Temporary until we generate them at build time automatically.
.url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst",
.hash = "gobject-0.3.0-Skun7EXXnAB96BrWabxhzOw7HY-NzVexaPOIYw5t-dIE",
.url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
.hash = "gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM",
.lazy = true,
},
@ -120,8 +120,8 @@
// Other
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
.iterm2_themes = .{
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
.hash = "N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv",
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
.hash = "N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu",
.lazy = true,
},
},

12
build.zig.zon.json generated
View File

@ -24,10 +24,10 @@
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
},
"gobject-0.3.0-Skun7EXXnAB96BrWabxhzOw7HY-NzVexaPOIYw5t-dIE": {
"gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM": {
"name": "gobject",
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst",
"hash": "sha256-ybeHo+NwcVZuyU037XB+/OofDoIoLsPnyNCG2jyiXC0="
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
"hash": "sha256-B0ziLzKud+kdKu5T1BTE9GMh8EPM/KhhhoNJlys5QPI="
},
"N-V-__8AALiNBAA-_0gprYr92CjrMj1I5bqNu0TSJOnjFNSr": {
"name": "gtk4_layer_shell",
@ -49,10 +49,10 @@
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
},
"N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv": {
"N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu": {
"name": "iterm2_themes",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
"hash": "sha256-w/biUQZ+AJv0atXypwQxJlKkHRUaFS0AlE/VlBJXlVU="
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
"hash": "sha256-gl42NOZ59ok+umHCHbdBQhWCgFVpj5PAZDVGhJRpbiA="
},
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
"name": "jetbrains_mono",

12
build.zig.zon.nix generated
View File

@ -122,11 +122,11 @@ in
};
}
{
name = "gobject-0.3.0-Skun7EXXnAB96BrWabxhzOw7HY-NzVexaPOIYw5t-dIE";
name = "gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM";
path = fetchZigArtifact {
name = "gobject";
url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst";
hash = "sha256-ybeHo+NwcVZuyU037XB+/OofDoIoLsPnyNCG2jyiXC0=";
url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst";
hash = "sha256-B0ziLzKud+kdKu5T1BTE9GMh8EPM/KhhhoNJlys5QPI=";
};
}
{
@ -162,11 +162,11 @@ in
};
}
{
name = "N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv";
name = "N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu";
path = fetchZigArtifact {
name = "iterm2_themes";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz";
hash = "sha256-w/biUQZ+AJv0atXypwQxJlKkHRUaFS0AlE/VlBJXlVU=";
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz";
hash = "sha256-gl42NOZ59ok+umHCHbdBQhWCgFVpj5PAZDVGhJRpbiA=";
};
}
{

4
build.zig.zon.txt generated
View File

@ -27,8 +27,8 @@ https://deps.files.ghostty.org/wuffs-122037b39d577ec2db3fd7b2130e7b69ef6cc1807d6
https://deps.files.ghostty.org/zig_js-12205a66d423259567764fa0fc60c82be35365c21aeb76c5a7dc99698401f4f6fefc.tar.gz
https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz

View File

@ -31,9 +31,9 @@
},
{
"type": "archive",
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-07-23-28-1/ghostty-gobject-0.14.1-2025-07-23-28-1.tar.zst",
"dest": "vendor/p/gobject-0.3.0-Skun7EXXnAB96BrWabxhzOw7HY-NzVexaPOIYw5t-dIE",
"sha256": "c9b787a3e37071566ec94d37ed707efcea1f0e82282ec3e7c8d086da3ca25c2d"
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.14.1-2025-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
"dest": "vendor/p/gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM",
"sha256": "074ce22f32ae77e91d2aee53d414c4f46321f043ccfca861868349972b3940f2"
},
{
"type": "archive",
@ -61,9 +61,9 @@
},
{
"type": "archive",
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/b2742b8baf86f556d6be4d9c6515bfd9d9c7a140.tar.gz",
"dest": "vendor/p/N-V-__8AAN83XASXgcKp4RDTj_WcQ19E5X24C3FjQoffeMFv",
"sha256": "c3f6e251067e009bf46ad5f2a704312652a41d151a152d00944fd59412579555"
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/3cbeca99efa10beba24b0efe86331736f09f9ed1.tar.gz",
"dest": "vendor/p/N-V-__8AABemXQQj_VhMpwuOSOiSzywW_yGD6aEL9YGI9uBu",
"sha256": "825e3634e679f6893eba61c21db7414215828055698f93c06435468494696e20"
},
{
"type": "archive",

View File

@ -14,7 +14,7 @@ struct TerminalEntity: AppEntity {
@Property(title: "Kind")
var kind: Kind
var screenshot: Image?
var screenshot: NSImage?
static var typeDisplayRepresentation: TypeDisplayRepresentation {
TypeDisplayRepresentation(name: "Terminal")
@ -24,8 +24,7 @@ struct TerminalEntity: AppEntity {
var displayRepresentation: DisplayRepresentation {
var rep = DisplayRepresentation(title: "\(title)")
if let screenshot,
let nsImage = ImageRenderer(content: screenshot).nsImage,
let data = nsImage.tiffRepresentation {
let data = screenshot.tiffRepresentation {
rep.image = .init(data: data)
}
@ -45,11 +44,14 @@ struct TerminalEntity: AppEntity {
static var defaultQuery = TerminalQuery()
@MainActor
init(_ view: Ghostty.SurfaceView) {
self.id = view.uuid
self.title = view.title
self.workingDirectory = view.pwd
self.screenshot = view.screenshot()
if let nsImage = ImageRenderer(content: view.screenshot()).nsImage {
self.screenshot = nsImage
}
// Determine the kind based on the window controller type
if view.window?.windowController is QuickTerminalController {

View File

@ -3,14 +3,15 @@
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
# This file is distributed under the same license as the com.mitchellh.ghostty package.
# Bartosz Sokorski <b.sokorski@gmail.com>, 2025.
# trag1c <dev@jakubr.me>, 2025.
#
msgid ""
msgstr ""
"Project-Id-Version: com.mitchellh.ghostty\n"
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
"PO-Revision-Date: 2025-03-17 12:15+0100\n"
"Last-Translator: Bartosz Sokorski <b.sokorski@gmail.com>\n"
"PO-Revision-Date: 2025-08-05 16:27+0200\n"
"Last-Translator: trag1c <dev@jakubr.me>\n"
"Language-Team: Polish <translation-team-pl@lists.sourceforge.net>\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
@ -89,7 +90,7 @@ msgstr "Podziel w prawo"
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
msgid "Execute a command…"
msgstr ""
msgstr "Wykonaj komendę…"
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
@ -162,7 +163,7 @@ msgstr "Otwórz konfigurację"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
msgid "Command Palette"
msgstr ""
msgstr "Paleta komend"
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
msgid "Terminal Inspector"
@ -210,12 +211,12 @@ msgstr "Zezwól"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
msgid "Remember choice for this split"
msgstr ""
msgstr "Zapamiętaj wybór dla tego podziału"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
msgid "Reload configuration to show this prompt again"
msgstr ""
msgstr "Przeładuj konfigurację, by ponownie wyświetlić ten komunikat"
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
@ -280,15 +281,15 @@ msgstr "Skopiowano do schowka"
#: src/apprt/gtk/Surface.zig:1268
msgid "Cleared clipboard"
msgstr ""
msgstr "Wyczyszczono schowek"
#: src/apprt/gtk/Surface.zig:2525
msgid "Command succeeded"
msgstr ""
msgstr "Komenda wykonana pomyślnie"
#: src/apprt/gtk/Surface.zig:2527
msgid "Command failed"
msgstr ""
msgstr "Komenda nie powiodła się"
#: src/apprt/gtk/Window.zig:216
msgid "Main Menu"
@ -300,7 +301,7 @@ msgstr "Zobacz otwarte karty"
#: src/apprt/gtk/Window.zig:266
msgid "New Split"
msgstr ""
msgstr "Nowy podział"
#: src/apprt/gtk/Window.zig:329
msgid ""

View File

@ -40,10 +40,13 @@ pub const blueprints: []const Blueprint = &.{
.{ .major = 1, .minor = 2, .name = "debug-warning" },
.{ .major = 1, .minor = 3, .name = "debug-warning" },
.{ .major = 1, .minor = 2, .name = "resize-overlay" },
.{ .major = 1, .minor = 5, .name = "split-tree" },
.{ .major = 1, .minor = 5, .name = "split-tree-split" },
.{ .major = 1, .minor = 2, .name = "surface" },
.{ .major = 1, .minor = 3, .name = "surface-child-exited" },
.{ .major = 1, .minor = 5, .name = "tab" },
.{ .major = 1, .minor = 5, .name = "window" },
.{ .major = 1, .minor = 5, .name = "command-palette" },
};
/// CSS files in css_path

View File

@ -5,6 +5,7 @@ const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const ext = @import("ext.zig");
pub const Application = @import("class/application.zig").Application;
pub const Window = @import("class/window.zig").Window;
pub const Config = @import("class/config.zig").Config;
@ -29,6 +30,12 @@ pub fn Common(
return @ptrCast(@alignCast(gobject.Object.ref(self.as(gobject.Object))));
}
/// If the reference count is 1 and the object is floating, clear the
/// floating attribute. Otherwise, increase the reference count by 1.
pub fn refSink(self: *Self) *Self {
return @ptrCast(@alignCast(gobject.Object.refSink(self.as(gobject.Object))));
}
/// Decrease the reference count of the object.
pub fn unref(self: *Self) void {
gobject.Object.unref(self.as(gobject.Object));
@ -73,7 +80,10 @@ pub fn Common(
fn set(self: *Self, value: *const gobject.Value) void {
const priv = private(self);
if (@field(priv, name)) |v| {
glib.ext.destroy(v);
ext.boxedFree(
@typeInfo(@TypeOf(v)).pointer.child,
v,
);
}
const T = @TypeOf(@field(priv, name));
@ -212,6 +222,11 @@ pub fn Common(
if (func_ti != .@"fn") {
@compileError("bound function must be a function pointer");
}
if (func_ti.@"fn".return_type == bool) {
// glib booleans are ints and returning a Zig bool type
// I think uses a byte and causes ABI issues.
@compileError("bound function must return c_int instead of bool");
}
}
gtk.Widget.Class.bindTemplateCallbackFull(

View File

@ -15,14 +15,17 @@ const apprt = @import("../../../apprt.zig");
const cgroup = @import("../cgroup.zig");
const CoreApp = @import("../../../App.zig");
const configpkg = @import("../../../config.zig");
const input = @import("../../../input.zig");
const internal_os = @import("../../../os/main.zig");
const systemd = @import("../../../os/systemd.zig");
const terminal = @import("../../../terminal/main.zig");
const xev = @import("../../../global.zig").xev;
const Binding = @import("../../../input.zig").Binding;
const CoreConfig = configpkg.Config;
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
const key = @import("../key.zig");
const adw_version = @import("../adw_version.zig");
const gtk_version = @import("../gtk_version.zig");
const winprotopkg = @import("../winproto.zig");
@ -31,9 +34,11 @@ const Common = @import("../class.zig").Common;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const Config = @import("config.zig").Config;
const Surface = @import("surface.zig").Surface;
const SplitTree = @import("split_tree.zig").SplitTree;
const Window = @import("window.zig").Window;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const ConfigErrorsDialog = @import("config_errors_dialog.zig").ConfigErrorsDialog;
const GlobalShortcuts = @import("global_shortcuts.zig").GlobalShortcuts;
const log = std.log.scoped(.gtk_ghostty_application);
@ -75,8 +80,6 @@ pub const Application = extern struct {
Self,
?*Config,
.{
.nick = "Config",
.blurb = "The current active configuration for the application.",
.accessor = gobject.ext.typedAccessor(
Self,
?*Config,
@ -105,6 +108,9 @@ pub const Application = extern struct {
/// State and logic for the underlying windowing protocol.
winproto: winprotopkg.App,
/// The global shortcut logic.
global_shortcuts: *GlobalShortcuts,
/// The base path of the transient cgroup used to put all surfaces
/// into their own cgroup. This is only set if cgroups are enabled
/// and initialization was successful.
@ -127,7 +133,7 @@ pub const Application = extern struct {
/// If non-null, we're currently showing a config errors dialog.
/// This is a WeakRef because the dialog can close on its own
/// outside of our own lifecycle and that's okay.
config_errors_dialog: WeakRef(ConfigErrorsDialog) = .{},
config_errors_dialog: WeakRef(ConfigErrorsDialog) = .empty,
/// glib source for our signal handler.
signal_source: ?c_uint = null,
@ -305,6 +311,7 @@ pub const Application = extern struct {
.winproto = wp,
.css_provider = css_provider,
.custom_css_providers = .empty,
.global_shortcuts = gobject.ext.newInstance(GlobalShortcuts, .{}),
};
// Signals
@ -332,6 +339,7 @@ pub const Application = extern struct {
const priv = self.private();
priv.config.unref();
priv.winproto.deinit(alloc);
priv.global_shortcuts.unref();
if (priv.transient_cgroup_base) |base| alloc.free(base);
if (gdk.Display.getDefault()) |display| {
gtk.StyleContext.removeProviderForDisplay(
@ -545,6 +553,10 @@ pub const Application = extern struct {
.desktop_notification => Action.desktopNotification(self, target, value),
.equalize_splits => return Action.equalizeSplits(target),
.goto_split => return Action.gotoSplit(target, value),
.goto_tab => return Action.gotoTab(target, value),
.initial_size => return Action.initialSize(target, value),
@ -555,6 +567,8 @@ pub const Application = extern struct {
.move_tab => return Action.moveTab(target, value),
.new_split => return Action.newSplit(target, value),
.new_tab => return Action.newTab(target),
.new_window => try Action.newWindow(
@ -598,16 +612,13 @@ pub const Application = extern struct {
.toggle_quick_terminal => return Action.toggleQuickTerminal(self),
.toggle_tab_overview => return Action.toggleTabOverview(target),
.toggle_window_decorations => return Action.toggleWindowDecorations(target),
.toggle_command_palette => return Action.toggleCommandPalette(target),
// Unimplemented but todo on gtk-ng branch
.prompt_title,
.toggle_command_palette,
.inspector,
// TODO: splits
.new_split,
.resize_split,
.equalize_splits,
.goto_split,
.toggle_split_zoom,
=> {
log.warn("unimplemented action={}", .{action});
@ -735,7 +746,7 @@ pub const Application = extern struct {
if (config.@"split-divider-color") |color| {
try writer.print(
\\.terminal-window .notebook separator {{
\\.window .split paned > separator {{
\\ color: rgb({[r]d},{[g]d},{[b]d});
\\ background: rgb({[r]d},{[g]d},{[b]d});
\\}}
@ -854,6 +865,65 @@ pub const Application = extern struct {
}
}
fn syncActionAccelerators(self: *Self) void {
self.syncActionAccelerator("app.quit", .{ .quit = {} });
self.syncActionAccelerator("app.open-config", .{ .open_config = {} });
self.syncActionAccelerator("app.reload-config", .{ .reload_config = {} });
self.syncActionAccelerator("win.toggle-inspector", .{ .inspector = .toggle });
self.syncActionAccelerator("app.show-gtk-inspector", .show_gtk_inspector);
self.syncActionAccelerator("win.toggle-command-palette", .toggle_command_palette);
self.syncActionAccelerator("win.close", .{ .close_window = {} });
self.syncActionAccelerator("win.new-window", .{ .new_window = {} });
self.syncActionAccelerator("win.new-tab", .{ .new_tab = {} });
self.syncActionAccelerator("win.close-tab", .{ .close_tab = {} });
self.syncActionAccelerator("win.split-right", .{ .new_split = .right });
self.syncActionAccelerator("win.split-down", .{ .new_split = .down });
self.syncActionAccelerator("win.split-left", .{ .new_split = .left });
self.syncActionAccelerator("win.split-up", .{ .new_split = .up });
self.syncActionAccelerator("win.copy", .{ .copy_to_clipboard = {} });
self.syncActionAccelerator("win.paste", .{ .paste_from_clipboard = {} });
self.syncActionAccelerator("win.reset", .{ .reset = {} });
self.syncActionAccelerator("win.clear", .{ .clear_screen = {} });
self.syncActionAccelerator("win.prompt-title", .{ .prompt_surface_title = {} });
self.syncActionAccelerator("split-tree.new-left", .{ .new_split = .left });
self.syncActionAccelerator("split-tree.new-right", .{ .new_split = .right });
self.syncActionAccelerator("split-tree.new-up", .{ .new_split = .up });
self.syncActionAccelerator("split-tree.new-down", .{ .new_split = .down });
}
fn syncActionAccelerator(
self: *Self,
gtk_action: [:0]const u8,
action: input.Binding.Action,
) void {
const gtk_app = self.as(gtk.Application);
// Reset it initially
const zero = [_:null]?[*:0]const u8{};
gtk_app.setAccelsForAction(gtk_action, &zero);
const config = self.private().config.get();
const trigger = config.keybind.set.getTrigger(action) orelse return;
var buf: [1024]u8 = undefined;
const accel = if (key.accelFromTrigger(
&buf,
trigger,
)) |accel_|
accel_ orelse return
else |err| switch (err) {
// This should really never, never happen. Its not critical enough
// to actually crash, but this is a bug somewhere. An accelerator
// for a trigger can't possibly be more than 1024 bytes.
error.NoSpaceLeft => {
log.warn("accelerator somehow longer than 1024 bytes: {}", .{trigger});
return;
},
};
const accels = [_:null]?[*:0]const u8{accel};
gtk_app.setAccelsForAction(gtk_action, &accels);
}
//---------------------------------------------------------------
// Properties
@ -884,6 +954,9 @@ pub const Application = extern struct {
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
// Sync our accelerators for menu items.
self.syncActionAccelerators();
// Load our runtime and custom CSS. If this fails then our window is
// just stuck with the old CSS but we don't want to fail the entire
// config change operation.
@ -912,7 +985,7 @@ pub const Application = extern struct {
//---------------------------------------------------------------
// Virtual Methods
fn startup(self: *Self) callconv(.C) void {
fn startup(self: *Self) callconv(.c) void {
log.debug("startup", .{});
gio.Application.virtual_methods.startup.call(
@ -935,6 +1008,9 @@ pub const Application = extern struct {
// Setup our action map
self.startupActionMap();
// Setup our global shortcuts
self.startupGlobalShortcuts();
// Setup our cgroup for the application.
self.startupCgroup() catch |err| {
log.warn("cgroup initialization failed err={}", .{err});
@ -1073,6 +1149,34 @@ pub const Application = extern struct {
}
}
/// Setup our global shortcuts.
fn startupGlobalShortcuts(self: *Self) void {
const priv = self.private();
// On startup, our dbus connection should be available.
priv.global_shortcuts.setDbusConnection(
self.as(gio.Application).getDbusConnection(),
);
// Setup a binding so that the shortcut config always matches the app.
_ = gobject.Object.bindProperty(
self.as(gobject.Object),
"config",
priv.global_shortcuts.as(gobject.Object),
"config",
.{ .sync_create = true },
);
// Setup the signal handler for global shortcut triggers
_ = GlobalShortcuts.signals.trigger.connect(
priv.global_shortcuts,
*Application,
globalShortcutTrigger,
self,
.{},
);
}
const CgroupError = error{
DbusConnectionFailed,
CgroupInitFailed,
@ -1139,7 +1243,7 @@ pub const Application = extern struct {
priv.transient_cgroup_base = path;
}
fn activate(self: *Self) callconv(.C) void {
fn activate(self: *Self) callconv(.c) void {
log.debug("activate", .{});
// Queue a new window
@ -1155,12 +1259,13 @@ pub const Application = extern struct {
);
}
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config_errors_dialog.get()) |diag| {
diag.close();
diag.unref(); // strong ref from get()
}
priv.config_errors_dialog.set(null);
if (priv.signal_source) |v| {
if (glib.Source.remove(v) == 0) {
log.warn("unable to remove signal source", .{});
@ -1174,7 +1279,7 @@ pub const Application = extern struct {
);
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
self.deinit();
gobject.Object.virtual_methods.finalize.call(
Class.parent,
@ -1303,6 +1408,16 @@ pub const Application = extern struct {
dialog.present(null);
}
fn globalShortcutTrigger(
_: *GlobalShortcuts,
action: *const Binding.Action,
self: *Self,
) callconv(.c) void {
self.core().performAllAction(self.rt(), action.*) catch |err| {
log.warn("failed to perform action={}", .{err});
};
}
fn actionReloadConfig(
_: *gio.SimpleAction,
_: ?*glib.Variant,
@ -1437,7 +1552,7 @@ pub const Application = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
// Register our compiled resources exactly once.
{
const c = @cImport({
@ -1538,6 +1653,52 @@ const Action = struct {
gio_app.sendNotification(n.body, notification);
}
pub fn equalizeSplits(target: apprt.Target) bool {
switch (target) {
.app => {
log.warn("equalize splits to app is unexpected", .{});
return false;
},
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction("split-tree.equalize", null) != 0;
},
}
}
pub fn gotoSplit(
target: apprt.Target,
to: apprt.action.GotoSplit,
) bool {
switch (target) {
.app => return false,
.surface => |core| {
// Design note: we can't use widget actions here because
// we need to know whether there is a goto target for returning
// the proper perform result (boolean).
const surface = core.rt_surface.surface;
const tree = ext.getAncestor(
SplitTree,
surface.as(gtk.Widget),
) orelse {
log.warn("surface is not in a split tree, ignoring goto_split", .{});
return false;
};
return tree.goto(switch (to) {
.previous => .previous_wrapped,
.next => .next_wrapped,
.up => .{ .spatial = .up },
.down => .{ .spatial = .down },
.left => .{ .spatial = .left },
.right => .{ .spatial = .right },
});
},
}
}
pub fn gotoTab(
target: apprt.Target,
tab: apprt.action.GotoTab,
@ -1587,16 +1748,9 @@ const Action = struct {
) void {
switch (target) {
.app => log.warn("mouse over link to app is unexpected", .{}),
.surface => |surface| {
var v = gobject.ext.Value.new([:0]const u8);
if (value.url.len > 0) gobject.ext.Value.set(&v, value.url);
defer v.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"mouse-hover-url",
&v,
);
},
.surface => |surface| surface.rt_surface.gobj().setMouseHoverUrl(
if (value.url.len > 0) value.url else null,
),
}
}
@ -1606,15 +1760,7 @@ const Action = struct {
) void {
switch (target) {
.app => log.warn("mouse shape to app is unexpected", .{}),
.surface => |surface| {
var value = gobject.ext.Value.newFrom(shape);
defer value.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"mouse-shape",
&value,
);
},
.surface => |surface| surface.rt_surface.gobj().setMouseShape(shape),
}
}
@ -1624,18 +1770,10 @@ const Action = struct {
) void {
switch (target) {
.app => log.warn("mouse visibility to app is unexpected", .{}),
.surface => |surface| {
var value = gobject.ext.Value.newFrom(switch (visibility) {
.surface => |surface| surface.rt_surface.gobj().setMouseHidden(switch (visibility) {
.visible => false,
.hidden => true,
});
defer value.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"mouse-hidden",
&value,
);
},
}),
}
}
@ -1663,6 +1801,28 @@ const Action = struct {
}
}
pub fn newSplit(
target: apprt.Target,
direction: apprt.action.SplitDirection,
) bool {
switch (target) {
.app => {
log.warn("new split to app is unexpected", .{});
return false;
},
.surface => |core| {
const surface = core.rt_surface.surface;
return surface.as(gtk.Widget).activateAction(switch (direction) {
.right => "split-tree.new-right",
.left => "split-tree.new-left",
.down => "split-tree.new-down",
.up => "split-tree.new-up",
}, null) != 0;
},
}
}
pub fn newTab(target: apprt.Target) bool {
switch (target) {
.app => {
@ -1756,15 +1916,7 @@ const Action = struct {
) void {
switch (target) {
.app => log.warn("pwd to app is unexpected", .{}),
.surface => |surface| {
var v = gobject.ext.Value.newFrom(value.pwd);
defer v.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"pwd",
&v,
);
},
.surface => |surface| surface.rt_surface.gobj().setPwd(value.pwd),
}
}
@ -1864,15 +2016,7 @@ const Action = struct {
) void {
switch (target) {
.app => log.warn("set_title to app is unexpected", .{}),
.surface => |surface| {
var v = gobject.ext.Value.newFrom(value.title);
defer v.unset();
gobject.Object.setProperty(
surface.rt_surface.gobj().as(gobject.Object),
"title",
&v,
);
},
.surface => |surface| surface.rt_surface.gobj().setTitle(value.title),
}
}
@ -2003,6 +2147,15 @@ const Action = struct {
},
}
}
pub fn toggleCommandPalette(target: apprt.Target) bool {
switch (target) {
.app => return false,
.surface => |surface| {
return surface.rt_surface.gobj().toggleCommandPalette();
},
}
}
};
/// This sets various GTK-related environment variables as necessary

View File

@ -37,8 +37,6 @@ pub const ClipboardConfirmationDialog = extern struct {
Self,
bool,
.{
.nick = "Can Remember",
.blurb = "Allow remembering the choice.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -57,8 +55,6 @@ pub const ClipboardConfirmationDialog = extern struct {
Self,
?*apprt.ClipboardRequest,
.{
.nick = "Request",
.blurb = "The clipboard request.",
.accessor = C.privateBoxedFieldAccessor("request"),
},
);
@ -71,8 +67,6 @@ pub const ClipboardConfirmationDialog = extern struct {
Self,
?*gtk.TextBuffer,
.{
.nick = "Clipboard Contents",
.blurb = "The clipboard contents being read/written.",
.accessor = C.privateObjFieldAccessor("clipboard_contents"),
},
);
@ -85,8 +79,6 @@ pub const ClipboardConfirmationDialog = extern struct {
Self,
bool,
.{
.nick = "Blur",
.blurb = "Blur the contents, allowing the user to reveal.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -150,7 +142,7 @@ pub const ClipboardConfirmationDialog = extern struct {
return gobject.ext.newInstance(Self, .{});
}
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Trigger initial values
@ -239,7 +231,7 @@ pub const ClipboardConfirmationDialog = extern struct {
fn response(
self: *Self,
response_id: [*:0]const u8,
) callconv(.C) void {
) callconv(.c) void {
const remember: bool = if (comptime can_remember) remember: {
const priv = self.private();
break :remember priv.remember_choice.getActive() != 0;
@ -262,7 +254,7 @@ pub const ClipboardConfirmationDialog = extern struct {
}
}
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.clipboard_contents) |v| {
v.unref();
@ -280,7 +272,7 @@ pub const ClipboardConfirmationDialog = extern struct {
);
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.request) |v| {
glib.ext.destroy(v);
@ -304,7 +296,7 @@ pub const ClipboardConfirmationDialog = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
if (comptime adw_version.atLeast(1, 4, 0))

View File

@ -32,8 +32,6 @@ pub const CloseConfirmationDialog = extern struct {
Self,
Target,
.{
.nick = "Target",
.blurb = "The target for this close confirmation.",
.default = .app,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -81,7 +79,7 @@ pub const CloseConfirmationDialog = extern struct {
});
}
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
@ -102,7 +100,7 @@ pub const CloseConfirmationDialog = extern struct {
fn response(
self: *Self,
response_id: [*:0]const u8,
) callconv(.C) void {
) callconv(.c) void {
if (std.mem.orderZ(u8, response_id, "close") == .eq) {
signals.@"close-request".impl.emit(
self,
@ -120,7 +118,7 @@ pub const CloseConfirmationDialog = extern struct {
}
}
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
@ -135,6 +133,7 @@ pub const CloseConfirmationDialog = extern struct {
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
@ -143,7 +142,7 @@ pub const CloseConfirmationDialog = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(Dialog);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
@ -181,12 +180,14 @@ pub const Target = enum(c_int) {
app,
tab,
window,
surface,
pub fn title(self: Target) [*:0]const u8 {
return switch (self) {
.app => i18n._("Quit Ghostty?"),
.tab => i18n._("Close Tab?"),
.window => i18n._("Close Window?"),
.surface => i18n._("Close Split?"),
};
}
@ -195,6 +196,7 @@ pub const Target = enum(c_int) {
.app => i18n._("All terminal sessions will be terminated."),
.tab => i18n._("All terminal sessions in this tab will be terminated."),
.window => i18n._("All terminal sessions in this window will be terminated."),
.surface => i18n._("The currently running process in this split will be terminated."),
};
}

View File

@ -0,0 +1,568 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const adw = @import("adw");
const gio = @import("gio");
const gobject = @import("gobject");
const gtk = @import("gtk");
const input = @import("../../../input.zig");
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Window = @import("window.zig").Window;
const Config = @import("config.zig").Config;
const log = std.log.scoped(.gtk_ghostty_command_palette);
pub const CommandPalette = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = adw.Bin;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyCommandPalette",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Config,
.{
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
};
pub const signals = struct {
/// Emitted when a command from the command palette is activated. The
/// action contains pointers to allocated data so if a receiver of this
/// signal needs to keep the action around it will need to clone the
/// action or there may be use-after-free errors.
pub const trigger = struct {
pub const name = "trigger";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{*const input.Binding.Action},
void,
);
};
};
const Private = struct {
/// The configuration that this command palette is using.
config: ?*Config = null,
/// The dialog object containing the palette UI.
dialog: *adw.Dialog,
/// The search input text field.
search: *gtk.SearchEntry,
/// The view containing each result row.
view: *gtk.ListView,
/// The model that provides filtered data for the view to display.
model: *gtk.SingleSelection,
/// The list that serves as the data source of the model.
/// This is where all command data is ultimately stored.
source: *gio.ListStore,
pub var offset: c_int = 0;
};
/// Create a new instance of the command palette. The caller will own a
/// reference to the object.
pub fn new() *Self {
const self = gobject.ext.newInstance(Self, .{});
// Sink ourselves so that we aren't floating anymore. We'll unref
// ourselves when the palette is closed or an action is activated.
_ = self.refSink();
// Bump the ref so that the caller has a reference.
return self.ref();
}
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// Listen for any changes to our config.
_ = gobject.Object.signals.notify.connect(
self,
?*anyopaque,
propConfig,
null,
.{
.detail = "config",
},
);
}
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
priv.source.removeAll();
if (priv.config) |config| {
config.unref();
priv.config = null;
}
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
);
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
// Signal Handlers
fn propConfig(self: *CommandPalette, _: *gobject.ParamSpec, _: ?*anyopaque) callconv(.c) void {
const priv = self.private();
const config = priv.config orelse {
log.warn("command palette does not have a config!", .{});
return;
};
const cfg = config.get();
// Clear existing binds
priv.source.removeAll();
for (cfg.@"command-palette-entry".value.items) |command| {
// Filter out actions that are not implemented or don't make sense
// for GTK.
switch (command.action) {
.close_all_windows,
.toggle_secure_input,
.check_for_updates,
.redo,
.undo,
.reset_window_size,
.toggle_window_float_on_top,
=> continue,
else => {},
}
const cmd = Command.new(config, command);
const cmd_ref = cmd.as(gobject.Object);
priv.source.append(cmd_ref);
cmd_ref.unref();
}
}
fn close(self: *CommandPalette) void {
const priv = self.private();
_ = priv.dialog.close();
}
fn dialogClosed(_: *adw.Dialog, self: *CommandPalette) callconv(.c) void {
self.unref();
}
fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
// ESC was pressed - close the palette
self.close();
}
fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
// If Enter is pressed, activate the selected entry
const priv = self.private();
self.activated(priv.model.getSelected());
}
fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
self.activated(pos);
}
//---------------------------------------------------------------
/// Show or hide the command palette dialog. If the dialog is shown it will
/// be modal over the given window.
pub fn toggle(self: *CommandPalette, window: *Window) void {
const priv = self.private();
// If the dialog has been shown, close it.
if (priv.dialog.as(gtk.Widget).getRealized() != 0) {
self.close();
return;
}
// Show the dialog
priv.dialog.present(window.as(gtk.Widget));
// Focus on the search bar when opening the dialog
_ = priv.search.as(gtk.Widget).grabFocus();
}
/// Helper function to send a signal containing the action that should be
/// performed.
fn activated(self: *CommandPalette, pos: c_uint) void {
const priv = self.private();
// Use priv.model and not priv.source here to use the list of *visible* results
const object_ = priv.model.as(gio.ListModel).getObject(pos);
defer if (object_) |object| object.unref();
// Close before running the action in order to avoid being replaced by
// another dialog (such as the change title dialog). If that occurs then
// the command palette dialog won't be counted as having closed properly
// and cannot receive focus when reopened.
self.close();
const cmd = gobject.ext.cast(Command, object_ orelse return) orelse return;
const action = cmd.getAction() orelse return;
// Signal that an an action has been selected. Signals are synchronous
// so we shouldn't need to worry about cloning the action.
signals.trigger.impl.emit(
self,
null,
.{&action},
null,
);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(Command);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
.major = 1,
.minor = 5,
.name = "command-palette",
}),
);
// Bindings
class.bindTemplateChildPrivate("dialog", .{});
class.bindTemplateChildPrivate("search", .{});
class.bindTemplateChildPrivate("view", .{});
class.bindTemplateChildPrivate("model", .{});
class.bindTemplateChildPrivate("source", .{});
// Template Callbacks
class.bindTemplateCallback("closed", &dialogClosed);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("search_stopped", &searchStopped);
class.bindTemplateCallback("search_activated", &searchActivated);
class.bindTemplateCallback("row_activated", &rowActivated);
// Properties
gobject.ext.registerProperties(class, &.{
properties.config.impl,
});
// Signals
signals.trigger.impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
pub const bindTemplateCallback = C.Class.bindTemplateCallback;
};
};
/// Object that wraps around a command.
///
/// As GTK list models only accept objects that are within the GObject hierarchy,
/// we have to construct a wrapper to be easily consumed by the list model.
const Command = extern struct {
pub const Self = @This();
pub const Parent = gobject.Object;
parent: Parent,
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyCommand",
.instanceInit = &init,
.classInit = Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
const properties = struct {
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Config,
.{
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
pub const action_key = struct {
pub const name = "action-key";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = gobject.ext.typedAccessor(
Self,
?[:0]const u8,
.{
.getter = propGetActionKey,
.getter_transfer = .none,
},
),
},
);
};
pub const action = struct {
pub const name = "action";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = gobject.ext.typedAccessor(
Self,
?[:0]const u8,
.{
.getter = propGetAction,
.getter_transfer = .none,
},
),
},
);
};
pub const title = struct {
pub const name = "title";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = gobject.ext.typedAccessor(
Self,
?[:0]const u8,
.{
.getter = propGetTitle,
.getter_transfer = .none,
},
),
},
);
};
pub const description = struct {
pub const name = "description";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = gobject.ext.typedAccessor(
Self,
?[:0]const u8,
.{
.getter = propGetDescription,
.getter_transfer = .none,
},
),
},
);
};
};
pub const Private = struct {
/// The configuration we should use to get keybindings.
config: ?*Config = null,
/// Arena used to manage our allocations.
arena: ArenaAllocator,
/// The command.
command: ?input.Command = null,
/// Cache the formatted action.
action: ?[:0]const u8 = null,
/// Cache the formatted action_key.
action_key: ?[:0]const u8 = null,
pub var offset: c_int = 0;
};
pub fn new(config: *Config, command: input.Command) *Self {
const self = gobject.ext.newInstance(Self, .{
.config = config,
});
const priv = self.private();
priv.command = command.clone(priv.arena.allocator()) catch null;
return self;
}
fn init(self: *Self, _: *Class) callconv(.c) void {
// NOTE: we do not watch for changes to the config here as the command
// palette will destroy and recreate this object if/when the config
// changes.
const priv = self.private();
priv.arena = .init(Application.default().allocator());
}
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config) |config| {
config.unref();
priv.config = null;
}
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
priv.arena.deinit();
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
//---------------------------------------------------------------
fn propGetActionKey(self: *Self) ?[:0]const u8 {
const priv = self.private();
if (priv.action_key) |action_key| return action_key;
const command = priv.command orelse return null;
priv.action_key = std.fmt.allocPrintZ(
priv.arena.allocator(),
"{}",
.{command.action},
) catch null;
return priv.action_key;
}
fn propGetAction(self: *Self) ?[:0]const u8 {
const priv = self.private();
if (priv.action) |action| return action;
const command = priv.command orelse return null;
const cfg = if (priv.config) |config| config.get() else return null;
const keybinds = cfg.keybind.set;
const alloc = priv.arena.allocator();
priv.action = action: {
var buf: [64]u8 = undefined;
const trigger = keybinds.getTrigger(command.action) orelse break :action null;
const accel = (key.accelFromTrigger(&buf, trigger) catch break :action null) orelse break :action null;
break :action alloc.dupeZ(u8, accel) catch return null;
};
return priv.action;
}
fn propGetTitle(self: *Self) ?[:0]const u8 {
const priv = self.private();
const command = priv.command orelse return null;
return command.title;
}
fn propGetDescription(self: *Self) ?[:0]const u8 {
const priv = self.private();
const command = priv.command orelse return null;
return command.description;
}
//---------------------------------------------------------------
/// Return a copy of the action. Callers must ensure that they do not use
/// the action beyond the lifetime of this object because it has internally
/// allocated data that will be freed when this object is.
pub fn getAction(self: *Self) ?input.Binding.Action {
const priv = self.private();
const command = priv.command orelse return null;
return command.action;
}
//---------------------------------------------------------------
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
gobject.ext.registerProperties(class, &.{
properties.config.impl,
properties.action_key.impl,
properties.action.impl,
properties.title.impl,
properties.description.impl,
});
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
};
};

View File

@ -39,8 +39,6 @@ pub const Config = extern struct {
Self,
?*gtk.TextBuffer,
.{
.nick = "Diagnostics Buffer",
.blurb = "A TextBuffer that contains the diagnostics.",
.accessor = gobject.ext.typedAccessor(
Self,
?*gtk.TextBuffer,
@ -57,8 +55,6 @@ pub const Config = extern struct {
Self,
bool,
.{
.nick = "has-diagnostics",
.blurb = "Whether the configuration has diagnostics.",
.default = false,
.accessor = gobject.ext.typedAccessor(
Self,
@ -139,7 +135,7 @@ pub const Config = extern struct {
return text_buf;
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
self.private().config.deinit();
gobject.Object.virtual_methods.finalize.call(
@ -159,7 +155,7 @@ pub const Config = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
gobject.ext.registerProperties(class, &.{
properties.@"diagnostics-buffer",

View File

@ -29,8 +29,6 @@ pub const ConfigErrorsDialog = extern struct {
Self,
?*Config,
.{
.nick = "config",
.blurb = "The configuration that this dialog is showing errors for.",
.accessor = gobject.ext.typedAccessor(
Self,
?*Config,
@ -67,7 +65,7 @@ pub const ConfigErrorsDialog = extern struct {
});
}
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
@ -82,7 +80,7 @@ pub const ConfigErrorsDialog = extern struct {
fn response(
self: *Self,
response_id: [*:0]const u8,
) callconv(.C) void {
) callconv(.c) void {
if (std.mem.orderZ(u8, response_id, "reload") != .eq) return;
signals.@"reload-config".impl.emit(
self,
@ -92,7 +90,7 @@ pub const ConfigErrorsDialog = extern struct {
);
}
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config) |v| {
v.unref();
@ -134,7 +132,7 @@ pub const ConfigErrorsDialog = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(Dialog);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),

View File

@ -82,7 +82,7 @@ pub const Dialog = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
_ = class;
}

View File

@ -0,0 +1,632 @@
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const adw = @import("adw");
const gio = @import("gio");
const glib = @import("glib");
const gobject = @import("gobject");
const gtk = @import("gtk");
const Binding = @import("../../../input.zig").Binding;
const gresource = @import("../build/gresource.zig");
const key = @import("../key.zig");
const Common = @import("../class.zig").Common;
const Application = @import("application.zig").Application;
const Config = @import("config.zig").Config;
const log = std.log.scoped(.gtk_ghostty_global_shortcuts);
pub const GlobalShortcuts = extern struct {
const Self = @This();
parent_instance: Parent,
pub const Parent = gobject.Object;
pub const getGObjectType = gobject.ext.defineClass(Self, .{
.name = "GhosttyGlobalShortcuts",
.instanceInit = &init,
.classInit = &Class.init,
.parent_class = &Class.parent,
.private = .{ .Type = Private, .offset = &Private.offset },
});
pub const properties = struct {
pub const config = struct {
pub const name = "config";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Config,
.{
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
pub const @"dbus-connection" = struct {
pub const name = "dbus-connection";
const impl = gobject.ext.defineProperty(
name,
Self,
?*gio.DBusConnection,
.{
.accessor = C.privateObjFieldAccessor("dbus_connection"),
},
);
};
};
const Private = struct {
/// The configuration that this is using.
config: ?*Config = null,
/// The dbus connection.
dbus_connection: ?*gio.DBusConnection = null,
/// An arena allocator that is present for each refresh.
arena: ?std.heap.ArenaAllocator = null,
/// A mapping from a unique ID to an action.
/// Currently the unique ID is simply the serialized representation of the
/// trigger that was used for the action as triggers are unique in the keymap,
/// but this may change in the future.
map: std.StringArrayHashMapUnmanaged(Binding.Action) = .{},
/// The handle of the current global shortcuts portal session,
/// as a D-Bus object path.
handle: ?[:0]const u8 = null,
/// The D-Bus signal subscription for the response signal on requests.
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
response_subscription: c_uint = 0,
/// The D-Bus signal subscription for the keybind activate signal.
/// The ID is guaranteed to be non-zero, so we can use 0 to indicate null.
activate_subscription: c_uint = 0,
pub var offset: c_int = 0;
};
pub const signals = struct {
/// Emitted whenever a global shortcut is triggered.
pub const trigger = struct {
pub const name = "trigger";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{*const Binding.Action},
void,
);
};
};
fn init(self: *Self, _: *Class) callconv(.c) void {
_ = gobject.Object.signals.notify.connect(
self,
*Self,
propConfig,
self,
.{ .detail = "config" },
);
}
fn close(self: *Self) void {
const priv = self.private();
const dbus = priv.dbus_connection orelse return;
if (priv.response_subscription != 0) {
dbus.signalUnsubscribe(priv.response_subscription);
priv.response_subscription = 0;
}
if (priv.activate_subscription != 0) {
dbus.signalUnsubscribe(priv.activate_subscription);
priv.activate_subscription = 0;
}
if (priv.handle) |handle| {
// Close existing session
dbus.call(
"org.freedesktop.portal.Desktop",
handle,
"org.freedesktop.portal.Session",
"Close",
null,
null,
.{},
-1,
null,
null,
null,
);
priv.handle = null;
}
if (priv.arena) |*arena| {
arena.deinit();
priv.arena = null;
priv.map = .{}; // Uses arena memory
}
}
fn refresh(self: *Self) Allocator.Error!void {
// Always close our previous state first.
self.close();
const priv = self.private();
// We need configuration to proceed.
const config = if (priv.config) |v| v.get() else return;
// Setup our new arena that we'll use for memory allocations.
assert(priv.arena == null);
var arena: std.heap.ArenaAllocator = .init(Application.default().allocator());
errdefer arena.deinit();
const alloc = arena.allocator();
// Our map starts out empty again. We don't need to worry about
// memory because its part of the arena we clear.
priv.map = .{};
errdefer priv.map = .{};
// Update map
var trigger_buf: [1024]u8 = undefined;
var it = config.keybind.set.bindings.iterator();
while (it.next()) |entry| {
const leaf = switch (entry.value_ptr.*) {
// Global shortcuts can't have leaders
.leader => continue,
.leaf => |leaf| leaf,
};
if (!leaf.flags.global) continue;
const trigger = if (key.xdgShortcutFromTrigger(
&trigger_buf,
entry.key_ptr.*,
)) |shortcut_|
shortcut_ orelse continue
else |err| switch (err) {
// If there isn't space to translate the trigger, then our
// buffer might be too small (but 1024 is insane!). In any case
// we don't want to stop registering globals.
error.NoSpaceLeft => {
log.warn(
"buffer too small to translate trigger, ignoring={}",
.{entry.key_ptr.*},
);
continue;
},
};
try priv.map.put(
alloc,
try alloc.dupeZ(u8, trigger),
leaf.action,
);
}
// Store our arena
priv.arena = arena;
// Create our session if we have global shortcuts.
if (priv.map.count() > 0) try self.request(.create_session);
}
const Method = enum {
create_session,
bind_shortcuts,
fn name(self: Method) [:0]const u8 {
return switch (self) {
.create_session => "CreateSession",
.bind_shortcuts => "BindShortcuts",
};
}
/// Construct the payload expected by the XDG portal call.
fn makePayload(
self: Method,
shortcuts: *GlobalShortcuts,
request_token: [:0]const u8,
) ?*glib.Variant {
switch (self) {
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-createsession
.create_session => {
var session_token: Token = undefined;
return glib.Variant.newParsed(
"({'handle_token': <%s>, 'session_handle_token': <%s>},)",
request_token.ptr,
generateToken(&session_token).ptr,
);
},
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-bindshortcuts
.bind_shortcuts => {
const priv = shortcuts.private();
const handle = priv.handle orelse return null;
const bind_type = glib.VariantType.new("a(sa{sv})");
defer glib.free(bind_type);
var binds: glib.VariantBuilder = undefined;
glib.VariantBuilder.init(&binds, bind_type);
var action_buf: [256]u8 = undefined;
var it = priv.map.iterator();
while (it.next()) |entry| {
const trigger = entry.key_ptr.*.ptr;
const action = std.fmt.bufPrintZ(
&action_buf,
"{}",
.{entry.value_ptr.*},
) catch continue;
binds.addParsed(
"(%s, {'description': <%s>, 'preferred_trigger': <%s>})",
trigger,
action.ptr,
trigger,
);
}
return glib.Variant.newParsed(
"(%o, %*, '', {'handle_token': <%s>})",
handle.ptr,
binds.end(),
request_token.ptr,
);
},
}
}
fn onResponse(self: Method, shortcuts: *GlobalShortcuts, vardict: *glib.Variant) void {
switch (self) {
.create_session => {
var handle: ?[*:0]u8 = null;
if (vardict.lookup("session_handle", "&s", &handle) == 0) {
log.warn(
"session handle not found in response={s}",
.{vardict.print(@intFromBool(true))},
);
return;
}
const priv = shortcuts.private();
const dbus = priv.dbus_connection.?;
const alloc = priv.arena.?.allocator();
priv.handle = alloc.dupeZ(u8, std.mem.span(handle.?)) catch {
log.warn("out of memory: failed to clone session handle", .{});
return;
};
log.debug("session_handle={?s}", .{handle});
// Subscribe to keybind activations
assert(priv.activate_subscription == 0);
priv.activate_subscription = dbus.signalSubscribe(
null,
"org.freedesktop.portal.GlobalShortcuts",
"Activated",
"/org/freedesktop/portal/desktop",
handle,
.{ .match_arg0_path = true },
shortcutActivated,
shortcuts,
null,
);
shortcuts.request(.bind_shortcuts) catch |err| {
log.warn("failed to bind shortcuts={}", .{err});
return;
};
},
.bind_shortcuts => {},
}
}
};
/// Submit a request to the global shortcuts portal.
fn request(
self: *Self,
comptime method: Method,
) Allocator.Error!void {
// NOTE(pluiedev):
// XDG Portals are really, really poorly-designed pieces of hot garbage.
// How the protocol is _initially_ designed to work is as follows:
//
// 1. The client calls a method which returns the path of a Request object;
// 2. The client waits for the Response signal under said object path;
// 3. When the signal arrives, the actual return value and status code
// become available for the client for further processing.
//
// THIS DOES NOT WORK. Once the first two steps are complete, the client
// needs to immediately start listening for the third step, but an overeager
// server implementation could easily send the Response signal before the
// client is even ready, causing communications to break down over a simple
// race condition/two generals' problem that even _TCP_ had figured out
// decades ago. Worse yet, you get exactly _one_ chance to listen for the
// signal, or else your communication attempt so far has all been in vain.
//
// And they know this. Instead of fixing their freaking protocol, they just
// ask clients to manually construct the expected object path and subscribe
// to the request signal beforehand, making the whole response value of
// the original call COMPLETELY MEANINGLESS.
//
// Furthermore, this is _entirely undocumented_ aside from one tiny
// paragraph under the documentation for the Request interface, and
// anyone would be forgiven for missing it without reading the libportal
// source code.
//
// When in Rome, do as the Romans do, I guess...?
const callbacks = struct {
fn gotResponseHandle(
source: ?*gobject.Object,
res: *gio.AsyncResult,
_: ?*anyopaque,
) callconv(.c) void {
const dbus_ = gobject.ext.cast(gio.DBusConnection, source.?).?;
var err: ?*glib.Error = null;
defer if (err) |err_| err_.free();
const params_ = dbus_.callFinish(res, &err) orelse {
if (err) |err_| log.warn("request failed={s} ({})", .{
err_.f_message orelse "(unknown)",
err_.f_code,
});
return;
};
defer params_.unref();
// TODO: XDG recommends updating the signal subscription if the actual
// returned request path is not the same as the expected request
// path, to retain compatibility with older versions of XDG portals.
// Although it suffers from the race condition outlined above,
// we should still implement this at some point.
}
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html#org-freedesktop-portal-request-response
fn responded(
dbus: *gio.DBusConnection,
_: ?[*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
params_: *glib.Variant,
ud: ?*anyopaque,
) callconv(.c) void {
const self_cb: *GlobalShortcuts = @ptrCast(@alignCast(ud));
const priv = self_cb.private();
// Unsubscribe from the response signal
if (priv.response_subscription != 0) {
dbus.signalUnsubscribe(priv.response_subscription);
priv.response_subscription = 0;
}
var response: u32 = 0;
var vardict: ?*glib.Variant = null;
defer if (vardict) |v| v.unref();
params_.get("(u@a{sv})", &response, &vardict);
switch (response) {
0 => {
log.debug("request successful", .{});
method.onResponse(self_cb, vardict.?);
},
1 => log.debug("request was cancelled by user", .{}),
2 => log.warn("request ended unexpectedly", .{}),
else => log.warn("unrecognized response code={}", .{response}),
}
}
};
var request_token_buf: Token = undefined;
const request_token = generateToken(&request_token_buf);
const payload = method.makePayload(self, request_token) orelse return;
const request_path = try self.getRequestPath(request_token);
const priv = self.private();
const dbus = priv.dbus_connection.?;
assert(priv.response_subscription == 0);
priv.response_subscription = dbus.signalSubscribe(
null,
"org.freedesktop.portal.Request",
"Response",
request_path,
null,
.{},
callbacks.responded,
self,
null,
);
dbus.call(
"org.freedesktop.portal.Desktop",
"/org/freedesktop/portal/desktop",
"org.freedesktop.portal.GlobalShortcuts",
method.name(),
payload,
null,
.{},
-1,
null,
callbacks.gotResponseHandle,
null,
);
}
/// Get the XDG portal request path for the current Ghostty instance.
///
/// If this sounds like nonsense, see `request` for an explanation as to
/// why we need to do this.
///
/// Precondition: dbus connection exists, arena setup
fn getRequestPath(self: *Self, token: [:0]const u8) Allocator.Error![:0]const u8 {
const priv = self.private();
const dbus = priv.dbus_connection.?;
const alloc = priv.arena.?.allocator();
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Request.html
// for the syntax XDG portals expect.
// `getUniqueName` should never return null here as we're using an ordinary
// message bus connection. If it doesn't, something is very wrong
const unique_name = std.mem.span(dbus.getUniqueName().?);
const object_path = try std.mem.joinZ(
alloc,
"/",
&.{
"/org/freedesktop/portal/desktop/request",
unique_name[1..], // Remove leading `:`
token,
},
);
// Sanitize the unique name by replacing every `.` with `_`.
// In effect, this will turn a unique name like `:1.192` into `1_192`.
// Valid D-Bus object path components never contain `.`s anyway, so we're
// free to replace all instances of `.` here and avoid extra allocation.
std.mem.replaceScalar(u8, object_path, '.', '_');
return object_path;
}
//---------------------------------------------------------------
// Property Handlers
pub fn setDbusConnection(
self: *Self,
dbus_connection: ?*gio.DBusConnection,
) void {
const priv = self.private();
// If we have a prior dbus connection we need to close our prior
// registrations first.
if (priv.dbus_connection) |v| {
self.close();
v.unref();
priv.dbus_connection = null;
}
priv.dbus_connection = null;
if (dbus_connection) |v| {
v.ref(); // Weird this doesn't return self
priv.dbus_connection = v;
self.refresh() catch |err| {
log.warn("error refreshing global shortcuts: {}", .{err});
};
}
self.as(gobject.Object).notifyByPspec(properties.@"dbus-connection".impl.param_spec);
}
fn propConfig(
_: *Self,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
self.refresh() catch |err| {
log.warn("error refreshing global shortcuts: {}", .{err});
};
}
//---------------------------------------------------------------
// Signal Handlers
fn shortcutActivated(
_: *gio.DBusConnection,
_: ?[*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
_: [*:0]const u8,
params: *glib.Variant,
ud: ?*anyopaque,
) callconv(.c) void {
const self: *Self = @ptrCast(@alignCast(ud));
// 2nd value in the tuple is the activated shortcut ID
// See https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.GlobalShortcuts.html#org-freedesktop-portal-globalshortcuts-activated
var shortcut_id: [*:0]const u8 = undefined;
params.getChild(1, "&s", &shortcut_id);
log.debug("activated={s}", .{shortcut_id});
const action = self.private().map.get(std.mem.span(shortcut_id)) orelse return;
signals.trigger.impl.emit(
self,
null,
.{&action},
null,
);
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.c) void {
// Since we drop references here we may lose access to things like
// dbus connections, so we need to close all our connections right
// away instead of in finalize.
self.close();
const priv = self.private();
if (priv.config) |v| {
v.unref();
priv.config = null;
}
if (priv.dbus_connection) |v| {
v.unref();
priv.dbus_connection = null;
}
gobject.Object.virtual_methods.dispose.call(
Class.parent,
self.as(Parent),
);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const unref = C.unref;
const private = C.private;
pub const Class = extern struct {
parent_class: Parent.Class,
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.c) void {
// Properties
gobject.ext.registerProperties(class, &.{
properties.config.impl,
properties.@"dbus-connection".impl,
});
// Signals
signals.trigger.impl.register(.{});
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
}
pub const as = C.Class.as;
pub const bindTemplateChildPrivate = C.Class.bindTemplateChildPrivate;
};
};
const Token = [16]u8;
/// Generate a random token suitable for use in requests.
fn generateToken(buf: *Token) [:0]const u8 {
// u28 takes up 7 bytes in hex, 8 bytes for "ghostty_" and 1 byte for NUL
// 7 + 8 + 1 = 16
return std.fmt.bufPrintZ(
buf,
"ghostty_{x:0<7}",
.{std.crypto.random.int(u28)},
) catch unreachable;
}

View File

@ -42,8 +42,6 @@ pub const ResizeOverlay = extern struct {
Self,
c_uint,
.{
.nick = "Duration",
.blurb = "The duration this overlay appears in milliseconds.",
.default = 750,
.minimum = 250,
.maximum = std.math.maxInt(c_uint),
@ -64,8 +62,6 @@ pub const ResizeOverlay = extern struct {
Self,
c_uint,
.{
.nick = "First Delay",
.blurb = "The delay in milliseconds before any overlay is shown for the first time.",
.default = 250,
.minimum = 250,
.maximum = std.math.maxInt(c_uint),
@ -79,6 +75,19 @@ pub const ResizeOverlay = extern struct {
);
};
pub const label = struct {
pub const name = "label";
const impl = gobject.ext.defineProperty(
name,
Self,
?[:0]const u8,
.{
.default = null,
.accessor = C.privateStringFieldAccessor("label_text"),
},
);
};
pub const @"overlay-halign" = struct {
pub const name = "overlay-halign";
const impl = gobject.ext.defineProperty(
@ -86,8 +95,6 @@ pub const ResizeOverlay = extern struct {
Self,
gtk.Align,
.{
.nick = "halign",
.blurb = "The alignment of the label.",
.default = .center,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -106,8 +113,6 @@ pub const ResizeOverlay = extern struct {
Self,
gtk.Align,
.{
.nick = "valign",
.blurb = "The alignment of the label.",
.default = .center,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -124,6 +129,9 @@ pub const ResizeOverlay = extern struct {
/// The label with the text
label: *gtk.Label,
/// The text to set on the label when scheduled.
label_text: ?[:0]const u8,
/// The time that the overlay appears.
duration: c_uint,
@ -147,7 +155,7 @@ pub const ResizeOverlay = extern struct {
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
@ -162,9 +170,12 @@ pub const ResizeOverlay = extern struct {
/// Set the label for the overlay. This will not show the
/// overlay if it is currently hidden; you must call schedule.
pub fn setLabel(self: *Self, label: [:0]const u8) void {
pub fn setLabel(self: *Self, label: ?[:0]const u8) void {
const priv = self.private();
priv.label.setText(label.ptr);
if (priv.label_text) |v| glib.free(@constCast(@ptrCast(v)));
priv.label_text = null;
if (label) |v| priv.label_text = glib.ext.dupeZ(u8, v);
self.as(gobject.Object).notifyByPspec(properties.label.impl.param_spec);
}
/// Schedule the overlay to be shown. To avoid flickering during
@ -192,15 +203,26 @@ pub const ResizeOverlay = extern struct {
// No matter what our idler is complete with this callback
priv.idler = null;
// Show ourselves
self.as(gtk.Widget).setVisible(1);
// Cancel our previous show timer no matter what.
if (priv.timer) |timer| {
if (glib.Source.remove(timer) == 0) {
log.warn("unable to remove size overlay timer", .{});
}
priv.timer = null;
}
// If we have a label to show, show ourselves. If we don't have
// label text, then hide our label.
const text = priv.label_text orelse {
self.as(gtk.Widget).setVisible(0);
return 0;
};
// Set our label and show it.
priv.label.setLabel(text);
self.as(gtk.Widget).setVisible(1);
// Setup the new timer to hide ourselves after the delay.
priv.timer = glib.timeoutAdd(
priv.duration,
onTimer,
@ -228,7 +250,7 @@ pub const ResizeOverlay = extern struct {
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.idler) |v| {
if (glib.Source.remove(v) == 0) {
@ -260,6 +282,19 @@ pub const ResizeOverlay = extern struct {
);
}
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.label_text) |v| {
glib.free(@constCast(@ptrCast(v)));
priv.label_text = null;
}
gobject.Object.virtual_methods.finalize.call(
Class.parent,
self.as(Parent),
);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -271,7 +306,7 @@ pub const ResizeOverlay = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
@ -287,6 +322,7 @@ pub const ResizeOverlay = extern struct {
// Properties
gobject.ext.registerProperties(class, &.{
properties.duration.impl,
properties.label.impl,
properties.@"first-delay".impl,
properties.@"overlay-halign".impl,
properties.@"overlay-valign".impl,
@ -294,6 +330,7 @@ pub const ResizeOverlay = extern struct {
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);
gobject.Object.virtual_methods.finalize.implement(class, &finalize);
}
pub const as = C.Class.as;

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@ const gobject = @import("gobject");
const gtk = @import("gtk");
const apprt = @import("../../../apprt.zig");
const datastruct = @import("../../../datastruct/main.zig");
const font = @import("../../../font/main.zig");
const input = @import("../../../input.zig");
const internal_os = @import("../../../os/main.zig");
@ -42,6 +43,9 @@ pub const Surface = extern struct {
.private = .{ .Type = Private, .offset = &Private.offset },
});
/// A SplitTree implementation that stores surfaces.
pub const Tree = datastruct.SplitTree(Self);
pub const properties = struct {
pub const config = struct {
pub const name = "config";
@ -50,8 +54,6 @@ pub const Surface = extern struct {
Self,
?*Config,
.{
.nick = "Config",
.blurb = "The configuration that this surface is using.",
.accessor = C.privateObjFieldAccessor("config"),
},
);
@ -64,8 +66,6 @@ pub const Surface = extern struct {
Self,
bool,
.{
.nick = "Child Exited",
.blurb = "True when the child process has exited.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -84,8 +84,6 @@ pub const Surface = extern struct {
Self,
?*Size,
.{
.nick = "Default Size",
.blurb = "The default size of the window for this surface.",
.accessor = C.privateBoxedFieldAccessor("default_size"),
},
);
@ -98,8 +96,6 @@ pub const Surface = extern struct {
Self,
?*font.face.DesiredSize,
.{
.nick = "Desired Font Size",
.blurb = "The desired font size, only affects initialization.",
.accessor = C.privateBoxedFieldAccessor("font_size_request"),
},
);
@ -112,8 +108,6 @@ pub const Surface = extern struct {
Self,
bool,
.{
.nick = "Focused",
.blurb = "The focused state of the surface.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -132,8 +126,6 @@ pub const Surface = extern struct {
Self,
?*Size,
.{
.nick = "Minimum Size",
.blurb = "The minimum size of the surface.",
.accessor = C.privateBoxedFieldAccessor("min_size"),
},
);
@ -146,14 +138,14 @@ pub const Surface = extern struct {
Self,
bool,
.{
.nick = "Mouse Hidden",
.blurb = "Whether the mouse cursor should be hidden.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
.accessor = gobject.ext.typedAccessor(
Self,
Private,
&Private.offset,
"mouse_hidden",
bool,
.{
.getter = getMouseHidden,
.setter = setMouseHidden,
},
),
},
);
@ -166,14 +158,14 @@ pub const Surface = extern struct {
Self,
terminal.MouseShape,
.{
.nick = "Mouse Shape",
.blurb = "The current mouse shape to show for the surface.",
.default = .text,
.accessor = gobject.ext.privateFieldAccessor(
.accessor = gobject.ext.typedAccessor(
Self,
Private,
&Private.offset,
"mouse_shape",
terminal.MouseShape,
.{
.getter = getMouseShape,
.setter = setMouseShape,
},
),
},
);
@ -188,8 +180,6 @@ pub const Surface = extern struct {
Self,
?[:0]const u8,
.{
.nick = "Mouse Hover URL",
.blurb = "The URL the mouse is currently hovering over (if any).",
.default = null,
.accessor = C.privateStringFieldAccessor("mouse_hover_url"),
},
@ -205,8 +195,6 @@ pub const Surface = extern struct {
Self,
?[:0]const u8,
.{
.nick = "Working Directory",
.blurb = "The current working directory as reported by core.",
.default = null,
.accessor = C.privateStringFieldAccessor("pwd"),
},
@ -222,8 +210,6 @@ pub const Surface = extern struct {
Self,
?[:0]const u8,
.{
.nick = "Title",
.blurb = "The title of the surface.",
.default = null,
.accessor = C.privateStringFieldAccessor("title"),
},
@ -237,8 +223,6 @@ pub const Surface = extern struct {
Self,
bool,
.{
.nick = "Zoom",
.blurb = "Whether the surface should be zoomed.",
.default = false,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -327,6 +311,18 @@ pub const Surface = extern struct {
);
};
/// Emitted just prior to the context menu appearing.
pub const menu = struct {
pub const name = "menu";
pub const connect = impl.connect;
const impl = gobject.ext.defineSignal(
name,
Self,
&.{},
void,
);
};
/// Emitted when the focus wants to be brought to the top and
/// focused.
pub const @"present-request" = struct {
@ -462,6 +458,7 @@ pub const Surface = extern struct {
// Template binds
child_exited_overlay: *ChildExited,
context_menu: *gtk.PopoverMenu,
drop_target: *gtk.DropTarget,
progress_bar_overlay: *gtk.ProgressBar,
@ -553,6 +550,11 @@ pub const Surface = extern struct {
);
}
pub fn toggleCommandPalette(self: *Self) bool {
// TODO: pass the surface with the action
return self.as(gtk.Widget).activateAction("win.toggle-command-palette", null) != 0;
}
/// Set the current progress report state.
pub fn setProgressReport(
self: *Self,
@ -1145,7 +1147,7 @@ pub const Surface = extern struct {
//---------------------------------------------------------------
// Virtual Methods
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
const priv = self.private();
@ -1194,7 +1196,7 @@ pub const Surface = extern struct {
self.propConfig(undefined, null);
}
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config) |v| {
v.unref();
@ -1218,7 +1220,7 @@ pub const Surface = extern struct {
);
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.core_surface) |v| {
// Remove ourselves from the list of known surfaces in the app.
@ -1273,11 +1275,34 @@ pub const Surface = extern struct {
return self.private().title;
}
/// Set the title for this surface, copies the value.
pub fn setTitle(self: *Self, title: ?[:0]const u8) void {
const priv = self.private();
if (priv.title) |v| glib.free(@constCast(@ptrCast(v)));
priv.title = null;
if (title) |v| priv.title = glib.ext.dupeZ(u8, v);
self.as(gobject.Object).notifyByPspec(properties.title.impl.param_spec);
}
/// Returns the pwd property without a copy.
pub fn getPwd(self: *Self) ?[:0]const u8 {
return self.private().pwd;
}
/// Set the pwd for this surface, copies the value.
pub fn setPwd(self: *Self, pwd: ?[:0]const u8) void {
const priv = self.private();
if (priv.pwd) |v| glib.free(@constCast(@ptrCast(v)));
priv.pwd = null;
if (pwd) |v| priv.pwd = glib.ext.dupeZ(u8, v);
self.as(gobject.Object).notifyByPspec(properties.pwd.impl.param_spec);
}
/// Returns the focus state of this surface.
pub fn getFocused(self: *Self) bool {
return self.private().focused;
}
/// Change the configuration for this surface.
pub fn setConfig(self: *Self, config: *Config) void {
const priv = self.private();
@ -1330,6 +1355,34 @@ pub const Surface = extern struct {
self.as(gobject.Object).notifyByPspec(properties.@"min-size".impl.param_spec);
}
pub fn getMouseShape(self: *Self) terminal.MouseShape {
return self.private().mouse_shape;
}
pub fn setMouseShape(self: *Self, shape: terminal.MouseShape) void {
const priv = self.private();
priv.mouse_shape = shape;
self.as(gobject.Object).notifyByPspec(properties.@"mouse-shape".impl.param_spec);
}
pub fn getMouseHidden(self: *Self) bool {
return self.private().mouse_hidden;
}
pub fn setMouseHidden(self: *Self, hidden: bool) void {
const priv = self.private();
priv.mouse_hidden = hidden;
self.as(gobject.Object).notifyByPspec(properties.@"mouse-hidden".impl.param_spec);
}
pub fn setMouseHoverUrl(self: *Self, url: ?[:0]const u8) void {
const priv = self.private();
if (priv.mouse_hover_url) |v| glib.free(@constCast(@ptrCast(v)));
priv.mouse_hover_url = null;
if (url) |v| priv.mouse_hover_url = glib.ext.dupeZ(u8, v);
self.as(gobject.Object).notifyByPspec(properties.@"mouse-hover-url".impl.param_spec);
}
fn propConfig(
self: *Self,
_: *gobject.ParamSpec,
@ -1473,6 +1526,16 @@ pub const Surface = extern struct {
self.close(.{ .surface = false });
}
fn contextMenuClosed(
_: *gtk.PopoverMenu,
self: *Self,
) callconv(.c) void {
// When the context menu closes, it moves focus back to the tab
// bar if there are tabs. That's not correct. We need to grab it
// on the surface.
self.grabFocus();
}
fn dtDrop(
_: *gtk.DropTarget,
value: *gobject.Value,
@ -1604,6 +1667,7 @@ pub const Surface = extern struct {
priv.focused = true;
priv.im_context.as(gtk.IMContext).focusIn();
_ = glib.idleAddOnce(idleFocus, self.ref());
self.as(gobject.Object).notifyByPspec(properties.focused.impl.param_spec);
}
fn ecFocusLeave(_: *gtk.EventControllerFocus, self: *Self) callconv(.c) void {
@ -1611,6 +1675,7 @@ pub const Surface = extern struct {
priv.focused = false;
priv.im_context.as(gtk.IMContext).focusOut();
_ = glib.idleAddOnce(idleFocus, self.ref());
self.as(gobject.Object).notifyByPspec(properties.focused.impl.param_spec);
}
/// The focus callback must be triggered on an idle loop source because
@ -1647,9 +1712,9 @@ pub const Surface = extern struct {
}
// Report the event
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const consumed = if (priv.core_surface) |surface| consumed: {
const gtk_mods = event.getModifierState();
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
const mods = gtk_key.translateMods(gtk_mods);
break :consumed surface.mouseButtonCallback(
.press,
@ -1661,10 +1726,28 @@ pub const Surface = extern struct {
};
} else false;
// TODO: context menu
_ = consumed;
_ = x;
_ = y;
// If a right click isn't consumed, mouseButtonCallback selects the hovered
// word and returns false. We can use this to handle the context menu
// opening under normal scenarios.
if (!consumed and button == .right) {
signals.menu.impl.emit(
self,
null,
.{},
null,
);
const rect: gdk.Rectangle = .{
.f_x = @intFromFloat(x),
.f_y = @intFromFloat(y),
.f_width = 1,
.f_height = 1,
};
const popover = priv.context_menu.as(gtk.Popover);
popover.setPointingTo(&rect);
popover.popup();
}
}
fn gcMouseUp(
@ -2234,6 +2317,7 @@ pub const Surface = extern struct {
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
pub const refSink = C.refSink;
pub const unref = C.unref;
const private = C.private;
@ -2242,7 +2326,7 @@ pub const Surface = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(ResizeOverlay);
gobject.ext.ensureType(ChildExited);
gtk.Widget.Class.setTemplateFromResource(
@ -2259,6 +2343,7 @@ pub const Surface = extern struct {
class.bindTemplateChildPrivate("url_left", .{});
class.bindTemplateChildPrivate("url_right", .{});
class.bindTemplateChildPrivate("child_exited_overlay", .{});
class.bindTemplateChildPrivate("context_menu", .{});
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
class.bindTemplateChildPrivate("resize_overlay", .{});
class.bindTemplateChildPrivate("drop_target", .{});
@ -2288,6 +2373,7 @@ pub const Surface = extern struct {
class.bindTemplateCallback("url_mouse_enter", &ecUrlMouseEnter);
class.bindTemplateCallback("url_mouse_leave", &ecUrlMouseLeave);
class.bindTemplateCallback("child_exited_close", &childExitedClose);
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
@ -2315,6 +2401,7 @@ pub const Surface = extern struct {
signals.@"clipboard-read".impl.register(.{});
signals.@"clipboard-write".impl.register(.{});
signals.init.impl.register(.{});
signals.menu.impl.register(.{});
signals.@"present-request".impl.register(.{});
signals.@"toggle-fullscreen".impl.register(.{});
signals.@"toggle-maximize".impl.register(.{});

View File

@ -40,8 +40,6 @@ const SurfaceChildExitedBanner = extern struct {
Self,
?*apprt.surface.Message.ChildExited,
.{
.nick = "Data",
.blurb = "The child exit data.",
.accessor = C.privateBoxedFieldAccessor("data"),
},
);
@ -72,7 +70,7 @@ const SurfaceChildExitedBanner = extern struct {
pub var offset: c_int = 0;
};
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
}
@ -134,7 +132,7 @@ const SurfaceChildExitedBanner = extern struct {
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
gtk.Widget.disposeTemplate(
self.as(gtk.Widget),
getGObjectType(),
@ -146,7 +144,7 @@ const SurfaceChildExitedBanner = extern struct {
);
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.data) |v| {
glib.ext.destroy(v);
@ -170,7 +168,7 @@ const SurfaceChildExitedBanner = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
comptime gresource.blueprint(.{
@ -255,7 +253,7 @@ const SurfaceChildExitedNoop = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
_ = class;
signals.@"close-request".impl.register(.{});
}

View File

@ -18,6 +18,7 @@ const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const SplitTree = @import("split_tree.zig").SplitTree;
const Surface = @import("surface.zig").Surface;
const log = std.log.scoped(.gtk_ghostty_window);
@ -35,7 +36,7 @@ pub const Tab = extern struct {
});
pub const properties = struct {
/// The active surface is the focus that should be receiving all
/// The active surface is the surface that should be receiving all
/// surface-targeted actions. This is usually the focused surface,
/// but may also not be focused if the user has selected a non-surface
/// widget.
@ -46,8 +47,6 @@ pub const Tab = extern struct {
Self,
?*Surface,
.{
.nick = "Active Surface",
.blurb = "The currently active surface.",
.accessor = gobject.ext.typedAccessor(
Self,
?*Surface,
@ -66,13 +65,29 @@ pub const Tab = extern struct {
Self,
?*Config,
.{
.nick = "Config",
.blurb = "The configuration that this surface is using.",
.accessor = C.privateObjFieldAccessor("config"),
},
);
};
pub const @"surface-tree" = struct {
pub const name = "surface-tree";
const impl = gobject.ext.defineProperty(
name,
Self,
?*Surface.Tree,
.{
.accessor = gobject.ext.typedAccessor(
Self,
?*Surface.Tree,
.{
.getter = getSurfaceTree,
},
),
},
);
};
pub const title = struct {
pub const name = "title";
pub const get = impl.get;
@ -82,8 +97,6 @@ pub const Tab = extern struct {
Self,
?[:0]const u8,
.{
.nick = "Title",
.blurb = "The title of the active surface.",
.default = null,
.accessor = C.privateStringFieldAccessor("title"),
},
@ -117,7 +130,7 @@ pub const Tab = extern struct {
surface_bindings: *gobject.BindingGroup,
// Template bindings
surface: *Surface,
split_tree: *SplitTree,
pub var offset: c_int = 0;
};
@ -125,15 +138,13 @@ pub const Tab = extern struct {
/// Set the parent of this tab page. This only affects the first surface
/// ever created for a tab. If a surface was already created this does
/// nothing.
pub fn setParent(
self: *Self,
parent: *CoreSurface,
) void {
const priv = self.private();
priv.surface.setParent(parent);
pub fn setParent(self: *Self, parent: *CoreSurface) void {
if (self.getActiveSurface()) |surface| {
surface.setParent(parent);
}
}
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// If our configuration is null then we get the configuration
@ -153,13 +164,15 @@ pub const Tab = extern struct {
.{},
);
// TODO: Eventually this should be set dynamically based on the
// current active surface.
priv.surface_bindings.setSource(priv.surface.as(gobject.Object));
// We need to do this so that the title initializes properly,
// I think because its a dynamic getter.
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
// Create our initial surface in the split tree.
priv.split_tree.newSplit(.right, null) catch |err| switch (err) {
error.OutOfMemory => {
// TODO: We should make our "no surfaces" state more aesthetically
// pleasing and show something like an "Oops, something went wrong"
// message. For now, this is incredibly unlikely.
@panic("oom");
},
};
}
//---------------------------------------------------------------
@ -167,15 +180,26 @@ pub const Tab = extern struct {
/// Get the currently active surface. See the "active-surface" property.
/// This does not ref the value.
pub fn getActiveSurface(self: *Self) *Surface {
pub fn getActiveSurface(self: *Self) ?*Surface {
return self.getSplitTree().getActiveSurface();
}
/// Get the surface tree of this tab.
pub fn getSurfaceTree(self: *Self) ?*Surface.Tree {
const priv = self.private();
return priv.surface;
return priv.split_tree.getTree();
}
/// Get the split tree widget that is in this tab.
pub fn getSplitTree(self: *Self) *SplitTree {
const priv = self.private();
return priv.split_tree;
}
/// Returns true if this tab needs confirmation before quitting based
/// on the various Ghostty configurations.
pub fn getNeedsConfirmQuit(self: *Self) bool {
const surface = self.getActiveSurface();
const surface = self.getActiveSurface() orelse return false;
const core_surface = surface.core() orelse return false;
return core_surface.needsConfirmQuit();
}
@ -183,7 +207,7 @@ pub const Tab = extern struct {
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.config) |v| {
v.unref();
@ -202,7 +226,7 @@ pub const Tab = extern struct {
);
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
if (priv.title) |v| {
glib.free(@constCast(@ptrCast(v)));
@ -218,27 +242,40 @@ pub const Tab = extern struct {
//---------------------------------------------------------------
// Signal handlers
fn surfaceCloseRequest(
_: *Surface,
scope: *const Surface.CloseScope,
fn propSplitTree(
_: *SplitTree,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
switch (scope.*) {
// Handled upstream... we don't control our window close.
.window => return,
self.as(gobject.Object).notifyByPspec(properties.@"surface-tree".impl.param_spec);
// Presently both the same, results in the tab closing.
.surface, .tab => {
// If our tree is empty we close the tab.
const tree: *const Surface.Tree = self.getSurfaceTree() orelse &.empty;
if (tree.isEmpty()) {
signals.@"close-request".impl.emit(
self,
null,
.{},
null,
);
},
return;
}
}
fn propActiveSurface(
_: *SplitTree,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
const priv = self.private();
priv.surface_bindings.setSource(null);
if (self.getActiveSurface()) |surface| {
priv.surface_bindings.setSource(surface.as(gobject.Object));
}
self.as(gobject.Object).notifyByPspec(properties.@"active-surface".impl.param_spec);
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -250,7 +287,8 @@ pub const Tab = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(SplitTree);
gobject.ext.ensureType(Surface);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
@ -265,14 +303,16 @@ pub const Tab = extern struct {
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.config.impl,
properties.@"surface-tree".impl,
properties.title.impl,
});
// Bindings
class.bindTemplateChildPrivate("surface", .{});
class.bindTemplateChildPrivate("split_tree", .{});
// Template Callbacks
class.bindTemplateCallback("surface_close_request", &surfaceCloseRequest);
class.bindTemplateCallback("notify_active_surface", &propActiveSurface);
class.bindTemplateCallback("notify_tree", &propSplitTree);
// Signals
signals.@"close-request".impl.register(.{});

View File

@ -11,6 +11,7 @@ const gtk = @import("gtk");
const i18n = @import("../../../os/main.zig").i18n;
const apprt = @import("../../../apprt.zig");
const configpkg = @import("../../../config.zig");
const TitlebarStyle = configpkg.Config.GtkTitlebarStyle;
const input = @import("../../../input.zig");
const CoreSurface = @import("../../../Surface.zig");
const ext = @import("../ext.zig");
@ -22,9 +23,12 @@ const Common = @import("../class.zig").Common;
const Config = @import("config.zig").Config;
const Application = @import("application.zig").Application;
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
const SplitTree = @import("split_tree.zig").SplitTree;
const Surface = @import("surface.zig").Surface;
const Tab = @import("tab.zig").Tab;
const DebugWarning = @import("debug_warning.zig").DebugWarning;
const CommandPalette = @import("command_palette.zig").CommandPalette;
const WeakRef = @import("../weak_ref.zig").WeakRef;
const log = std.log.scoped(.gtk_ghostty_window);
@ -52,8 +56,6 @@ pub const Window = extern struct {
Self,
?*Surface,
.{
.nick = "Active Surface",
.blurb = "The currently active surface.",
.accessor = gobject.ext.typedAccessor(
Self,
?*Surface,
@ -72,8 +74,6 @@ pub const Window = extern struct {
Self,
?*Config,
.{
.nick = "Config",
.blurb = "The configuration that this surface is using.",
.accessor = C.privateObjFieldAccessor("config"),
},
);
@ -86,8 +86,6 @@ pub const Window = extern struct {
Self,
bool,
.{
.nick = "Debug",
.blurb = "True if runtime safety checks are enabled.",
.default = build_config.is_debug,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = struct {
@ -100,6 +98,25 @@ pub const Window = extern struct {
);
};
pub const @"titlebar-style" = struct {
pub const name = "titlebar-style";
const impl = gobject.ext.defineProperty(
name,
Self,
TitlebarStyle,
.{
.default = .native,
.accessor = gobject.ext.typedAccessor(
Self,
TitlebarStyle,
.{
.getter = Self.getTitlebarStyle,
},
),
},
);
};
pub const @"headerbar-visible" = struct {
pub const name = "headerbar-visible";
const impl = gobject.ext.defineProperty(
@ -107,8 +124,6 @@ pub const Window = extern struct {
Self,
bool,
.{
.nick = "Headerbar Visible",
.blurb = "True if the headerbar is visible.",
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getHeaderbarVisible,
@ -117,23 +132,6 @@ pub const Window = extern struct {
);
};
pub const @"background-opaque" = struct {
pub const name = "background-opaque";
const impl = gobject.ext.defineProperty(
name,
Self,
bool,
.{
.nick = "Background Opaque",
.blurb = "True if the background should be opaque.",
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getBackgroundOpaque,
}),
},
);
};
pub const @"quick-terminal" = struct {
pub const name = "quick-terminal";
const impl = gobject.ext.defineProperty(
@ -141,8 +139,6 @@ pub const Window = extern struct {
Self,
bool,
.{
.nick = "Quick Terminal",
.blurb = "Whether this window behaves like a quick terminal.",
.default = true,
.accessor = gobject.ext.privateFieldAccessor(
Self,
@ -161,8 +157,6 @@ pub const Window = extern struct {
Self,
bool,
.{
.nick = "Autohide Tab Bar",
.blurb = "If true, tab bar should autohide.",
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getTabsAutohide,
@ -178,8 +172,6 @@ pub const Window = extern struct {
Self,
bool,
.{
.nick = "Wide Tabs",
.blurb = "If true, tabs will be in the wide expanded style.",
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getTabsWide,
@ -195,8 +187,6 @@ pub const Window = extern struct {
Self,
bool,
.{
.nick = "Tab Bar Visibility",
.blurb = "If true, tab bar should be visible.",
.default = true,
.accessor = gobject.ext.typedAccessor(Self, bool, .{
.getter = Self.getTabsVisible,
@ -212,8 +202,6 @@ pub const Window = extern struct {
Self,
adw.ToolbarStyle,
.{
.nick = "Toolbar Style",
.blurb = "The style for the toolbar top/bottom bars.",
.default = .raised,
.accessor = gobject.ext.typedAccessor(
Self,
@ -261,6 +249,9 @@ pub const Window = extern struct {
/// See tabOverviewOpen for why we have this.
tab_overview_focus_timer: ?c_uint = null,
/// A weak reference to a command palette.
command_palette: WeakRef(CommandPalette) = .empty,
// Template bindings
tab_overview: *adw.TabOverview,
tab_bar: *adw.TabBar,
@ -277,7 +268,7 @@ pub const Window = extern struct {
});
}
fn init(self: *Self, _: *Class) callconv(.C) void {
fn init(self: *Self, _: *Class) callconv(.c) void {
gtk.Widget.initTemplate(self.as(gtk.Widget));
// If our configuration is null then we get the configuration
@ -345,10 +336,16 @@ pub const Window = extern struct {
.{ "close-tab", actionCloseTab, null },
.{ "new-tab", actionNewTab, null },
.{ "new-window", actionNewWindow, null },
.{ "split-right", actionSplitRight, null },
.{ "split-left", actionSplitLeft, null },
.{ "split-up", actionSplitUp, null },
.{ "split-down", actionSplitDown, null },
.{ "copy", actionCopy, null },
.{ "paste", actionPaste, null },
.{ "reset", actionReset, null },
.{ "clear", actionClear, null },
// TODO: accept the surface that toggled the command palette
.{ "toggle-command-palette", actionToggleCommandPalette, null },
};
const action_map = self.as(gio.ActionMap);
@ -420,6 +417,25 @@ pub const Window = extern struct {
.{ .sync_create = true },
);
// Bind signals
const split_tree = tab.getSplitTree();
_ = SplitTree.signals.changed.connect(
split_tree,
*Self,
tabSplitTreeChanged,
self,
.{},
);
// Run an initial notification for the surface tree so we can setup
// initial state.
tabSplitTreeChanged(
split_tree,
null,
split_tree.getTree(),
self,
);
return page;
}
@ -553,12 +569,12 @@ pub const Window = extern struct {
// Trigger all our dynamic properties that depend on the config.
inline for (&.{
"background-opaque",
"headerbar-visible",
"tabs-autohide",
"tabs-visible",
"tabs-wide",
"toolbar-style",
"titlebar-style",
}) |key| {
self.as(gobject.Object).notifyByPspec(
@field(properties, key).impl.param_spec,
@ -568,6 +584,12 @@ pub const Window = extern struct {
// Remainder uses the config
const config = if (priv.config) |v| v.get() else return;
// Only add a solid background if we're opaque.
self.toggleCssClass(
"background",
config.@"background-opacity" >= 1,
);
// Apply class to color headerbar if window-theme is set to `ghostty` and
// GTK version is before 4.16. The conditional is because above 4.16
// we use GTK CSS color variables.
@ -590,6 +612,27 @@ pub const Window = extern struct {
};
}
/// Sync the state of any actions on this window.
fn syncActions(self: *Self) void {
const has_selection = selection: {
const surface = self.getActiveSurface() orelse
break :selection false;
const core_surface = surface.core() orelse
break :selection false;
break :selection core_surface.hasSelection();
};
const action_map: *gio.ActionMap = gobject.ext.cast(
gio.ActionMap,
self,
) orelse return;
const action: *gio.SimpleAction = gobject.ext.cast(
gio.SimpleAction,
action_map.lookupAction("copy") orelse return,
) orelse return;
action.setEnabled(@intFromBool(has_selection));
}
fn toggleCssClass(self: *Self, class: [:0]const u8, value: bool) void {
const widget = self.as(gtk.Widget);
if (value)
@ -623,6 +666,95 @@ pub const Window = extern struct {
self.private().toast_overlay.addToast(toast);
}
fn connectSurfaceHandlers(
self: *Self,
tree: *const Surface.Tree,
) void {
const priv = self.private();
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
_ = Surface.signals.@"close-request".connect(
surface,
*Self,
surfaceCloseRequest,
self,
.{},
);
_ = Surface.signals.@"present-request".connect(
surface,
*Self,
surfacePresentRequest,
self,
.{},
);
_ = Surface.signals.@"clipboard-write".connect(
surface,
*Self,
surfaceClipboardWrite,
self,
.{},
);
_ = Surface.signals.menu.connect(
surface,
*Self,
surfaceMenu,
self,
.{},
);
_ = Surface.signals.@"toggle-fullscreen".connect(
surface,
*Self,
surfaceToggleFullscreen,
self,
.{},
);
_ = Surface.signals.@"toggle-maximize".connect(
surface,
*Self,
surfaceToggleMaximize,
self,
.{},
);
// If we've never had a surface initialize yet, then we register
// this signal. Its theoretically possible to launch multiple surfaces
// before init so we could register this on multiple and that is not
// a problem because we'll check the flag again in each handler.
if (!priv.surface_init) {
_ = Surface.signals.init.connect(
surface,
*Self,
surfaceInit,
self,
.{},
);
}
}
}
/// Disconnect all the surface handlers for the given tree. This should
/// be called whenever a tree is no longer present in the window, e.g.
/// when a tab is detached or the tree changes.
fn disconnectSurfaceHandlers(
self: *Self,
tree: *const Surface.Tree,
) void {
var it = tree.iterator();
while (it.next()) |entry| {
const surface = entry.view;
_ = gobject.signalHandlersDisconnectMatched(
surface.as(gobject.Object),
.{ .data = true },
0,
0,
null,
null,
self,
);
}
}
//---------------------------------------------------------------
// Properties
@ -702,6 +834,14 @@ pub const Window = extern struct {
return false;
}
fn isFullscreen(self: *Window) bool {
return self.as(gtk.Window).isFullscreen() != 0;
}
fn isMaximized(self: *Window) bool {
return self.as(gtk.Window).isMaximized() != 0;
}
fn getHeaderbarVisible(self: *Self) bool {
const priv = self.private();
@ -713,33 +853,37 @@ pub const Window = extern struct {
if (priv.quick_terminal) return false;
// If we're fullscreen we never show the header bar.
if (self.as(gtk.Window).isFullscreen() != 0) return false;
if (self.isFullscreen()) return false;
// The remainder needs a config
const config_obj = self.private().config orelse return true;
const config = config_obj.get();
// *Conditionally* disable the header bar when maximized,
// and gtk-titlebar-hide-when-maximized is set
if (self.as(gtk.Window).isMaximized() != 0 and
config.@"gtk-titlebar-hide-when-maximized")
{
// *Conditionally* disable the header bar when maximized, and
// gtk-titlebar-hide-when-maximized is set
if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") {
return false;
}
return config.@"gtk-titlebar";
}
return switch (config.@"gtk-titlebar-style") {
// If the titlebar style is tabs never show the titlebar.
.tabs => false,
fn getBackgroundOpaque(self: *Self) bool {
const priv = self.private();
const config = (priv.config orelse return true).get();
return config.@"background-opacity" >= 1.0;
// If the titlebar style is native show the titlebar if configured
// to do so.
.native => config.@"gtk-titlebar",
};
}
fn getTabsAutohide(self: *Self) bool {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return true;
return switch (config.@"window-show-tab-bar") {
return switch (config.@"gtk-titlebar-style") {
// If the titlebar style is tabs we cannot autohide.
.tabs => false,
.native => switch (config.@"window-show-tab-bar") {
// Auto we always autohide... obviously.
.auto => true,
@ -749,16 +893,30 @@ pub const Window = extern struct {
// Never we autohide because it doesn't actually matter,
// since getTabsVisible will return false.
.never => true,
},
};
}
fn getTabsVisible(self: *Self) bool {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return true;
switch (config.@"gtk-titlebar-style") {
.tabs => {
// *Conditionally* disable the tab bar when maximized, the titlebar
// style is tabs, and gtk-titlebar-hide-when-maximized is set.
if (self.isMaximized() and config.@"gtk-titlebar-hide-when-maximized") return false;
// If the titlebar style is tabs the tab bar must always be visible.
return true;
},
.native => {
return switch (config.@"window-show-tab-bar") {
.always, .auto => true,
.never => false,
};
},
}
}
fn getTabsWide(self: *Self) bool {
@ -777,6 +935,12 @@ pub const Window = extern struct {
};
}
fn getTitlebarStyle(self: *Self) TitlebarStyle {
const priv = self.private();
const config = if (priv.config) |v| v.get() else return .native;
return config.@"gtk-titlebar-style";
}
fn propConfig(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
@ -845,23 +1009,7 @@ pub const Window = extern struct {
const active = button.getActive() != 0;
if (!active) return;
const has_selection = selection: {
const surface = self.getActiveSurface() orelse
break :selection false;
const core_surface = surface.core() orelse
break :selection false;
break :selection core_surface.hasSelection();
};
const action_map: *gio.ActionMap = gobject.ext.cast(
gio.ActionMap,
self,
) orelse return;
const action: *gio.SimpleAction = gobject.ext.cast(
gio.SimpleAction,
action_map.lookupAction("copy") orelse return,
) orelse return;
action.setEnabled(@intFromBool(has_selection));
self.syncActions();
}
fn propQuickTerminal(
@ -884,16 +1032,6 @@ pub const Window = extern struct {
}
}
/// Add or remove "background" CSS class depending on if the background
/// should be opaque.
fn propBackgroundOpaque(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
self: *Self,
) callconv(.c) void {
self.toggleCssClass("background", self.getBackgroundOpaque());
}
fn propScaleFactor(
_: *adw.ApplicationWindow,
_: *gobject.ParamSpec,
@ -912,15 +1050,29 @@ pub const Window = extern struct {
};
}
fn closureTitlebarStyleIsTab(
_: *Self,
value: TitlebarStyle,
) callconv(.c) c_int {
return @intFromBool(switch (value) {
.native => false,
.tabs => true,
});
}
//---------------------------------------------------------------
// Virtual methods
fn dispose(self: *Self) callconv(.C) void {
fn dispose(self: *Self) callconv(.c) void {
const priv = self.private();
priv.command_palette.set(null);
if (priv.config) |v| {
v.unref();
priv.config = null;
}
priv.tab_bindings.setSource(null);
gtk.Widget.disposeTemplate(
@ -934,7 +1086,7 @@ pub const Window = extern struct {
);
}
fn finalize(self: *Self) callconv(.C) void {
fn finalize(self: *Self) callconv(.c) void {
const priv = self.private();
priv.tab_bindings.unref();
priv.winproto.deinit(Application.default().allocator());
@ -1152,8 +1304,6 @@ pub const Window = extern struct {
_: c_int,
self: *Self,
) callconv(.c) void {
const priv = self.private();
// Get the attached page which must be a Tab object.
const child = page.getChild();
const tab = gobject.ext.cast(Tab, child) orelse return;
@ -1186,57 +1336,8 @@ pub const Window = extern struct {
// behavior is consistent with macOS and the previous GTK apprt,
// but that behavior was all implicit and not documented, so here
// I am.
//
// TODO: When we have a split tree we'll want to attach to that.
const surface = tab.getActiveSurface();
_ = Surface.signals.@"close-request".connect(
surface,
*Self,
surfaceCloseRequest,
self,
.{},
);
_ = Surface.signals.@"present-request".connect(
surface,
*Self,
surfacePresentRequest,
self,
.{},
);
_ = Surface.signals.@"clipboard-write".connect(
surface,
*Self,
surfaceClipboardWrite,
self,
.{},
);
_ = Surface.signals.@"toggle-fullscreen".connect(
surface,
*Self,
surfaceToggleFullscreen,
self,
.{},
);
_ = Surface.signals.@"toggle-maximize".connect(
surface,
*Self,
surfaceToggleMaximize,
self,
.{},
);
// If we've never had a surface initialize yet, then we register
// this signal. Its theoretically possible to launch multiple surfaces
// before init so we could register this on multiple and that is not
// a problem because we'll check the flag again in each handler.
if (!priv.surface_init) {
_ = Surface.signals.init.connect(
surface,
*Self,
surfaceInit,
self,
.{},
);
if (tab.getSurfaceTree()) |tree| {
self.connectSurfaceHandlers(tree);
}
}
@ -1259,17 +1360,10 @@ pub const Window = extern struct {
self,
);
// Remove all the signals that have this window as the userdata.
const surface = tab.getActiveSurface();
_ = gobject.signalHandlersDisconnectMatched(
surface.as(gobject.Object),
.{ .data = true },
0,
0,
null,
null,
self,
);
// Remove the tree handlers
if (tab.getSurfaceTree()) |tree| {
self.disconnectSurfaceHandlers(tree);
}
}
fn tabViewCreateWindow(
@ -1355,6 +1449,13 @@ pub const Window = extern struct {
}
}
fn surfaceMenu(
_: *Surface,
self: *Self,
) callconv(.c) void {
self.syncActions();
}
fn surfacePresentRequest(
surface: *Surface,
self: *Self,
@ -1452,6 +1553,21 @@ pub const Window = extern struct {
}
}
fn tabSplitTreeChanged(
_: *SplitTree,
old_tree: ?*const Surface.Tree,
new_tree: ?*const Surface.Tree,
self: *Self,
) callconv(.c) void {
if (old_tree) |tree| {
self.disconnectSurfaceHandlers(tree);
}
if (new_tree) |tree| {
self.connectSurfaceHandlers(tree);
}
}
fn actionAbout(
_: *gio.SimpleAction,
_: ?*glib.Variant,
@ -1528,6 +1644,38 @@ pub const Window = extern struct {
self.performBindingAction(.new_tab);
}
fn actionSplitRight(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .right });
}
fn actionSplitLeft(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .left });
}
fn actionSplitUp(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .up });
}
fn actionSplitDown(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
self.performBindingAction(.{ .new_split = .down });
}
fn actionCopy(
_: *gio.SimpleAction,
_: ?*glib.Variant,
@ -1560,6 +1708,68 @@ pub const Window = extern struct {
self.performBindingAction(.clear_screen);
}
/// Toggle the command palette.
///
/// TODO: accept the surface that toggled the command palette as a parameter
fn toggleCommandPalette(self: *Window) void {
const priv = self.private();
// Get a reference to a command palette. First check the weak reference
// that we save to see if we already have one stored. If we don't then
// create a new one.
const command_palette = priv.command_palette.get() orelse command_palette: {
// Create a fresh command palette.
const command_palette = CommandPalette.new();
// Synchronize our config to the command palette's config.
_ = gobject.Object.bindProperty(
self.as(gobject.Object),
"config",
command_palette.as(gobject.Object),
"config",
.{ .sync_create = true },
);
// Listen to the activate signal to know if the user selected an option in
// the command palette.
_ = CommandPalette.signals.trigger.connect(
command_palette,
*Window,
signalCommandPaletteTrigger,
self,
.{},
);
// Save a weak reference to the command palette. We use a weak reference to avoid
// reference counting cycles that might cause problems later.
priv.command_palette.set(command_palette);
break :command_palette command_palette;
};
defer command_palette.unref();
// Tell the command palette to toggle itself. If the dialog gets
// presented (instead of hidden) it will be modal over our window.
command_palette.toggle(self);
}
// React to a signal from a command palette asking an action to be performed.
fn signalCommandPaletteTrigger(_: *CommandPalette, action: *const input.Binding.Action, self: *Self) callconv(.c) void {
// If the activation actually has an action, perform it.
self.performBindingAction(action.*);
}
/// React to a GTK action requesting that the command palette be toggled.
fn actionToggleCommandPalette(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.c) void {
// TODO: accept the surface that toggled the command palette as a
// parameter
self.toggleCommandPalette();
}
const C = Common(Self, Private);
pub const as = C.as;
pub const ref = C.ref;
@ -1571,7 +1781,7 @@ pub const Window = extern struct {
var parent: *Parent.Class = undefined;
pub const Instance = Self;
fn init(class: *Class) callconv(.C) void {
fn init(class: *Class) callconv(.c) void {
gobject.ext.ensureType(DebugWarning);
gtk.Widget.Class.setTemplateFromResource(
class.as(gtk.Widget.Class),
@ -1585,7 +1795,6 @@ pub const Window = extern struct {
// Properties
gobject.ext.registerProperties(class, &.{
properties.@"active-surface".impl,
properties.@"background-opaque".impl,
properties.config.impl,
properties.debug.impl,
properties.@"headerbar-visible".impl,
@ -1594,6 +1803,7 @@ pub const Window = extern struct {
properties.@"tabs-visible".impl,
properties.@"tabs-wide".impl,
properties.@"toolbar-style".impl,
properties.@"titlebar-style".impl,
});
// Bindings
@ -1615,13 +1825,13 @@ pub const Window = extern struct {
class.bindTemplateCallback("tab_create_window", &tabViewCreateWindow);
class.bindTemplateCallback("notify_n_pages", &tabViewNPages);
class.bindTemplateCallback("notify_selected_page", &tabViewSelectedPage);
class.bindTemplateCallback("notify_background_opaque", &propBackgroundOpaque);
class.bindTemplateCallback("notify_config", &propConfig);
class.bindTemplateCallback("notify_fullscreened", &propFullscreened);
class.bindTemplateCallback("notify_maximized", &propMaximized);
class.bindTemplateCallback("notify_menu_active", &propMenuActive);
class.bindTemplateCallback("notify_quick_terminal", &propQuickTerminal);
class.bindTemplateCallback("notify_scale_factor", &propScaleFactor);
class.bindTemplateCallback("titlebar_style_is_tabs", &closureTitlebarStyleIsTab);
// Virtual methods
gobject.Object.virtual_methods.dispose.implement(class, &dispose);

View File

@ -1,3 +1,8 @@
.transparent {
background-color: transparent;
}
.window .split paned > separator {
background-color: rgba(36, 36, 36, 1);
background-clip: content-box;
}

View File

@ -101,3 +101,39 @@ label.resize-overlay {
/* after GTK 4.16 is a requirement, switch to the following: */
/* background-color: color-mix(in srgb, var(--error-bg-color), transparent); */
}
/*
* Command Palette
*/
.command-palette-search > image:first-child {
margin-left: 8px;
margin-right: 4px;
}
.command-palette-search > image:last-child {
margin-left: 4px;
margin-right: 8px;
}
/*
* Splits
*/
.window .split paned > separator {
background-color: rgba(250, 250, 250, 1);
background-clip: content-box;
/* This works around the oversized drag area for the right side of GtkPaned.
*
* Upstream Gtk issue:
* https://gitlab.gnome.org/GNOME/gtk/-/issues/4484#note_2362002
*
* Ghostty issue:
* https://github.com/ghostty-org/ghostty/issues/3020
*
* Without this, it's not possible to select the first character on the
* right-hand side of a split.
*/
margin: 0;
padding: 0;
}

View File

@ -9,7 +9,10 @@ const input = @import("../../input.zig");
const winproto = @import("winproto.zig");
/// Returns a GTK accelerator string from a trigger.
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
pub fn accelFromTrigger(
buf: []u8,
trigger: input.Binding.Trigger,
) error{NoSpaceLeft}!?[:0]const u8 {
var buf_stream = std.io.fixedBufferStream(buf);
const writer = buf_stream.writer();
@ -30,7 +33,10 @@ pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u
/// Returns a XDG-compliant shortcuts string from a trigger.
/// Spec: https://specifications.freedesktop.org/shortcuts-spec/latest/
pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
pub fn xdgShortcutFromTrigger(
buf: []u8,
trigger: input.Binding.Trigger,
) error{NoSpaceLeft}!?[:0]const u8 {
var buf_stream = std.io.fixedBufferStream(buf);
const writer = buf_stream.writer();
@ -54,7 +60,7 @@ pub fn xdgShortcutFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]c
return slice[0 .. slice.len - 1 :0];
}
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) !bool {
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) error{NoSpaceLeft}!bool {
switch (trigger.key) {
.physical => |k| {
const keyval = keyvalFromKey(k) orelse return false;

View File

@ -15,6 +15,10 @@ template $GhosttySurface: Adw.Bin {
focusable: false;
focus-on-click: false;
child: Box {
hexpand: true;
vexpand: true;
GLArea gl_area {
realize => $gl_realize();
unrealize => $gl_unrealize();
@ -26,9 +30,18 @@ template $GhosttySurface: Adw.Bin {
focus-on-click: true;
has-stencil-buffer: false;
has-depth-buffer: false;
use-es: false;
allowed-apis: gl;
}
PopoverMenu context_menu {
closed => $context_menu_closed();
menu-model: context_menu_model;
flags: nested;
halign: start;
has-arrow: false;
}
};
[overlay]
ProgressBar progress_bar_overlay {
styles [
@ -122,3 +135,104 @@ IMMulticontext im_context {
preedit-end => $im_preedit_end();
commit => $im_commit();
}
menu context_menu_model {
section {
item {
label: _("Copy");
action: "win.copy";
}
item {
label: _("Paste");
action: "win.paste";
}
}
section {
item {
label: _("Clear");
action: "win.clear";
}
item {
label: _("Reset");
action: "win.reset";
}
}
section {
submenu {
label: _("Split");
item {
label: _("Change Title…");
action: "win.prompt-title";
}
item {
label: _("Split Up");
action: "split-tree.new-up";
}
item {
label: _("Split Down");
action: "split-tree.new-down";
}
item {
label: _("Split Left");
action: "split-tree.new-left";
}
item {
label: _("Split Right");
action: "split-tree.new-right";
}
}
submenu {
label: _("Tab");
item {
label: _("New Tab");
action: "win.new-tab";
}
item {
label: _("Close Tab");
action: "win.close-tab";
}
}
submenu {
label: _("Window");
item {
label: _("New Window");
action: "win.new-window";
}
item {
label: _("Close Window");
action: "win.close";
}
}
}
section {
submenu {
label: _("Config");
item {
label: _("Open Configuration");
action: "app.open-config";
}
item {
label: _("Reload Configuration");
action: "app.reload-config";
}
}
}
}

View File

@ -0,0 +1,110 @@
using Gtk 4.0;
using Gio 2.0;
using Adw 1;
Adw.Dialog dialog {
content-width: 700;
closed => $closed();
Adw.ToolbarView {
top-bar-style: flat;
[top]
Adw.HeaderBar {
[title]
Gtk.SearchEntry search {
hexpand: true;
placeholder-text: _("Execute a command…");
stop-search => $search_stopped();
activate => $search_activated();
styles [
"command-palette-search",
]
}
}
Gtk.ScrolledWindow {
min-content-height: 300;
Gtk.ListView view {
show-separators: true;
single-click-activate: true;
activate => $row_activated();
model: Gtk.SingleSelection model {
model: Gtk.FilterListModel {
incremental: true;
filter: Gtk.AnyFilter {
Gtk.StringFilter {
expression: expr item as <$GhosttyCommand>.title;
search: bind search.text;
}
Gtk.StringFilter {
expression: expr item as <$GhosttyCommand>.action-key;
search: bind search.text;
}
};
model: Gio.ListStore source {
item-type: typeof<$GhosttyCommand>;
};
};
};
styles [
"rich-list",
]
factory: Gtk.BuilderListItemFactory {
template Gtk.ListItem {
child: Gtk.Box {
orientation: horizontal;
spacing: 10;
tooltip-text: bind template.item as <$GhosttyCommand>.description;
Gtk.Box {
orientation: vertical;
hexpand: true;
Gtk.Label {
ellipsize: end;
halign: start;
wrap: false;
single-line-mode: true;
styles [
"title",
]
label: bind template.item as <$GhosttyCommand>.title;
}
Gtk.Label {
ellipsize: end;
halign: start;
wrap: false;
single-line-mode: true;
styles [
"subtitle",
"monospace",
]
label: bind template.item as <$GhosttyCommand>.action-key;
}
}
Gtk.ShortcutLabel {
accelerator: bind template.item as <$GhosttyCommand>.action;
valign: center;
}
};
}
};
}
}
}
}

View File

@ -0,0 +1,20 @@
using Gtk 4.0;
using Adw 1;
template $GhosttySplitTreeSplit: Adw.Bin {
styles [
"split",
]
// The double-nesting is required due to a GTK bug where you can't
// bind the first child of a builder layout. If you do, you get a double
// dispose. Easiest way to see that is simply remove this and see the
// GTK critical errors (and sometimes crashes).
Adw.Bin {
Paned paned {
notify::max-position => $notify_max_position();
notify::min-position => $notify_min_position();
notify::position => $notify_position();
}
}
}

View File

@ -0,0 +1,25 @@
using Gtk 4.0;
using Adw 1;
template $GhosttySplitTree: Box {
notify::tree => $notify_tree();
orientation: vertical;
Adw.Bin tree_bin {
visible: bind template.has-surfaces;
hexpand: true;
vexpand: true;
}
// This could be a lot more visually pleasing but in practice this doesn't
// ever happen at the time of writing this comment. A surface-less split
// tree always closes its parent.
Label {
visible: bind template.has-surfaces inverted;
// Purposely not localized currently because this shouldn't really
// ever appear. When we have a situation it does appear, we may want
// to change the styling and text so I don't want to burden localizers
// to handle this yet.
label: "No surfaces.";
}
}

View File

@ -5,11 +5,12 @@ template $GhosttyTab: Box {
"tab",
]
orientation: vertical;
hexpand: true;
vexpand: true;
// A tab currently just contains a surface directly. When we introduce
// splits we probably want to replace this with the split widget type.
$GhosttySurface surface {
close-request => $surface_close_request();
$GhosttySplitTree split_tree {
notify::active-surface => $notify_active_surface();
notify::tree => $notify_tree();
}
}

View File

@ -8,7 +8,6 @@ template $GhosttyWindow: Adw.ApplicationWindow {
close-request => $close_request();
realize => $realize();
notify::background-opaque => $notify_background_opaque();
notify::config => $notify_config();
notify::fullscreened => $notify_fullscreened();
notify::maximized => $notify_maximized();
@ -50,6 +49,8 @@ template $GhosttyWindow: Adw.ApplicationWindow {
tooltip-text: _("New Tab");
dropdown-tooltip: _("New Split");
menu-model: split_menu;
can-focus: false;
focus-on-click: false;
}
[end]
@ -78,6 +79,64 @@ template $GhosttyWindow: Adw.ApplicationWindow {
expand-tabs: bind template.tabs-wide;
view: tab_view;
visible: bind template.tabs-visible;
[start]
Gtk.Box {
orientation: horizontal;
visible: bind $titlebar_style_is_tabs(template.titlebar-style) as <bool>;
Gtk.WindowControls {
side: start;
}
Adw.SplitButton {
styles [
"flat",
]
clicked => $new_tab();
icon-name: "tab-new-symbolic";
tooltip-text: _("New Tab");
dropdown-tooltip: _("New Split");
menu-model: split_menu;
can-focus: false;
focus-on-click: false;
}
}
[end]
Gtk.Box {
orientation: horizontal;
visible: bind $titlebar_style_is_tabs(template.titlebar-style) as <bool>;
Gtk.ToggleButton {
styles [
"flat",
]
icon-name: "view-grid-symbolic";
tooltip-text: _("View Open Tabs");
active: bind tab_overview.open bidirectional;
can-focus: false;
focus-on-click: false;
}
Gtk.MenuButton {
styles [
"flat",
]
notify::active => $notify_menu_active();
icon-name: "open-menu-symbolic";
menu-model: main_menu;
tooltip-text: _("Main Menu");
can-focus: false;
}
Gtk.WindowControls {
side: end;
}
}
}
Box {

View File

@ -10,6 +10,8 @@ pub fn WeakRef(comptime T: type) type {
ref: gobject.WeakRef = std.mem.zeroes(gobject.WeakRef),
pub const empty: Self = .{};
/// Set the weak reference to the given object. This will not
/// increase the reference count of the object.
pub fn set(self: *Self, v_: ?*T) void {
@ -23,14 +25,9 @@ pub fn WeakRef(comptime T: type) type {
/// Get a strong reference to the object, or null if the object
/// has been finalized. This increases the reference count by one.
pub fn get(self: *Self) ?*T {
// The GIR of g_weak_ref_get has a bug where the optional
// is not encoded. Or, it may be a bug in zig-gobject.
const obj_: ?*gobject.Object = @ptrCast(self.ref.get());
const obj = obj_ orelse return null;
// We can't use `as` because `as` guarantees conversion and
// that can't be statically guaranteed.
return gobject.ext.cast(T, obj);
return gobject.ext.cast(T, self.ref.get() orelse return null);
}
};
}
@ -38,7 +35,7 @@ pub fn WeakRef(comptime T: type) type {
test WeakRef {
const testing = std.testing;
var ref: WeakRef(gtk.TextBuffer) = .{};
var ref: WeakRef(gtk.TextBuffer) = .empty;
const obj: *gtk.TextBuffer = .new(null);
ref.set(obj);
ref.get().?.unref(); // The "?" asserts non-null

View File

@ -184,14 +184,14 @@ fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
self.destroy();
}
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.C) void {
fn gtkChoose(dialog_: ?*gobject.Object, result: *gio.AsyncResult, ud: ?*anyopaque) callconv(.c) void {
const dialog = gobject.ext.cast(DialogType, dialog_.?).?;
const self: *ClipboardConfirmation = @ptrCast(@alignCast(ud.?));
const response = dialog.chooseFinish(result);
self.handleResponse(response);
}
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.C) void {
fn gtkResponse(_: *DialogType, response: [*:0]u8, self: *ClipboardConfirmation) callconv(.c) void {
self.handleResponse(response);
}

View File

@ -335,6 +335,7 @@ fn request(
var response: u32 = 0;
var vardict: ?*glib.Variant = null;
defer if (vardict) |v| v.unref();
params_.get("(u@a{sv})", &response, &vardict);
switch (response) {

View File

@ -1121,7 +1121,7 @@ fn gtkActionToggleCommandPalette(
_: *gio.SimpleAction,
_: ?*glib.Variant,
self: *Window,
) callconv(.C) void {
) callconv(.c) void {
self.performBindingAction(.toggle_command_palette);
}

View File

@ -82,10 +82,6 @@ pub fn Menu(
return self.menu_widget.as(gtk.Widget).getVisible() != 0;
}
pub fn setVisible(self: *const Self, visible: bool) void {
self.menu_widget.as(gtk.Widget).setVisible(@intFromBool(visible));
}
/// Refresh the menu. Right now that means enabling/disabling the "Copy"
/// menu item based on whether there is an active selection or not, but
/// that may change in the future.

View File

@ -37,6 +37,7 @@ font_backend: font.Backend = .freetype,
x11: bool = false,
wayland: bool = false,
sentry: bool = true,
i18n: bool = true,
wasm_shared: bool = true,
/// Ghostty exe properties
@ -175,6 +176,16 @@ pub fn init(b: *std.Build) !Config {
"Enables linking against X11 libraries when using the GTK rendering backend.",
) orelse gtk_targets.x11;
config.i18n = b.option(
bool,
"i18n",
"Enables gettext-based internationalization. Enabled by default only for macOS, and other Unix-like systems like Linux and FreeBSD when using glibc.",
) orelse switch (target.result.os.tag) {
.macos, .ios => true,
.linux, .freebsd => target.result.isGnuLibC(),
else => false,
};
//---------------------------------------------------------------
// Ghostty Exe Properties
@ -420,6 +431,7 @@ pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
step.addOption(bool, "x11", self.x11);
step.addOption(bool, "wayland", self.wayland);
step.addOption(bool, "sentry", self.sentry);
step.addOption(bool, "i18n", self.i18n);
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);
@ -467,6 +479,7 @@ pub fn fromOptions() Config {
.exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?,
.wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?,
.wasm_shared = options.wasm_shared,
.i18n = options.i18n,
};
}

View File

@ -17,7 +17,7 @@ xctest: *std.Build.Step.Run,
pub const Deps = struct {
xcframework: *const XCFramework,
docs: *const Docs,
i18n: *const I18n,
i18n: ?*const I18n,
resources: *const Resources,
};
@ -81,7 +81,7 @@ pub fn init(
// We also need all these resources because the xcode project
// references them via symlinks.
deps.resources.addStepDependencies(&step.step);
deps.i18n.addStepDependencies(&step.step);
if (deps.i18n) |v| v.addStepDependencies(&step.step);
deps.docs.installDummy(&step.step);
// Expect success
@ -113,7 +113,7 @@ pub fn init(
// We also need all these resources because the xcode project
// references them via symlinks.
deps.resources.addStepDependencies(&step.step);
deps.i18n.addStepDependencies(&step.step);
if (deps.i18n) |v| v.addStepDependencies(&step.step);
deps.docs.installDummy(&step.step);
// Expect success

View File

@ -41,6 +41,7 @@ pub const flatpak = options.flatpak;
pub const app_runtime: apprt.Runtime = config.app_runtime;
pub const font_backend: font.Backend = config.font_backend;
pub const renderer: rendererpkg.Impl = config.renderer;
pub const i18n: bool = config.i18n;
/// The bundle ID for the app. This is used in many places and is currently
/// hardcoded here. We could make this configurable in the future if there

View File

@ -2892,6 +2892,21 @@ else
/// more subtle border.
@"gtk-toolbar-style": GtkToolbarStyle = .raised,
/// The style of the GTK titlbar. Available values are `native` and `tabs`.
///
/// The `native` titlebar style is a traditional titlebar with a title, a few
/// buttons and window controls. A separate tab bar will show up below the
/// titlebar if you have multiple tabs open in the window.
///
/// The `tabs` titlebar merges the tab bar and the traditional titlebar.
/// This frees up vertical space on your screen if you use multiple tabs. One
/// limitation of the `tabs` titlebar is that you cannot drag the titlebar
/// by the titles any longer (as they are tab titles now). Other areas of the
/// `tabs` title bar can be used to drag the window around.
///
/// The default style is `native`.
@"gtk-titlebar-style": GtkTitlebarStyle = .native,
/// If `true` (default), then the Ghostty GTK tabs will be "wide." Wide tabs
/// are the new typical Gnome style where tabs fill their available space.
/// If you set this to `false` then tabs will only take up space they need,
@ -6947,6 +6962,21 @@ pub const GtkToolbarStyle = enum {
@"raised-border",
};
/// See gtk-titlebar-style
pub const GtkTitlebarStyle = enum(c_int) {
native,
tabs,
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineEnum(
GtkTitlebarStyle,
.{ .name = "GhosttyGtkTitlebarStyle" },
),
.none => void,
};
};
/// See app-notifications
pub const AppNotifications = packed struct {
@"clipboard-copy": bool = true,

View File

@ -6,6 +6,7 @@ const cache_table = @import("cache_table.zig");
const circ_buf = @import("circ_buf.zig");
const intrusive_linked_list = @import("intrusive_linked_list.zig");
const segmented_pool = @import("segmented_pool.zig");
const split_tree = @import("split_tree.zig");
pub const lru = @import("lru.zig");
pub const BlockingQueue = blocking_queue.BlockingQueue;
@ -13,6 +14,7 @@ pub const CacheTable = cache_table.CacheTable;
pub const CircBuf = circ_buf.CircBuf;
pub const IntrusiveDoublyLinkedList = intrusive_linked_list.DoublyLinkedList;
pub const SegmentedPool = segmented_pool.SegmentedPool;
pub const SplitTree = split_tree.SplitTree;
test {
@import("std").testing.refAllDecls(@This());

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,10 @@ cursor_height: u32,
/// The constraint height for nerd fonts icons.
icon_height: u32,
/// Original cell width in pixels. This is used to keep
/// glyphs centered if the cell width is adjusted wider.
original_cell_width: ?u32 = null,
/// Minimum acceptable values for some fields to prevent modifiers
/// from being able to, for example, cause 0-thickness underlines.
const Minimums = struct {
@ -263,6 +267,11 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
const new = @max(entry.value_ptr.apply(original), 1);
if (new == original) continue;
// Preserve the original cell width if not set.
if (self.original_cell_width == null) {
self.original_cell_width = self.cell_width;
}
// Set the new value
@field(self, @tagName(tag)) = new;

View File

@ -222,6 +222,16 @@ pub const RenderOptions = struct {
y: f64,
};
/// Returns true if the constraint does anything. If it doesn't,
/// because it neither sizes nor positions the glyph, then this
/// returns false.
pub inline fn doesAnything(self: Constraint) bool {
return self.size_horizontal != .none or
self.align_horizontal != .none or
self.size_vertical != .none or
self.align_vertical != .none;
}
/// Apply this constraint to the provided glyph
/// size, given the available width and height.
pub fn constrain(

View File

@ -346,89 +346,76 @@ pub const Face = struct {
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width);
// const cell_height: f64 = @floatFromInt(metrics.cell_height);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
// Next we apply any constraints to get the final size of the glyph.
var constraint = opts.constraint;
// We eliminate any negative vertical padding since these overlap
// values aren't needed under CoreText with how precisely we apply
// constraints, and they can lead to extra height that looks bad
// for things like powerline glyphs.
var constraint = opts.constraint;
// values aren't needed with how precisely we apply constraints,
// and they can lead to extra height that looks bad for things like
// powerline glyphs.
constraint.pad_top = @max(0.0, constraint.pad_top);
constraint.pad_bottom = @max(0.0, constraint.pad_bottom);
// We need to add the baseline position before passing to the constrain
// function since it operates on cell-relative positions, not baseline.
const cell_baseline: f64 = @floatFromInt(metrics.cell_baseline);
const glyph_size = constraint.constrain(
.{
.width = rect.size.width,
.height = rect.size.height,
.x = rect.origin.x,
.y = rect.origin.y + @as(f64, @floatFromInt(metrics.cell_baseline)),
.y = rect.origin.y + cell_baseline,
},
metrics,
opts.constraint_width,
);
// These calculations are an attempt to mostly imitate the effect of
// `shouldSubpixelQuantizeFonts`[^1], which helps maximize legibility
// at small pixel sizes (low DPI). We do this math ourselves instead
// of letting CoreText do it because it's not entirely clear how the
// math in CoreText works and we've run in to edge cases where glyphs
// have their bottom or left row cut off due to bad rounding.
//
// This math seems to have a mostly comparable result to whatever it
// is that CoreText does, and is even (in my opinion) better in some
// cases.
//
// I'm not entirely certain but I suspect that when you enable the
// CoreText option it also does some sort of rudimentary hinting,
// but it doesn't seem to make that big of a difference in terms
// of legibility in the end.
//
// [^1]: https://developer.apple.com/documentation/coregraphics/cgcontext/setshouldsubpixelquantizefonts(_:)?language=objc
var x = glyph_size.x;
var y = glyph_size.y;
var width = glyph_size.width;
var height = glyph_size.height;
// We only want to apply quantization if we don't have any
// constraints and this isn't a bitmap glyph, since CoreText
// doesn't seem to apply its quantization to bitmap glyphs.
//
// TODO: Maybe gate this so it only applies at small font sizes,
// or else offer a user config option that can disable it.
const should_quantize = !sbix and std.meta.eql(opts.constraint, .none);
// If this is a bitmap glyph, it will always render as full pixels,
// not fractional pixels, so we need to quantize its position and
// size accordingly to align to full pixels so we get good results.
if (sbix) {
width = cell_width - @round(cell_width - width - x) - @round(x);
height = cell_height - @round(cell_height - height - y) - @round(y);
x = @round(x);
y = @round(y);
}
// We offset our glyph by its bearings when we draw it, using `@floor`
// here rounds it *up* since we negate it right outside. Moving it by
// whole pixels ensures that we don't disturb the pixel alignment of
// the glyph, fractional pixels will still be drawn on all sides as
// necessary.
const draw_x = -@floor(rect.origin.x);
const draw_y = -@floor(rect.origin.y);
// If the cell width was adjusted wider, we re-center all glyphs
// in the new width, so that they aren't weirdly off to the left.
if (metrics.original_cell_width) |original| recenter: {
// We don't do this if the constraint has a horizontal alignment,
// since in that case the position was already calculated with the
// new cell width in mind.
if (opts.constraint.align_horizontal != .none) break :recenter;
// We use `x` and `y` for our full pixel bearings post-raster.
// We need to subtract the fractional pixel of difference from
// the edge of the draw area to the edge of the actual glyph.
const frac_x = rect.origin.x + draw_x;
const frac_y = rect.origin.y + draw_y;
const x = glyph_size.x - frac_x;
const y = glyph_size.y - frac_y;
// If the original width was wider then we don't do anything.
if (original >= metrics.cell_width) break :recenter;
// We never modify the width.
//
// When using the CoreText option the widths do seem to be
// modified extremely subtly, but even at very small font
// sizes it's hardly a noticeable difference.
const width = glyph_size.width;
// We add half the difference to re-center.
x += (cell_width - @as(f64, @floatFromInt(original))) / 2;
}
// If the top of the glyph (taking in to account the y position)
// is within half a pixel of an exact pixel edge, we round up the
// height, otherwise leave it alone.
//
// This seems to match what CoreText does.
const frac_top = (glyph_size.height + frac_y) - @floor(glyph_size.height + frac_y);
const height =
if (should_quantize)
if (frac_top >= 0.5)
glyph_size.height + 1 - frac_top
else
glyph_size.height
else
glyph_size.height;
// Our whole-pixel bearings for the final glyph.
// The fractional portion will be included in the rasterized position.
const px_x: i32 = @intFromFloat(@floor(x));
const px_y: i32 = @intFromFloat(@floor(y));
// We offset our glyph by its bearings when we draw it, so that it's
// rendered fully inside our canvas area, but we make sure to keep the
// fractional pixel offset so that we rasterize with the appropriate
// sub-pixel position.
const frac_x = x - @floor(x);
const frac_y = y - @floor(y);
const draw_x = -rect.origin.x + frac_x;
const draw_y = -rect.origin.y + frac_y;
// Add the fractional pixel to the width and height and take
// the ceiling to get a canvas size that will definitely fit
@ -511,7 +498,9 @@ pub const Face = struct {
context.setAllowsFontSubpixelPositioning(ctx, true);
context.setShouldSubpixelPositionFonts(ctx, true);
// See comments about quantization earlier in the function.
// We don't want subpixel quantization, since we very carefully
// manage the position of our glyphs ourselves, and dont want to
// mess that up.
context.setAllowsFontSubpixelQuantization(ctx, false);
context.setShouldSubpixelQuantizeFonts(ctx, false);
@ -553,46 +542,11 @@ pub const Face = struct {
// This should be the distance from the bottom of
// the cell to the top of the glyph's bounding box.
const offset_y: i32 = @as(i32, @intFromFloat(@round(y))) + @as(i32, @intCast(px_height));
const offset_y: i32 = px_y + @as(i32, @intCast(px_height));
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
// If the glyph's advance is narrower than the cell width then we
// center the advance of the glyph within the cell width. At first
// I implemented this to proportionally scale the center position
// of the glyph but that messes up glyphs that are meant to align
// vertically with others, so this is a compromise.
//
// This makes it so that when the `adjust-cell-width` config is
// used, or when a fallback font with a different advance width
// is used, we don't get weirdly aligned glyphs.
//
// We don't do this if the constraint has a horizontal alignment,
// since in that case the position was already calculated with the
// new cell width in mind.
if (opts.constraint.align_horizontal == .none) {
const advance = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, null);
const new_advance =
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
// If the original advance is greater than the cell width then
// it's possible that this is a ligature or other glyph that is
// intended to overflow the cell to one side or the other, and
// adjusting the bearings could mess that up, so we just leave
// it alone if that's the case.
//
// We also don't want to do anything if the advance is zero or
// less, since this is used for stuff like combining characters.
if (advance > new_advance or advance <= 0.0) {
break :offset_x @intFromFloat(@round(x));
}
break :offset_x @intFromFloat(
@round(x + (new_advance - advance) / 2),
);
} else {
break :offset_x @intFromFloat(@round(x));
}
};
const offset_x: i32 = px_x;
return .{
.width = px_width,

View File

@ -156,23 +156,58 @@ pub const Face = struct {
/// but sometimes allocation isn't required and a static string is
/// returned.
pub fn name(self: *const Face, buf: []u8) Allocator.Error![]const u8 {
// We don't use this today but its possible the table below
// returns UTF-16 in which case we'd want to use this for conversion.
_ = buf;
const count = self.face.getSfntNameCount();
// We look for the font family entry.
for (0..count) |i| {
const entry = self.face.getSfntName(i) catch continue;
if (entry.name_id == freetype.c.TT_NAME_ID_FONT_FAMILY) {
return entry.string[0..entry.string_len];
const string = entry.string[0..entry.string_len];
// There are other encodings that are something other than UTF-8
// but this is one we've seen "in the wild" so far.
if (entry.platform_id == freetype.c.TT_PLATFORM_MICROSOFT and entry.encoding_id == freetype.c.TT_MS_ID_UNICODE_CS) skip: {
if (string.len % 2 != 0) break :skip;
if (string.len > 1024) break :skip;
var tmp: [512]u16 = undefined;
const max = string.len / 2;
for (@as([]const u16, @alignCast(@ptrCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c);
const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string;
return buf[0..len];
}
return string;
}
}
return "";
}
test "face name" {
const embedded = @import("../embedded.zig");
var lib: Library = try .init(testing.allocator);
defer lib.deinit();
{
var face: Face = try .init(lib, embedded.variable, .{ .size = .{ .points = 14 } });
defer face.deinit();
var buf: [1024]u8 = undefined;
const actual = try face.name(&buf);
try testing.expectEqualStrings("JetBrains Mono", actual);
}
{
var face: Face = try .init(lib, embedded.inconsolata, .{ .size = .{ .points = 14 } });
defer face.deinit();
var buf: [1024]u8 = undefined;
const actual = try face.name(&buf);
try testing.expectEqualStrings("Inconsolata", actual);
}
}
/// Return a new face that is the same as this but also has synthetic
/// bold applied.
pub fn syntheticBold(self: *const Face, opts: font.face.Options) !Face {
@ -328,19 +363,11 @@ pub const Face = struct {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
// We enable hinting by default, and disable it if either of the
// constraint alignments are not center or none, since this means
// that the glyph needs to be aligned flush to the cell edge, and
// hinting can mess that up.
const do_hinting = self.load_flags.hinting and
switch (opts.constraint.align_horizontal) {
.start, .end => false,
.center, .none => true,
} and
switch (opts.constraint.align_vertical) {
.start, .end => false,
.center, .none => true,
};
// Hinting should only be enabled if the configured load flags specify
// it and the provided constraint doesn't actually do anything, since
// if it does, then it'll mess up the hinting anyway when it moves or
// resizes the glyph.
const do_hinting = self.load_flags.hinting and !opts.constraint.doesAnything();
// Load the glyph.
try self.face.loadGlyph(glyph_index, .{
@ -356,6 +383,11 @@ pub const Face = struct {
.force_autohint = self.load_flags.@"force-autohint",
.no_autohint = !self.load_flags.autohint,
// If we're gonna be rendering this glyph in monochrome,
// then we should use the monochrome hinter as well, or
// else it won't look very good at all.
.target_mono = self.load_flags.monochrome,
// NO_SVG set to true because we don't currently support rendering
// SVG glyphs under FreeType, since that requires bundling another
// dependency to handle rendering the SVG.
@ -363,14 +395,45 @@ pub const Face = struct {
});
const glyph = self.face.handle.*.glyph;
const glyph_width: f64 = f26dot6ToF64(glyph.*.metrics.width);
const glyph_height: f64 = f26dot6ToF64(glyph.*.metrics.height);
// We get a rect that represents the position
// and size of the glyph before any changes.
const rect: struct {
x: f64,
y: f64,
width: f64,
height: f64,
} = metrics: {
// If we're dealing with an outline glyph then we get the
// outline's bounding box instead of using the built-in
// metrics, since that's more precise and allows better
// cell-fitting.
if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) {
// Get the glyph's bounding box before we transform it at all.
// We use this rather than the metrics, since it's more precise.
var bbox: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox);
break :metrics .{
.x = f26dot6ToF64(bbox.xMin),
.y = f26dot6ToF64(bbox.yMin),
.width = f26dot6ToF64(bbox.xMax - bbox.xMin),
.height = f26dot6ToF64(bbox.yMax - bbox.yMin),
};
}
break :metrics .{
.x = f26dot6ToF64(glyph.*.metrics.horiBearingX),
.y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height),
.width = f26dot6ToF64(glyph.*.metrics.width),
.height = f26dot6ToF64(glyph.*.metrics.height),
};
};
// If our glyph is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
//
// In this case we just return 0-sized glyph struct.
if (glyph_width < 0.25 or glyph_height < 0.25)
if (rect.width < 0.25 or rect.height < 0.25)
return font.Glyph{
.width = 0,
.height = 0,
@ -391,31 +454,70 @@ pub const Face = struct {
_ = freetype.c.FT_Outline_Embolden(&glyph.*.outline, @intFromFloat(amount));
}
// Next we need to apply any constraints.
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width);
// const cell_height: f64 = @floatFromInt(metrics.cell_height);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
const glyph_y: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingY) - glyph_height;
// Next we apply any constraints to get the final size of the glyph.
var constraint = opts.constraint;
const glyph_size = opts.constraint.constrain(
// We eliminate any negative vertical padding since these overlap
// values aren't needed with how precisely we apply constraints,
// and they can lead to extra height that looks bad for things like
// powerline glyphs.
constraint.pad_top = @max(0.0, constraint.pad_top);
constraint.pad_bottom = @max(0.0, constraint.pad_bottom);
// We need to add the baseline position before passing to the constrain
// function since it operates on cell-relative positions, not baseline.
const cell_baseline: f64 = @floatFromInt(metrics.cell_baseline);
const glyph_size = constraint.constrain(
.{
.width = glyph_width,
.height = glyph_height,
.x = glyph_x,
.y = glyph_y + @as(f64, @floatFromInt(metrics.cell_baseline)),
.width = rect.width,
.height = rect.height,
.x = rect.x,
.y = rect.y + cell_baseline,
},
metrics,
opts.constraint_width,
);
const width = glyph_size.width;
const height = glyph_size.height;
// This may need to be adjusted later on.
var width = glyph_size.width;
var height = glyph_size.height;
var x = glyph_size.x;
const y = glyph_size.y;
var y = glyph_size.y;
// If this is a bitmap glyph, it will always render as full pixels,
// not fractional pixels, so we need to quantize its position and
// size accordingly to align to full pixels so we get good results.
if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_BITMAP) {
width = cell_width - @round(cell_width - width - x) - @round(x);
height = cell_height - @round(cell_height - height - y) - @round(y);
x = @round(x);
y = @round(y);
}
// If the cell width was adjusted wider, we re-center all glyphs
// in the new width, so that they aren't weirdly off to the left.
if (metrics.original_cell_width) |original| recenter: {
// We don't do this if the constraint has a horizontal alignment,
// since in that case the position was already calculated with the
// new cell width in mind.
if (opts.constraint.align_horizontal != .none) break :recenter;
// If the original width was wider then we don't do anything.
if (original >= metrics.cell_width) break :recenter;
// We add half the difference to re-center.
//
// NOTE: We round this to a whole-pixel amount because under
// FreeType, the outlines will be hinted, which isn't
// the case under CoreText. If we move the outlines by
// a non-whole-pixel amount, it completely ruins the
// hinting.
x += @round((cell_width - @as(f64, @floatFromInt(original))) / 2);
}
// Now we can render the glyph.
var bitmap: freetype.c.FT_Bitmap = undefined;
@ -429,8 +531,8 @@ pub const Face = struct {
// matrix, since that has 16.16 coefficients, and also I was having
// weird issues that I can only assume where due to freetype doing
// some bad caching or something when I did this using the matrix.
const scale_x = width / glyph_width;
const scale_y = height / glyph_height;
const scale_x = width / rect.width;
const scale_y = height / rect.height;
const skew: f64 =
if (self.synthetic.italic)
// We skew by 12 degrees to synthesize italics.
@ -438,19 +540,24 @@ pub const Face = struct {
else
0.0;
var bbox_before: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox_before);
const outline = &glyph.*.outline;
for (outline.points[0..@intCast(outline.n_points)]) |*p| {
// Convert to f64 for processing
var px = f26dot6ToF64(p.x);
var py = f26dot6ToF64(p.y);
// Subtract original bearings
px -= rect.x;
py -= rect.y;
// Scale
px *= scale_x;
py *= scale_y;
// Add new bearings
px += x;
py += y - cell_baseline;
// Skew
px += py * skew;
@ -459,16 +566,6 @@ pub const Face = struct {
p.y = @as(i32, @bitCast(F26Dot6.from(py)));
}
var bbox_after: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox_after);
// If our bounding box changed, account for the lsb difference.
//
// This can happen when we skew glyphs that have a bit sticking
// out to the left higher up, like the top of the T or the serif
// on the lower case l in many monospace fonts.
x += f26dot6ToF64(bbox_after.xMin) - f26dot6ToF64(bbox_before.xMin);
try self.face.renderGlyph(
if (self.load_flags.monochrome)
.mono
@ -566,6 +663,10 @@ pub const Face = struct {
) != 0) {
return error.BitmapHandlingError;
}
// Update the bearings to account for the new positioning.
glyph.*.bitmap_top = @intFromFloat(@floor(y - cell_baseline + height));
glyph.*.bitmap_left = @intFromFloat(@floor(x));
},
else => |f| {
@ -600,6 +701,20 @@ pub const Face = struct {
},
}
// Our whole-pixel bearings for the final glyph.
// The fractional portion will be included in the rasterized position.
//
// For the Y position, FreeType's `bitmap_top` is the distance from the
// baseline to the top of the glyph, but we need the distance from the
// bottom of the cell to the bottom of the glyph, so first we add the
// baseline to get the distance from the bottom of the cell to the top
// of the glyph, then we subtract the height of the glyph to get the
// bottom.
const px_x: i32 = glyph.*.bitmap_left;
const px_y: i32 = glyph.*.bitmap_top +
@as(i32, @intCast(metrics.cell_baseline)) -
@as(i32, @intCast(bitmap.rows));
const px_width = bitmap.width;
const px_height = bitmap.rows;
const len: usize = @intCast(
@ -635,48 +750,11 @@ pub const Face = struct {
// This should be the distance from the bottom of
// the cell to the top of the glyph's bounding box.
const offset_y: i32 =
@as(i32, @intFromFloat(@floor(y))) +
@as(i32, @intCast(px_height));
const offset_y: i32 = px_y + @as(i32, @intCast(px_height));
// This should be the distance from the left of
// the cell to the left of the glyph's bounding box.
const offset_x: i32 = offset_x: {
// If the glyph's advance is narrower than the cell width then we
// center the advance of the glyph within the cell width. At first
// I implemented this to proportionally scale the center position
// of the glyph but that messes up glyphs that are meant to align
// vertically with others, so this is a compromise.
//
// This makes it so that when the `adjust-cell-width` config is
// used, or when a fallback font with a different advance width
// is used, we don't get weirdly aligned glyphs.
//
// We don't do this if the constraint has a horizontal alignment,
// since in that case the position was already calculated with the
// new cell width in mind.
if (opts.constraint.align_horizontal == .none) {
const advance = f26dot6ToFloat(glyph.*.advance.x);
const new_advance =
cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1));
// If the original advance is greater than the cell width then
// it's possible that this is a ligature or other glyph that is
// intended to overflow the cell to one side or the other, and
// adjusting the bearings could mess that up, so we just leave
// it alone if that's the case.
//
// We also don't want to do anything if the advance is zero or
// less, since this is used for stuff like combining characters.
if (advance > new_advance or advance <= 0.0) {
break :offset_x @intFromFloat(@floor(x));
}
break :offset_x @intFromFloat(
@floor(x + (new_advance - advance) / 2),
);
} else {
break :offset_x @intFromFloat(@floor(x));
}
};
const offset_x: i32 = px_x;
return Glyph{
.width = px_width,

View File

@ -1833,7 +1833,10 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
grid_ptr.* = try .init(alloc, .{ .collection = c });
errdefer grid_ptr.*.deinit(alloc);
var shaper = try Shaper.init(alloc, .{});
var shaper = try Shaper.init(alloc, .{
// Some of our tests rely on dlig being enabled by default
.features = &.{"dlig"},
});
errdefer shaper.deinit();
return TestShaper{

View File

@ -287,7 +287,6 @@ pub const FeatureList = struct {
/// These features are hardcoded to always be on by default. Users
/// can turn them off by setting the features to "-liga" for example.
pub const default_features = [_]Feature{
.{ .tag = "dlig".*, .value = 1 },
.{ .tag = "liga".*, .value = 1 },
};

View File

@ -1296,7 +1296,10 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
grid_ptr.* = try .init(alloc, .{ .collection = c });
errdefer grid_ptr.*.deinit(alloc);
var shaper = try Shaper.init(alloc, .{});
var shaper = try Shaper.init(alloc, .{
// Some of our tests rely on dlig being enabled by default
.features = &.{"dlig"},
});
errdefer shaper.deinit();
return TestShaper{

View File

@ -247,7 +247,7 @@ pub const RunIterator = struct {
if (j == self.i) current_font = font_info.idx;
// If our fonts are not equal, then we're done with our run.
if (font_info.idx.int() != current_font.int()) break;
if (font_info.idx != current_font) break;
// If we're a fallback character, add that and continue; we
// don't want to add the entire grapheme.

View File

@ -5,6 +5,7 @@ const Binding = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const build_config = @import("../build_config.zig");
const ziglyph = @import("ziglyph");
const key = @import("key.zig");
const KeyEvent = key.KeyEvent;
@ -729,6 +730,16 @@ pub const Action = union(enum) {
pub const Key = @typeInfo(Action).@"union".tag_type.?;
/// Make this a valid gobject if we're in a GTK environment.
pub const getGObjectType = switch (build_config.app_runtime) {
.gtk, .@"gtk-ng" => @import("gobject").ext.defineBoxed(
Action,
.{ .name = "GhosttyBindingAction" },
),
.none => void,
};
pub const CrashThread = enum {
main,
io,

View File

@ -756,7 +756,7 @@ fn renderSizeWindow(self: *Inspector) void {
{
_ = cimgui.c.igTableSetColumnIndex(1);
cimgui.c.igText(
"%d px",
"%.2f px",
self.surface.font_size.pixels(),
);
}

View File

@ -73,6 +73,8 @@ pub const InitError = error{
/// want to set the domain for the entire application since this is also
/// used by libghostty.
pub fn init(resources_dir: []const u8) InitError!void {
if (comptime !build_config.i18n) return;
switch (builtin.os.tag) {
// i18n is unsupported on Windows
.windows => return,
@ -102,11 +104,13 @@ pub fn init(resources_dir: []const u8) InitError!void {
/// This should only be called for apprts that are fully owning the
/// Ghostty application. This should not be called for libghostty users.
pub fn initGlobalDomain() error{OutOfMemory}!void {
if (comptime !build_config.i18n) return;
_ = textdomain(build_config.bundle_id) orelse return error.OutOfMemory;
}
/// Translate a message for the Ghostty domain.
pub fn _(msgid: [*:0]const u8) [*:0]const u8 {
if (comptime !build_config.i18n) return msgid;
return dgettext(build_config.bundle_id, msgid);
}
@ -132,8 +136,15 @@ pub fn canonicalizeLocale(
buf: []u8,
locale: []const u8,
) error{NoSpaceLeft}![:0]const u8 {
if (comptime !build_config.i18n) return locale;
// Fix zh locales for macOS
if (fixZhLocale(locale)) |fixed| return fixed;
if (fixZhLocale(locale)) |fixed| {
if (buf.len < fixed.len + 1) return error.NoSpaceLeft;
@memcpy(buf[0..fixed.len], fixed);
buf[fixed.len] = 0;
return buf[0..fixed.len :0];
}
// Buffer must be 16 or at least as long as the locale and null term
if (buf.len < @max(16, locale.len + 1)) return error.NoSpaceLeft;

View File

@ -229,24 +229,39 @@ pub fn isCovering(cp: u21) bool {
};
}
/// Returns true of the codepoint is a "symbol-like" character, which
/// for now we define as anything in a private use area and anything
/// in the "dingbats" unicode block.
///
/// In the future it may be prudent to expand this to encompass more
/// symbol-like characters, and/or exclude some PUA sections.
pub fn isSymbol(cp: u21) bool {
return uucode.get(.general_category, cp) == .other_private_use or
uucode.get(.block, cp) == .dingbats;
}
/// Returns the appropriate `constraint_width` for
/// the provided cell when rendering its glyph(s).
pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
const cell = cell_pin.rowAndCell().cell;
const cp = cell.codepoint();
// If not a Co (Private Use) and not a Dingbats, use grid width.
if (uucode.get("general_category", cp) != .Co and
uucode.get("block", cp) != .dingbats)
{
return cell.gridWidth();
}
const grid_width = cell.gridWidth();
// If the grid width of the cell is 2, the constraint
// width will always be 2, so we can just return early.
if (grid_width > 1) return grid_width;
// We allow "symbol-like" glyphs to extend to 2 cells wide if there's
// space, and if the previous glyph wasn't also a symbol. So if this
// codepoint isn't a symbol then we can return the grid width.
if (!isSymbol(cp)) return grid_width;
// If we are at the end of the screen it must be constrained to one cell.
if (cell_pin.x == cell_pin.node.data.size.cols - 1) return 1;
// If we have a previous cell and it was PUA then we need to
// also constrain. This is so that multiple PUA glyphs align.
// If we have a previous cell and it was a symbol then we need
// to also constrain. This is so that multiple PUA glyphs align.
// As an exception, we ignore powerline glyphs since they are
// used for box drawing and we consider them whitespace.
if (cell_pin.x > 0) prev: {
@ -260,14 +275,13 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
// We consider powerline glyphs whitespace.
if (isPowerline(prev_cp)) break :prev;
// If it's Private Use (Co) use 1 as the width.
if (uucode.get("general_category", prev_cp) == .Co) {
if (isSymbol(prev_cp)) {
return 1;
}
}
// If the next cell is whitespace, then
// we allow it to be up to two cells wide.
// If the next cell is whitespace, then we
// allow the glyph to be up to two cells wide.
const next_cp = next_cp: {
var copy = cell_pin;
copy.x += 1;
@ -281,7 +295,7 @@ pub fn constraintWidth(cell_pin: terminal.Pin) u2 {
return 2;
}
// Must be constrained
// Otherwise, this has to be 1 cell wide.
return 1;
}

View File

@ -13,6 +13,66 @@
# You must gracefully exit Ghostty (do not SIGINT) by closing all windows
# and quitting. Otherwise, we leave a number of GTK resources around.
# Reproduction:
# 1. Launch Ghostty (no config)
# 2. Right Click on the terminal
# 3. Hover over "Split" to get a submenu
# 4. Close menu by clicking away
# 5. Exit
#
# The menu model and popover are fully defined in the blueprint so I don't
# THINK we need to do any manual unrefing. But there's a lot of leaks here
# so if someone wants to take a closer look I'd appreciate it.
{
GTK PopOver Menu Model Leak
Memcheck:Leak
match-leak-kinds: possible
...
fun:gtk_menu_section_box_insert_func
...
fun:gtk_popover_menu_set_menu_model
...
}
{
GTK/Blueprint Popover GSK Transform
Memcheck:Leak
match-leak-kinds: possible
...
fun:gtk_popover_size_allocate
fun:gtk_widget_allocate
fun:gtk_popover_native_layout
...
}
# Reproduction:
#
# 1. Launch Ghostty
# 2. Split Right
# 3. Hit "X" to close
{
GTK CSS Node State
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
fun:g_malloc
fun:g_memdup2
fun:gtk_css_node_declaration_set_state
fun:gtk_css_node_set_state
fun:gtk_widget_propagate_state
fun:gtk_widget_update_state_flags
fun:gtk_main_do_event
fun:surface_event
fun:_gdk_marshal_BOOLEAN__POINTERv
fun:gdk_surface_event_marshallerv
fun:_g_closure_invoke_va
fun:signal_emit_valist_unlocked
fun:g_signal_emit_valist
fun:g_signal_emit
fun:gdk_surface_handle_event
...
}
{
GTK CSS Provider Leak
Memcheck:Leak
@ -484,9 +544,7 @@
pango font map
Memcheck:Leak
match-leak-kinds: possible
fun:calloc
fun:g_malloc0
fun:g_rc_box_alloc_full
...
fun:pango_fc_font_map_load_fontset
...
}
@ -840,6 +898,26 @@
fun:FcConfigSubstituteWithPat
}
{
FcConfigValues
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
obj:/usr/lib*/libfontconfig.so*
obj:/usr/lib*/libfontconfig.so*
fun:FcConfigValues
}
{
FcValueSave
Memcheck:Leak
match-leak-kinds: possible
fun:malloc
obj:/usr/lib*/libfontconfig.so*
obj:/usr/lib*/libfontconfig.so*
fun:FcValueSave
}
# Pixman
{
pixman_image_composite32