Merge remote-tracking branch 'upstream/main' into jacob/uucode
commit
563cfb94ba
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -21,3 +21,5 @@ glad.zip
|
|||
/Box_test.ppm
|
||||
/Box_test_diff.ppm
|
||||
/ghostty.qcow2
|
||||
|
||||
vgcore.*
|
||||
|
|
|
|||
16
build.zig
16
build.zig
|
|
@ -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", .{});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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."),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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(.{});
|
||||
|
|
|
|||
|
|
@ -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(.{});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(.{});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
.transparent {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.window .split paned > separator {
|
||||
background-color: rgba(36, 36, 36, 1);
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1121,7 +1121,7 @@ fn gtkActionToggleCommandPalette(
|
|||
_: *gio.SimpleAction,
|
||||
_: ?*glib.Variant,
|
||||
self: *Window,
|
||||
) callconv(.C) void {
|
||||
) callconv(.c) void {
|
||||
self.performBindingAction(.toggle_command_palette);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue