mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-06 11:28:27 +00:00
Compare commits
131 Commits
quick-term
...
tip
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e4c3a56242 | ||
![]() |
cf39d5c512 | ||
![]() |
6b21662219 | ||
![]() |
8824256059 | ||
![]() |
15777050f3 | ||
![]() |
12bd7baaeb | ||
![]() |
0333a6f1d2 | ||
![]() |
ef822612d3 | ||
![]() |
a88e6cd428 | ||
![]() |
1ef220a679 | ||
![]() |
0492cd16fa | ||
![]() |
587f47a587 | ||
![]() |
d10e474860 | ||
![]() |
4552ea9104 | ||
![]() |
19b76df80e | ||
![]() |
e024b77ad5 | ||
![]() |
a7da96faee | ||
![]() |
93debc439c | ||
![]() |
bb78adbd93 | ||
![]() |
968b9d536d | ||
![]() |
ac52af27d3 | ||
![]() |
8a2ab8ff21 | ||
![]() |
ee573ebd36 | ||
![]() |
e2504d9cbf | ||
![]() |
93744a4002 | ||
![]() |
a590194cd7 | ||
![]() |
3ac2da99f4 | ||
![]() |
43ee3cc8c6 | ||
![]() |
aeae54072c | ||
![]() |
5c1d87fda6 | ||
![]() |
7c4b45ecee | ||
![]() |
2464728851 | ||
![]() |
c3e7857a2c | ||
![]() |
e67db2a01c | ||
![]() |
befee07f16 | ||
![]() |
c8243ffd99 | ||
![]() |
084ff2de67 | ||
![]() |
e1f3f52686 | ||
![]() |
fe3dab9467 | ||
![]() |
b90c72aea6 | ||
![]() |
e6d60dee07 | ||
![]() |
508e36bc03 | ||
![]() |
6a9b8b70cc | ||
![]() |
1dee9e7cb2 | ||
![]() |
291d4ed423 | ||
![]() |
5eb69b405d | ||
![]() |
7d5be8e960 | ||
![]() |
d05ec81b86 | ||
![]() |
f016b79f22 | ||
![]() |
52f5ab1a36 | ||
![]() |
8d11c08db3 | ||
![]() |
90c0fc2590 | ||
![]() |
e909e28876 | ||
![]() |
cf0390bab5 | ||
![]() |
4614e5fdad | ||
![]() |
ce94bb9f6a | ||
![]() |
ac104a3dfc | ||
![]() |
16e47e7586 | ||
![]() |
e8217aa007 | ||
![]() |
9aa1698e5a | ||
![]() |
3664ee9f87 | ||
![]() |
a72995590b | ||
![]() |
2bf0d3f4c7 | ||
![]() |
4af290d5f0 | ||
![]() |
ef7857f9be | ||
![]() |
7dcf2c9b62 | ||
![]() |
d316449ebf | ||
![]() |
650028fa9f | ||
![]() |
5ef6412823 | ||
![]() |
0b58830882 | ||
![]() |
a41ec17b61 | ||
![]() |
c535d0a664 | ||
![]() |
2009ea511d | ||
![]() |
d8578a9ee2 | ||
![]() |
0d30f859bd | ||
![]() |
650095e7e9 | ||
![]() |
6319464cfb | ||
![]() |
fc6266133f | ||
![]() |
a51a956bdb | ||
![]() |
c94805f0aa | ||
![]() |
937d17cc35 | ||
![]() |
0bc90b2a20 | ||
![]() |
75e3835a9e | ||
![]() |
d1e01ec5c3 | ||
![]() |
b0d9b0dee0 | ||
![]() |
e6b019b197 | ||
![]() |
5761f66f35 | ||
![]() |
a5eef1d227 | ||
![]() |
85e642097a | ||
![]() |
bed350f0be | ||
![]() |
460fcc1344 | ||
![]() |
f91e6f1764 | ||
![]() |
f802d33652 | ||
![]() |
2701932475 | ||
![]() |
6cfd89e248 | ||
![]() |
9962e523a8 | ||
![]() |
04956f3dc1 | ||
![]() |
f1ea30dcf1 | ||
![]() |
d3cadf2495 | ||
![]() |
7106d71a42 | ||
![]() |
f54f2dc7f3 | ||
![]() |
5013d028a3 | ||
![]() |
adfc93047c | ||
![]() |
a3f4997fbc | ||
![]() |
56d3fd872e | ||
![]() |
58e85bf133 | ||
![]() |
19a27383f8 | ||
![]() |
8fa065512f | ||
![]() |
6530107e3b | ||
![]() |
87056a2600 | ||
![]() |
6a128189e3 | ||
![]() |
2490171304 | ||
![]() |
31c96d906a | ||
![]() |
64d8492836 | ||
![]() |
c1ab41afac | ||
![]() |
f047db6a3b | ||
![]() |
cd8455c24b | ||
![]() |
4d6269a859 | ||
![]() |
3fb17dc802 | ||
![]() |
6cf636b1ad | ||
![]() |
673afd193b | ||
![]() |
ff61cad1e2 | ||
![]() |
520eaec61c | ||
![]() |
e676eae640 | ||
![]() |
830194d436 | ||
![]() |
6f630a27be | ||
![]() |
9c725187e1 | ||
![]() |
35102ddb5a | ||
![]() |
f7994e6412 | ||
![]() |
54b56af570 | ||
![]() |
53b029284d |
64
.agents/commands/gh-issue
Executable file
64
.agents/commands/gh-issue
Executable file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env nu
|
||||
|
||||
# A command to generate an agent prompt to diagnose and formulate
|
||||
# a plan for resolving a GitHub issue.
|
||||
#
|
||||
# IMPORTANT: This command is prompted to NOT write any code and to ONLY
|
||||
# produce a plan. You should still be vigilant when running this but that
|
||||
# is the expected behavior.
|
||||
#
|
||||
# The `<issue>` parameter can be either an issue number or a full GitHub
|
||||
# issue URL.
|
||||
def main [
|
||||
issue: any, # Ghostty issue number or URL
|
||||
--repo: string = "ghostty-org/ghostty" # GitHub repository in the format "owner/repo"
|
||||
] {
|
||||
# TODO: This whole script doesn't handle errors very well. I actually
|
||||
# don't know Nu well enough to know the proper way to handle it all.
|
||||
|
||||
let issueData = gh issue view $issue --json author,title,number,body,comments | from json
|
||||
let comments = $issueData.comments | each { |comment|
|
||||
$"
|
||||
### Comment by ($comment.author.login)
|
||||
($comment.body)
|
||||
" | str trim
|
||||
} | str join "\n\n"
|
||||
|
||||
$"
|
||||
Deep-dive on this GitHub issue. Find the problem and generate a plan.
|
||||
Do not write code. Explain the problem clearly and propose a comprehensive plan
|
||||
to solve it.
|
||||
|
||||
# ($issueData.title) \(($issueData.number)\)
|
||||
|
||||
## Description
|
||||
($issueData.body)
|
||||
|
||||
## Comments
|
||||
($comments)
|
||||
|
||||
## Your Tasks
|
||||
|
||||
You are an experienced software developer tasked with diagnosing issues.
|
||||
|
||||
1. Review the issue context and details.
|
||||
2. Examine the relevant parts of the codebase. Analyze the code thoroughly
|
||||
until you have a solid understanding of how it works.
|
||||
3. Explain the issue in detail, including the problem and its root cause.
|
||||
4. Create a comprehensive plan to solve the issue. The plan should include:
|
||||
- Required code changes
|
||||
- Potential impacts on other parts of the system
|
||||
- Necessary tests to be written or updated
|
||||
- Documentation updates
|
||||
- Performance considerations
|
||||
- Security implications
|
||||
- Backwards compatibility \(if applicable\)
|
||||
- Include the reference link to the source issue and any related discussions
|
||||
4. Think deeply about all aspects of the task. Consider edge cases, potential
|
||||
challenges, and best practices for addressing the issue. Review the plan
|
||||
with the oracle and adjust it based on its feedback.
|
||||
|
||||
**ONLY CREATE A PLAN. DO NOT WRITE ANY CODE.** Your task is to create
|
||||
a thorough, comprehensive strategy for understanding and resolving the issue.
|
||||
" | str trim
|
||||
}
|
4
.github/workflows/nix.yml
vendored
4
.github/workflows/nix.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
4
.github/workflows/release-tag.yml
vendored
4
.github/workflows/release-tag.yml
vendored
@@ -83,13 +83,13 @@ jobs:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
|
||||
|
4
.github/workflows/release-tip.yml
vendored
4
.github/workflows/release-tip.yml
vendored
@@ -107,12 +107,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
186
.github/workflows/test.yml
vendored
186
.github/workflows/test.yml
vendored
@@ -13,6 +13,7 @@ jobs:
|
||||
- build-bench
|
||||
- build-dist
|
||||
- build-flatpak
|
||||
- build-freebsd
|
||||
- build-linux
|
||||
- build-linux-libghostty
|
||||
- build-nix
|
||||
@@ -22,7 +23,6 @@ jobs:
|
||||
- build-windows
|
||||
- test
|
||||
- test-gtk
|
||||
- test-gtk-ng
|
||||
- test-sentry-linux
|
||||
- test-macos
|
||||
- pinact
|
||||
@@ -69,14 +69,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -100,14 +100,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -136,14 +136,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -165,14 +165,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -198,14 +198,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -242,14 +242,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -377,7 +377,7 @@ jobs:
|
||||
mkdir dist
|
||||
tar --verbose --extract --strip-components 1 --directory dist --file ghostty-source.tar.gz
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
@@ -473,14 +473,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -494,9 +494,6 @@ jobs:
|
||||
- name: Test GTK Build
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk -Demit-docs -Demit-webdata
|
||||
|
||||
- name: Test GTK-NG Build
|
||||
run: nix develop -c zig build -Dapp-runtime=gtk-ng -Demit-docs -Demit-webdata
|
||||
|
||||
# This relies on the cache being populated by the commands above.
|
||||
- name: Test System Build
|
||||
run: nix develop -c zig build --system ${ZIG_GLOBAL_CACHE_DIR}/p
|
||||
@@ -518,14 +515,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -550,55 +547,6 @@ jobs:
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }}
|
||||
|
||||
test-gtk-ng:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
x11: ["true", "false"]
|
||||
wayland: ["true", "false"]
|
||||
name: GTK x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Test
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
-Dapp-runtime=gtk-ng \
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }} \
|
||||
test
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
nix develop -c \
|
||||
zig build \
|
||||
-Dapp-runtime=gtk-ng \
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }}
|
||||
|
||||
test-sentry-linux:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -615,14 +563,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -673,12 +621,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -701,12 +649,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -728,12 +676,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -755,12 +703,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -782,12 +730,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -809,12 +757,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -843,12 +791,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -870,12 +818,12 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -905,14 +853,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -992,14 +940,14 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
- uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
@@ -1015,3 +963,57 @@ jobs:
|
||||
- name: valgrind
|
||||
run: |
|
||||
nix develop -c zig build test-valgrind
|
||||
|
||||
build-freebsd:
|
||||
name: Build on FreeBSD
|
||||
needs: test
|
||||
runs-on: namespace-profile-mitchellh-sm-systemd
|
||||
strategy:
|
||||
matrix:
|
||||
release:
|
||||
- "14.3"
|
||||
# - "15.0" # disable until fixed: https://github.com/vmactions/freebsd-vm/issues/108
|
||||
steps:
|
||||
- name: Checkout Ghostty
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Start SSH
|
||||
run: |
|
||||
sudo systemctl start ssh
|
||||
|
||||
- name: Set up FreeBSD VM
|
||||
uses: vmactions/freebsd-vm@05856381fab64eeee9b038a0818f6cec649ca17a # v1.2.3
|
||||
with:
|
||||
release: ${{ matrix.release }}
|
||||
copyback: false
|
||||
usesh: true
|
||||
prepare: |
|
||||
pkg install -y \
|
||||
devel/blueprint-compiler \
|
||||
devel/gettext \
|
||||
devel/git \
|
||||
devel/pkgconf \
|
||||
graphics/wayland \
|
||||
lang/zig \
|
||||
security/ca_root_nss \
|
||||
textproc/hs-pandoc \
|
||||
x11-fonts/jetbrains-mono \
|
||||
x11-toolkits/libadwaita \
|
||||
x11-toolkits/gtk40 \
|
||||
x11-toolkits/gtk4-layer-shell
|
||||
|
||||
run: |
|
||||
zig env
|
||||
|
||||
- name: Run tests
|
||||
shell: freebsd {0}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
zig build test
|
||||
|
||||
- name: Build GTK app runtime
|
||||
shell: freebsd {0}
|
||||
run: |
|
||||
cd $GITHUB_WORKSPACE
|
||||
zig build
|
||||
./zig-out/bin/ghostty +version
|
||||
|
4
.github/workflows/update-colorschemes.yml
vendored
4
.github/workflows/update-colorschemes.yml
vendored
@@ -22,14 +22,14 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@305bfa7ea980a858d511af4899414a84847c7991 # v1.2.16
|
||||
uses: namespacelabs/nscloud-cache-action@a289cf5d2fcd6874376aa92f0ef7f99dc923592a # v1.2.17
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
- name: Setup Nix
|
||||
uses: cachix/install-nix-action@fc6e360bedc9ee72d75e701397f0bb30dce77568 # v31.5.2
|
||||
uses: cachix/install-nix-action@56a7bb7b56d9a92d4fd1bc05758de7eea4a370a8 # v31.6.0
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
|
23
AGENTS.md
Normal file
23
AGENTS.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Agent Development Guide
|
||||
|
||||
A file for [guiding coding agents](https://agents.md/).
|
||||
|
||||
## Commands
|
||||
|
||||
- **Build:** `zig build`
|
||||
- **Test (Zig):** `zig build test`
|
||||
- **Test filter (Zig)**: `zig build test -Dtest-filter=<test name>`
|
||||
- **Formatting (Zig)**: `zig fmt .`
|
||||
- **Formatting (other)**: `prettier -w .`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
- Shared Zig core: `src/`
|
||||
- C API: `include/ghostty.h`
|
||||
- macOS app: `macos/`
|
||||
- GTK (Linux and FreeBSD) app: `src/apprt/gtk`
|
||||
|
||||
## macOS App
|
||||
|
||||
- Do not use `xcodebuild`
|
||||
- Use `zig build` to build the macOS app and any shared Zig code
|
@@ -119,7 +119,6 @@
|
||||
|
||||
# GTK
|
||||
/src/apprt/gtk/ @ghostty-org/gtk
|
||||
/src/apprt/gtk-ng/ @ghostty-org/gtk
|
||||
/src/os/cgroup.zig @ghostty-org/gtk
|
||||
/src/os/flatpak.zig @ghostty-org/gtk
|
||||
/dist/linux/ @ghostty-org/gtk
|
||||
|
270
CONTRIBUTING.md
270
CONTRIBUTING.md
@@ -1,9 +1,9 @@
|
||||
# Ghostty Development Process
|
||||
# Contributing to Ghostty
|
||||
|
||||
This document describes the development process for Ghostty. It is intended for
|
||||
anyone considering opening an **issue** or **pull request**. If in doubt,
|
||||
please open a [discussion](https://github.com/ghostty-org/ghostty/discussions);
|
||||
we can always convert that to an issue later.
|
||||
This document describes the process of contributing to Ghostty. It is intended
|
||||
for anyone considering opening an **issue**, **discussion** or **pull request**.
|
||||
For people who are interested in developing Ghostty and technical details behind
|
||||
it, please check out our ["Developing Ghostty"](HACKING.md) document as well.
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
@@ -45,17 +45,25 @@ work than any human. That isn't the world we live in today, and in most cases
|
||||
it's generating slop. I say this despite being a fan of and using them
|
||||
successfully myself (with heavy supervision)!
|
||||
|
||||
When using AI assistance, we expect contributors to understand the code
|
||||
that is produced and be able to answer critical questions about it. It
|
||||
isn't a maintainers job to review a PR so broken that it requires
|
||||
significant rework to be acceptable.
|
||||
|
||||
Please be respectful to maintainers and disclose AI assistance.
|
||||
|
||||
## Quick Guide
|
||||
|
||||
**I'd like to contribute!**
|
||||
### I'd like to contribute
|
||||
|
||||
All issues are actionable. Pick one and start working on it. Thank you.
|
||||
If you need help or guidance, comment on the issue. Issues that are extra
|
||||
friendly to new contributors are tagged with "contributor friendly".
|
||||
[All issues are actionable](#issues-are-actionable). Pick one and start
|
||||
working on it. Thank you. If you need help or guidance, comment on the issue.
|
||||
Issues that are extra friendly to new contributors are tagged with
|
||||
["contributor friendly"].
|
||||
|
||||
**I'd like to translate Ghostty to my language!**
|
||||
["contributor friendly"]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20is%3Aopen%20label%3A%22contributor%20friendly%22
|
||||
|
||||
### I'd like to translate Ghostty to my language
|
||||
|
||||
We have written a [Translator's Guide](po/README_TRANSLATORS.md) for
|
||||
everyone interested in contributing translations to Ghostty.
|
||||
@@ -64,25 +72,39 @@ and you can submit pull requests directly, although please make sure that
|
||||
our [Style Guide](po/README_TRANSLATORS.md#style-guide) is followed before
|
||||
submission.
|
||||
|
||||
**I have a bug!**
|
||||
### I have a bug! / Something isn't working
|
||||
|
||||
1. Search the issue tracker and discussions for similar issues.
|
||||
2. If you don't have steps to reproduce, open a discussion.
|
||||
3. If you have steps to reproduce, open an issue.
|
||||
1. Search the issue tracker and discussions for similar issues. Tip: also
|
||||
search for [closed issues] and [discussions] — your issue might have already
|
||||
been fixed!
|
||||
2. If your issue hasn't been reported already, open an ["Issue Triage" discussion]
|
||||
and make sure to fill in the template **completely**. They are vital for
|
||||
maintainers to figure out important details about your setup. Because of
|
||||
this, please make sure that you _only_ use the "Issue Triage" category for
|
||||
reporting bugs — thank you!
|
||||
|
||||
**I have an idea for a feature!**
|
||||
[closed issues]: https://github.com/ghostty-org/ghostty/issues?q=is%3Aissue%20state%3Aclosed
|
||||
[discussions]: https://github.com/ghostty-org/ghostty/discussions?discussions_q=is%3Aclosed
|
||||
["Issue Triage" discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=issue-triage
|
||||
|
||||
1. Open a discussion.
|
||||
### I have an idea for a feature
|
||||
|
||||
**I've implemented a feature!**
|
||||
Open a discussion in the ["Feature Requests, Ideas" category](https://github.com/ghostty-org/ghostty/discussions/new?category=feature-requests-ideas).
|
||||
|
||||
1. If there is an issue for the feature, open a pull request.
|
||||
### I've implemented a feature
|
||||
|
||||
1. If there is an issue for the feature, open a pull request straight away.
|
||||
2. If there is no issue, open a discussion and link to your branch.
|
||||
3. If you want to live dangerously, open a pull request and hope for the best.
|
||||
3. If you want to live dangerously, open a pull request and
|
||||
[hope for the best](#pull-requests-implement-an-issue).
|
||||
|
||||
**I have a question!**
|
||||
### I have a question
|
||||
|
||||
1. Open a discussion or use Discord.
|
||||
Open an [Q&A discussion], or join our [Discord Server] and ask away in the
|
||||
`#help` channel.
|
||||
|
||||
[Q&A discussion]: https://github.com/ghostty-org/ghostty/discussions/new?category=q-a
|
||||
[Discord Server]: https://discord.gg/ghostty
|
||||
|
||||
## General Patterns
|
||||
|
||||
@@ -120,209 +142,3 @@ pull request will be accepted with a high degree of certainty.
|
||||
> **Pull requests are NOT a place to discuss feature design.** Please do
|
||||
> not open a WIP pull request to discuss a feature. Instead, use a discussion
|
||||
> and link to your branch.
|
||||
|
||||
# Developer Guide
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> **The remainder of this file is dedicated to developers actively
|
||||
> working on Ghostty.** If you're a user reporting an issue, you can
|
||||
> ignore the rest of this document.
|
||||
|
||||
## Including and Updating Translations
|
||||
|
||||
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
|
||||
|
||||
## Checking for Memory Leaks
|
||||
|
||||
While Zig does an amazing job of finding and preventing memory leaks,
|
||||
Ghostty uses many third-party libraries that are written in C. Improper usage
|
||||
of those libraries or bugs in those libraries can cause memory leaks that
|
||||
Zig cannot detect by itself.
|
||||
|
||||
### On Linux
|
||||
|
||||
On Linux the recommended tool to check for memory leaks is Valgrind. The
|
||||
recommended way to run Valgrind is via `zig build`:
|
||||
|
||||
```sh
|
||||
zig build run-valgrind
|
||||
```
|
||||
|
||||
This builds a Ghostty executable with Valgrind support and runs Valgrind
|
||||
with the proper flags to ensure we're suppressing known false positives.
|
||||
|
||||
You can combine the same build args with `run-valgrind` that you can with
|
||||
`run`, such as specifying additional configurations after a trailing `--`.
|
||||
|
||||
## Input Stack Testing
|
||||
|
||||
The input stack is the part of the codebase that starts with a
|
||||
key event and ends with text encoding being sent to the pty (it
|
||||
does not include _rendering_ the text, which is part of the
|
||||
font or rendering stack).
|
||||
|
||||
If you modify any part of the input stack, you must manually verify
|
||||
all the following input cases work properly. We unfortunately do
|
||||
not automate this in any way, but if we can do that one day that'd
|
||||
save a LOT of grief and time.
|
||||
|
||||
Note: this list may not be exhaustive, I'm still working on it.
|
||||
|
||||
### Linux IME
|
||||
|
||||
IME (Input Method Editors) are a common source of bugs in the input stack,
|
||||
especially on Linux since there are multiple different IME systems
|
||||
interacting with different windowing systems and application frameworks
|
||||
all written by different organizations.
|
||||
|
||||
The following matrix should be tested to ensure that all IME input works
|
||||
properly:
|
||||
|
||||
1. Wayland, X11
|
||||
2. ibus, fcitx, none
|
||||
3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
|
||||
4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a **work in progress**. I'm still working on this list and it
|
||||
> is not complete. As I find more test cases, I will add them here.
|
||||
|
||||
#### Dead Key Input
|
||||
|
||||
Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `'`
|
||||
3. Press `a`
|
||||
4. Verify that `á` is displayed
|
||||
|
||||
Note that the dead key may or may not show a preedit state visually.
|
||||
For ibus and fcitx it does but for the "none" case it does not. Importantly,
|
||||
the text should be correct when it is sent to the pty.
|
||||
|
||||
We should also test canceling dead key input:
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `'`
|
||||
3. Press escape
|
||||
4. Press `a`
|
||||
5. Verify that `a` is displayed (no diacritic)
|
||||
|
||||
#### CJK Input
|
||||
|
||||
Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
|
||||
exact layout doesn't matter.
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `Ctrl+Shift` to switch to "Hiragana"
|
||||
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
|
||||
4. Press `Enter`
|
||||
5. Verify that `こん` is displayed in the terminal.
|
||||
|
||||
We should also test switching input methods while preedit is active, which
|
||||
should commit the text:
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `Ctrl+Shift` to switch to "Hiragana"
|
||||
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
|
||||
4. Press `Ctrl+Shift` to switch to another layout (any)
|
||||
5. Verify that `こん` is displayed in the terminal as committed text.
|
||||
|
||||
## Nix Virtual Machines
|
||||
|
||||
Several Nix virtual machine definitions are provided by the project for testing
|
||||
and developing Ghostty against multiple different Linux desktop environments.
|
||||
|
||||
Running these requires a working Nix installation, either Nix on your
|
||||
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
|
||||
requirements for macOS are detailed below.
|
||||
|
||||
VMs should only be run on your local desktop and then powered off when not in
|
||||
use, which will discard any changes to the VM.
|
||||
|
||||
The VM definitions provide minimal software "out of the box" but additional
|
||||
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
|
||||
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
|
||||
with `common` or `create`.
|
||||
3. The VM will build and then launch. Depending on the speed of your system, this
|
||||
can take a while, but eventually you should get a new VM window.
|
||||
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
|
||||
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
|
||||
writable by the VM user, so be careful!
|
||||
|
||||
### macOS
|
||||
|
||||
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
|
||||
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
|
||||
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
|
||||
blog post for more information about the Linux builder and how to tune the performance.
|
||||
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
|
||||
above to launch a VM.
|
||||
|
||||
### Custom VMs
|
||||
|
||||
To easily create a custom VM without modifying the Ghostty source, create a new
|
||||
directory, then create a file called `flake.nix` with the following text in the
|
||||
new directory.
|
||||
|
||||
```
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
ghostty.url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
ghostty,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
|
||||
nixpkgs = nixpkgs;
|
||||
system = "x86_64-linux";
|
||||
overlay = ghostty.overlays.releasefast;
|
||||
# module = ./configuration.nix # also works
|
||||
module = {pkgs, ...}: {
|
||||
environment.systemPackages = [
|
||||
pkgs.btop
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The custom VM can then be run with a command like this:
|
||||
|
||||
```
|
||||
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
|
||||
```
|
||||
|
||||
A file named `ghostty.qcow2` will be created that is used to persist any changes
|
||||
made in the VM. To "reset" the VM to default delete the file and it will be
|
||||
recreated the next time you run the VM.
|
||||
|
||||
### Contributing new VM definitions
|
||||
|
||||
#### VM Acceptance Criteria
|
||||
|
||||
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
|
||||
|
||||
1. They should be different enough from existing VM definitions that they represent a distinct
|
||||
user (and developer) experience.
|
||||
2. There's a significant Ghostty user population that uses a similar environment.
|
||||
3. The VMs can be built using only packages from the current stable NixOS release.
|
||||
|
||||
#### VM Definition Criteria
|
||||
|
||||
1. VMs should be as minimal as possible so that they build and launch quickly.
|
||||
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
|
||||
2. VMs should not expose any services to the network, or run any remote access
|
||||
software like SSH daemons, VNC or RDP.
|
||||
3. VMs should auto-login using the "ghostty" user.
|
||||
|
355
HACKING.md
Normal file
355
HACKING.md
Normal file
@@ -0,0 +1,355 @@
|
||||
# Developing Ghostty
|
||||
|
||||
This document describes the technical details behind Ghostty's development.
|
||||
If you'd like to open any pull requests or would like to implement new features
|
||||
into Ghostty, please make sure to read our ["Contributing to Ghostty"](CONTRIBUTING.md)
|
||||
document first.
|
||||
|
||||
To start development on Ghostty, you need to build Ghostty from a Git checkout,
|
||||
which is very similar in process to [building Ghostty from a source tarball](http://ghostty.org/docs/install/build). One key difference is that obviously
|
||||
you need to clone the Git repository instead of unpacking the source tarball:
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ghostty-org/ghostty
|
||||
cd ghostty
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Ghostty may require [extra dependencies](#extra-dependencies)
|
||||
> when building from a Git checkout compared to a source tarball.
|
||||
> Tip versions may also require a different version of Zig or other toolchains
|
||||
> (e.g. the Xcode SDK on macOS) compared to stable versions — make sure to
|
||||
> follow the steps closely!
|
||||
|
||||
When you're developing Ghostty, it's very likely that you will want to build a
|
||||
_debug_ build to diagnose issues more easily. This is already the default for
|
||||
Zig builds, so simply run `zig build` **without any `-Doptimize` flags**.
|
||||
|
||||
There are many more build steps than just `zig build`, some of which are listed
|
||||
here:
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `zig build run` | Runs Ghostty |
|
||||
| `zig build run-valgrind` | Runs Ghostty under Valgrind to [check for memory leaks](#checking-for-memory-leaks) |
|
||||
| `zig build test` | Runs unit tests (accepts `-Dtest-filter=<filter>` to only run tests whose name matches the filter) |
|
||||
| `zig build update-translations` | Updates Ghostty's translation strings (see the [Contributor's Guide on Localizing Ghostty](po/README_CONTRIBUTORS.md)) |
|
||||
| `zig build dist` | Builds a source tarball |
|
||||
| `zig build distcheck` | Builds and validates a source tarball |
|
||||
|
||||
## Extra Dependencies
|
||||
|
||||
Building Ghostty from a Git checkout on Linux requires some additional
|
||||
dependencies:
|
||||
|
||||
- `blueprint-compiler` (version 0.16.0 or newer)
|
||||
|
||||
macOS users don't require any additional dependencies.
|
||||
|
||||
## Xcode Version and SDKs
|
||||
|
||||
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
|
||||
and the iOS SDK are all installed.
|
||||
|
||||
A common issue is that the incorrect version of Xcode is either
|
||||
installed or selected. Use the `xcode-select` command to
|
||||
ensure that the correct version of Xcode is selected:
|
||||
|
||||
```shell-session
|
||||
sudo xcode-select --switch /Applications/Xcode-beta.app
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Main branch development of Ghostty is preparing for the next major
|
||||
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
|
||||
> **Xcode 26 and the macOS 26 SDK**.
|
||||
>
|
||||
> You do not need to be running on macOS 26 to build Ghostty, you can
|
||||
> still use Xcode 26 beta on macOS 15 stable.
|
||||
|
||||
## AI and Agents
|
||||
|
||||
If you're using AI assistance with Ghostty, Ghostty provides an
|
||||
[AGENTS.md file](https://github.com/ghostty-org/ghostty/blob/main/AGENTS.md)
|
||||
read by most of the popular AI agents to help produce higher quality
|
||||
results.
|
||||
|
||||
We also provide commands in `.agents/commands` that have some vetted
|
||||
prompts for common tasks that have been shown to produce good results.
|
||||
We provide these to help reduce the amount of time a contributor has to
|
||||
spend prompting the AI to get good results, and hopefully to lower the slop
|
||||
produced.
|
||||
|
||||
- `/gh-issue <number/url>` - Produces a prompt for diagnosing a GitHub
|
||||
issue, explaining the problem, and suggesting a plan for resolving it.
|
||||
Requires `gh` to be installed with read-only access to Ghostty.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
> All AI assistance usage [must be disclosed](https://github.com/ghostty-org/ghostty/blob/main/CONTRIBUTING.md#ai-assistance-notice)
|
||||
> and we expect contributors to understand the code that is produced and
|
||||
> be able to answer questions about it. If you don't understand the
|
||||
> code produced, feel free to disclose that, but if it has problems, we
|
||||
> may ask you to fix it and close the issue. It isn't a maintainers job to
|
||||
> review a PR so broken that it requires significant rework to be acceptable.
|
||||
|
||||
## Linting
|
||||
|
||||
### Prettier
|
||||
|
||||
Ghostty's docs and resources (not including Zig code) are linted using
|
||||
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
|
||||
check will fail builds with improper formatting. Therefore, if you are
|
||||
modifying anything Prettier will lint, you may want to install it locally and
|
||||
run this from the repo root before you commit:
|
||||
|
||||
```
|
||||
prettier --write .
|
||||
```
|
||||
|
||||
Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
|
||||
|
||||
Nix users can use the following command to format with Prettier:
|
||||
|
||||
```
|
||||
nix develop -c prettier --write .
|
||||
```
|
||||
|
||||
### Alejandra
|
||||
|
||||
Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check
|
||||
will fail builds with improper formatting.
|
||||
|
||||
Nix users can use the following command to format with Alejandra:
|
||||
|
||||
```
|
||||
nix develop -c alejandra .
|
||||
```
|
||||
|
||||
Non-Nix users should install Alejandra and use the following command to format with Alejandra:
|
||||
|
||||
```
|
||||
alejandra .
|
||||
```
|
||||
|
||||
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
|
||||
|
||||
### Updating the Zig Cache Fixed-Output Derivation Hash
|
||||
|
||||
The Nix package depends on a [fixed-output
|
||||
derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash)
|
||||
that manages the Zig package cache. This allows the package to be built in the
|
||||
Nix sandbox.
|
||||
|
||||
Occasionally (usually when `build.zig.zon` is updated), the hash that
|
||||
identifies the cache will need to be updated. There are jobs that monitor the
|
||||
hash in CI, and builds will fail if it drifts.
|
||||
|
||||
To update it, you can run the following in the repository root:
|
||||
|
||||
```
|
||||
./nix/build-support/check-zig-cache-hash.sh --update
|
||||
```
|
||||
|
||||
This will write out the `nix/zigCacheHash.nix` file with the updated hash
|
||||
that can then be committed and pushed to fix the builds.
|
||||
|
||||
## Including and Updating Translations
|
||||
|
||||
See the [Contributor's Guide](po/README_CONTRIBUTORS.md) for more details.
|
||||
|
||||
## Checking for Memory Leaks
|
||||
|
||||
While Zig does an amazing job of finding and preventing memory leaks,
|
||||
Ghostty uses many third-party libraries that are written in C. Improper usage
|
||||
of those libraries or bugs in those libraries can cause memory leaks that
|
||||
Zig cannot detect by itself.
|
||||
|
||||
### On Linux
|
||||
|
||||
On Linux the recommended tool to check for memory leaks is Valgrind. The
|
||||
recommended way to run Valgrind is via `zig build`:
|
||||
|
||||
```sh
|
||||
zig build run-valgrind
|
||||
```
|
||||
|
||||
This builds a Ghostty executable with Valgrind support and runs Valgrind
|
||||
with the proper flags to ensure we're suppressing known false positives.
|
||||
|
||||
You can combine the same build args with `run-valgrind` that you can with
|
||||
`run`, such as specifying additional configurations after a trailing `--`.
|
||||
|
||||
## Input Stack Testing
|
||||
|
||||
The input stack is the part of the codebase that starts with a
|
||||
key event and ends with text encoding being sent to the pty (it
|
||||
does not include _rendering_ the text, which is part of the
|
||||
font or rendering stack).
|
||||
|
||||
If you modify any part of the input stack, you must manually verify
|
||||
all the following input cases work properly. We unfortunately do
|
||||
not automate this in any way, but if we can do that one day that'd
|
||||
save a LOT of grief and time.
|
||||
|
||||
Note: this list may not be exhaustive, I'm still working on it.
|
||||
|
||||
### Linux IME
|
||||
|
||||
IME (Input Method Editors) are a common source of bugs in the input stack,
|
||||
especially on Linux since there are multiple different IME systems
|
||||
interacting with different windowing systems and application frameworks
|
||||
all written by different organizations.
|
||||
|
||||
The following matrix should be tested to ensure that all IME input works
|
||||
properly:
|
||||
|
||||
1. Wayland, X11
|
||||
2. ibus, fcitx, none
|
||||
3. Dead key input (e.g. Spanish), CJK (e.g. Japanese), Emoji, Unicode Hex
|
||||
4. ibus versions: 1.5.29, 1.5.30, 1.5.31 (each exhibit slightly different behaviors)
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> This is a **work in progress**. I'm still working on this list and it
|
||||
> is not complete. As I find more test cases, I will add them here.
|
||||
|
||||
#### Dead Key Input
|
||||
|
||||
Set your keyboard layout to "Spanish" (or another layout that uses dead keys).
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `'`
|
||||
3. Press `a`
|
||||
4. Verify that `á` is displayed
|
||||
|
||||
Note that the dead key may or may not show a preedit state visually.
|
||||
For ibus and fcitx it does but for the "none" case it does not. Importantly,
|
||||
the text should be correct when it is sent to the pty.
|
||||
|
||||
We should also test canceling dead key input:
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `'`
|
||||
3. Press escape
|
||||
4. Press `a`
|
||||
5. Verify that `a` is displayed (no diacritic)
|
||||
|
||||
#### CJK Input
|
||||
|
||||
Configure fcitx or ibus with a keyboard layout like Japanese or Mozc. The
|
||||
exact layout doesn't matter.
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `Ctrl+Shift` to switch to "Hiragana"
|
||||
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
|
||||
4. Press `Enter`
|
||||
5. Verify that `こん` is displayed in the terminal.
|
||||
|
||||
We should also test switching input methods while preedit is active, which
|
||||
should commit the text:
|
||||
|
||||
1. Launch Ghostty
|
||||
2. Press `Ctrl+Shift` to switch to "Hiragana"
|
||||
3. On a US physical layout, type: `konn`, you should see `こん` in preedit.
|
||||
4. Press `Ctrl+Shift` to switch to another layout (any)
|
||||
5. Verify that `こん` is displayed in the terminal as committed text.
|
||||
|
||||
## Nix Virtual Machines
|
||||
|
||||
Several Nix virtual machine definitions are provided by the project for testing
|
||||
and developing Ghostty against multiple different Linux desktop environments.
|
||||
|
||||
Running these requires a working Nix installation, either Nix on your
|
||||
favorite Linux distribution, NixOS, or macOS with nix-darwin installed. Further
|
||||
requirements for macOS are detailed below.
|
||||
|
||||
VMs should only be run on your local desktop and then powered off when not in
|
||||
use, which will discard any changes to the VM.
|
||||
|
||||
The VM definitions provide minimal software "out of the box" but additional
|
||||
software can be installed by using standard Nix mechanisms like `nix run nixpkgs#<package>`.
|
||||
|
||||
### Linux
|
||||
|
||||
1. Check out the Ghostty source and change to the directory.
|
||||
2. Run `nix run .#<vmtype>`. `<vmtype>` can be any of the VMs defined in the
|
||||
`nix/vm` directory (without the `.nix` suffix) excluding any file prefixed
|
||||
with `common` or `create`.
|
||||
3. The VM will build and then launch. Depending on the speed of your system, this
|
||||
can take a while, but eventually you should get a new VM window.
|
||||
4. The Ghostty source directory should be mounted to `/tmp/shared` in the VM. Depending
|
||||
on what UID and GID of the user that you launched the VM as, `/tmp/shared` _may_ be
|
||||
writable by the VM user, so be careful!
|
||||
|
||||
### macOS
|
||||
|
||||
1. To run the VMs on macOS you will need to enable the Linux builder in your `nix-darwin`
|
||||
config. This _should_ be as simple as adding `nix.linux-builder.enable=true` to your
|
||||
configuration and then rebuilding. See [this](https://nixcademy.com/posts/macos-linux-builder/)
|
||||
blog post for more information about the Linux builder and how to tune the performance.
|
||||
2. Once the Linux builder has been enabled, you should be able to follow the Linux instructions
|
||||
above to launch a VM.
|
||||
|
||||
### Custom VMs
|
||||
|
||||
To easily create a custom VM without modifying the Ghostty source, create a new
|
||||
directory, then create a file called `flake.nix` with the following text in the
|
||||
new directory.
|
||||
|
||||
```
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "nixpkgs/nixpkgs-unstable";
|
||||
ghostty.url = "github:ghostty-org/ghostty";
|
||||
};
|
||||
outputs = {
|
||||
nixpkgs,
|
||||
ghostty,
|
||||
...
|
||||
}: {
|
||||
nixosConfigurations.custom-vm = ghostty.create-gnome-vm {
|
||||
nixpkgs = nixpkgs;
|
||||
system = "x86_64-linux";
|
||||
overlay = ghostty.overlays.releasefast;
|
||||
# module = ./configuration.nix # also works
|
||||
module = {pkgs, ...}: {
|
||||
environment.systemPackages = [
|
||||
pkgs.btop
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
The custom VM can then be run with a command like this:
|
||||
|
||||
```
|
||||
nix run .#nixosConfigurations.custom-vm.config.system.build.vm
|
||||
```
|
||||
|
||||
A file named `ghostty.qcow2` will be created that is used to persist any changes
|
||||
made in the VM. To "reset" the VM to default delete the file and it will be
|
||||
recreated the next time you run the VM.
|
||||
|
||||
### Contributing new VM definitions
|
||||
|
||||
#### VM Acceptance Criteria
|
||||
|
||||
We welcome the contribution of new VM definitions, as long as they meet the following criteria:
|
||||
|
||||
1. They should be different enough from existing VM definitions that they represent a distinct
|
||||
user (and developer) experience.
|
||||
2. There's a significant Ghostty user population that uses a similar environment.
|
||||
3. The VMs can be built using only packages from the current stable NixOS release.
|
||||
|
||||
#### VM Definition Criteria
|
||||
|
||||
1. VMs should be as minimal as possible so that they build and launch quickly.
|
||||
Additional software can be added at runtime with a command like `nix run nixpkgs#<package name>`.
|
||||
2. VMs should not expose any services to the network, or run any remote access
|
||||
software like SSH daemons, VNC or RDP.
|
||||
3. VMs should auto-login using the "ghostty" user.
|
128
README.md
128
README.md
@@ -13,7 +13,9 @@
|
||||
·
|
||||
<a href="https://ghostty.org/docs">Documentation</a>
|
||||
·
|
||||
<a href="#developing-ghostty">Developing</a>
|
||||
<a href="CONTRIBUTING.md">Contributing</a>
|
||||
·
|
||||
<a href="HACKING.md">Developing</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
@@ -49,6 +51,14 @@ See the [download page](https://ghostty.org/download) on the Ghostty website.
|
||||
|
||||
See the [documentation](https://ghostty.org/docs) on the Ghostty website.
|
||||
|
||||
## Contributing and Developing
|
||||
|
||||
If you have any ideas, issues, etc. regarding Ghostty, or would like to
|
||||
contribute to Ghostty through pull requests, please check out our
|
||||
["Contributing to Ghostty"](CONTRIBUTING.md) document. Those who would like
|
||||
to get involved with Ghostty's development as well should also read the
|
||||
["Developing Ghostty"](HACKING.md) document for more technical details.
|
||||
|
||||
## Roadmap and Status
|
||||
|
||||
The high-level ambitious plan for the project, in order:
|
||||
@@ -184,119 +194,3 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
|
||||
> stack memory of each thread at the time of the crash. This information
|
||||
> is used to rebuild the stack trace but can also contain sensitive data
|
||||
> depending when the crash occurred.
|
||||
|
||||
## Developing Ghostty
|
||||
|
||||
See the documentation on the Ghostty website for
|
||||
[building Ghostty from a source tarball](http://ghostty.org/docs/install/build).
|
||||
Building Ghostty from a Git checkout is very similar, except you want to
|
||||
omit the `-Doptimize` flag to build a debug build, and you may require
|
||||
additional dependencies since the source tarball includes some processed
|
||||
files that are not in the Git repository.
|
||||
|
||||
Other useful commands:
|
||||
|
||||
- `zig build test` for running unit tests.
|
||||
- `zig build test -Dtest-filter=<filter>` for running a specific subset of those unit tests
|
||||
- `zig build run -Dconformance=<name>` runs a conformance test case from
|
||||
the `conformance` directory. The `name` is the name of the file. This runs
|
||||
in the current running terminal emulator so if you want to check the
|
||||
behavior of this project, you must run this command in Ghostty.
|
||||
|
||||
### Extra Dependencies
|
||||
|
||||
Building Ghostty from a Git checkout on Linux requires some additional
|
||||
dependencies:
|
||||
|
||||
- `blueprint-compiler`
|
||||
|
||||
macOS users don't require any additional dependencies.
|
||||
|
||||
> [!NOTE]
|
||||
> This only applies to building from a _Git checkout_. This section does
|
||||
> not apply if you're building from a released _source tarball_. For
|
||||
> source tarballs, see the
|
||||
> [website](http://ghostty.org/docs/install/build).
|
||||
|
||||
### Xcode Version and SDKs
|
||||
|
||||
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
|
||||
and the iOS SDK are all installed.
|
||||
|
||||
A common issue is that the incorrect version of Xcode is either
|
||||
installed or selected. Use the `xcode-select` command to
|
||||
ensure that the correct version of Xcode is selected:
|
||||
|
||||
```shell-session
|
||||
sudo xcode-select --switch /Applications/Xcode-beta.app
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Main branch development of Ghostty is preparing for the next major
|
||||
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
|
||||
> **Xcode 26 and the macOS 26 SDK**.
|
||||
>
|
||||
> You do not need to be running on macOS 26 to build Ghostty, you can
|
||||
> still use Xcode 26 beta on macOS 15 stable.
|
||||
|
||||
### Linting
|
||||
|
||||
#### Prettier
|
||||
|
||||
Ghostty's docs and resources (not including Zig code) are linted using
|
||||
[Prettier](https://prettier.io) with out-of-the-box settings. A Prettier CI
|
||||
check will fail builds with improper formatting. Therefore, if you are
|
||||
modifying anything Prettier will lint, you may want to install it locally and
|
||||
run this from the repo root before you commit:
|
||||
|
||||
```
|
||||
prettier --write .
|
||||
```
|
||||
|
||||
Make sure your Prettier version matches the version of Prettier in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
|
||||
|
||||
Nix users can use the following command to format with Prettier:
|
||||
|
||||
```
|
||||
nix develop -c prettier --write .
|
||||
```
|
||||
|
||||
#### Alejandra
|
||||
|
||||
Nix modules are formatted with [Alejandra](https://github.com/kamadorueda/alejandra/). An Alejandra CI check
|
||||
will fail builds with improper formatting.
|
||||
|
||||
Nix users can use the following command to format with Alejandra:
|
||||
|
||||
```
|
||||
nix develop -c alejandra .
|
||||
```
|
||||
|
||||
Non-Nix users should install Alejandra and use the following command to format with Alejandra:
|
||||
|
||||
```
|
||||
alejandra .
|
||||
```
|
||||
|
||||
Make sure your Alejandra version matches the version of Alejandra in [devShell.nix](https://github.com/ghostty-org/ghostty/blob/main/nix/devShell.nix).
|
||||
|
||||
#### Updating the Zig Cache Fixed-Output Derivation Hash
|
||||
|
||||
The Nix package depends on a [fixed-output
|
||||
derivation](https://nix.dev/manual/nix/stable/language/advanced-attributes.html#adv-attr-outputHash)
|
||||
that manages the Zig package cache. This allows the package to be built in the
|
||||
Nix sandbox.
|
||||
|
||||
Occasionally (usually when `build.zig.zon` is updated), the hash that
|
||||
identifies the cache will need to be updated. There are jobs that monitor the
|
||||
hash in CI, and builds will fail if it drifts.
|
||||
|
||||
To update it, you can run the following in the repository root:
|
||||
|
||||
```
|
||||
./nix/build-support/check-zig-cache-hash.sh --update
|
||||
```
|
||||
|
||||
This will write out the `nix/zigCacheHash.nix` file with the updated hash
|
||||
that can then be committed and pushed to fix the builds.
|
||||
|
@@ -55,8 +55,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-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM",
|
||||
.url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst",
|
||||
.hash = "gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z",
|
||||
.lazy = true,
|
||||
},
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz",
|
||||
.hash = "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
|
||||
.hash = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
12
build.zig.zon.json
generated
12
build.zig.zon.json
generated
@@ -24,10 +24,10 @@
|
||||
"url": "https://deps.files.ghostty.org/glslang-12201278a1a05c0ce0b6eb6026c65cd3e9247aa041b1c260324bf29cee559dd23ba1.tar.gz",
|
||||
"hash": "sha256-FKLtu1Ccs+UamlPj9eQ12/WXFgS0uDPmPmB26MCpl7U="
|
||||
},
|
||||
"gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM": {
|
||||
"gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z": {
|
||||
"name": "gobject",
|
||||
"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="
|
||||
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst",
|
||||
"hash": "sha256-h6aKUerGlX2ATVEeoN03eWaqDqvUmKdedgpxrSoHvrY="
|
||||
},
|
||||
"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-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri": {
|
||||
"N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz",
|
||||
"hash": "sha256-LQIa9siNICX5zzajvrJNKBmgDqAlBDY7QEmHihs65d0="
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
|
||||
"hash": "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc="
|
||||
},
|
||||
"N-V-__8AAIC5lwAVPJJzxnCAahSvZTIlG-HhtOvnM1uh-66x": {
|
||||
"name": "jetbrains_mono",
|
||||
|
12
build.zig.zon.nix
generated
12
build.zig.zon.nix
generated
@@ -123,11 +123,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "gobject-0.3.0-Skun7AngnABC2BPiaoobs6YSSzSgMuEIcjb2rYrRyaAM";
|
||||
name = "gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z";
|
||||
path = fetchZigArtifact {
|
||||
name = "gobject";
|
||||
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=";
|
||||
url = "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst";
|
||||
hash = "sha256-h6aKUerGlX2ATVEeoN03eWaqDqvUmKdedgpxrSoHvrY=";
|
||||
};
|
||||
}
|
||||
{
|
||||
@@ -163,11 +163,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri";
|
||||
name = "N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz";
|
||||
hash = "sha256-LQIa9siNICX5zzajvrJNKBmgDqAlBDY7QEmHihs65d0=";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz";
|
||||
hash = "sha256-NlUXcBOmaA8W+7RXuXcn9TIhm964dXO2Op4QCQxhDyc=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
4
build.zig.zon.txt
generated
4
build.zig.zon.txt
generated
@@ -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-08-09-37-1/ghostty-gobject-0.14.1-2025-08-09-37-1.tar.zst
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz
|
||||
https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz
|
||||
https://github.com/mitchellh/libxev/archive/7f803181b158a10fec8619f793e3b4df515566cb.tar.gz
|
||||
https://github.com/mitchellh/zig-objc/archive/c9e917a4e15a983b672ca779c7985d738a2d517c.tar.gz
|
||||
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
|
||||
|
4
dist/linux/app.desktop.in
vendored
4
dist/linux/app.desktop.in
vendored
@@ -4,7 +4,7 @@ Name=@NAME@
|
||||
Type=Application
|
||||
Comment=A terminal emulator
|
||||
TryExec=@GHOSTTY@
|
||||
Exec=@GHOSTTY@ --launched-from=desktop
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true
|
||||
Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
@@ -23,4 +23,4 @@ X-KDE-Shortcuts=Ctrl+Alt+T
|
||||
|
||||
[Desktop Action new-window]
|
||||
Name=New Window
|
||||
Exec=@GHOSTTY@ --launched-from=desktop
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true
|
||||
|
2
dist/linux/dbus.service.flatpak.in
vendored
2
dist/linux/dbus.service.flatpak.in
vendored
@@ -1,3 +1,3 @@
|
||||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
2
dist/linux/dbus.service.in
vendored
2
dist/linux/dbus.service.in
vendored
@@ -1,4 +1,4 @@
|
||||
[D-BUS Service]
|
||||
Name=@APPID@
|
||||
SystemdService=app-@APPID@.service
|
||||
Exec=@GHOSTTY@ --launched-from=dbus
|
||||
Exec=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
2
dist/linux/systemd.service.in
vendored
2
dist/linux/systemd.service.in
vendored
@@ -8,7 +8,7 @@ Requires=dbus.socket
|
||||
Type=notify-reload
|
||||
ReloadSignal=SIGUSR2
|
||||
BusName=@APPID@
|
||||
ExecStart=@GHOSTTY@ --launched-from=systemd
|
||||
ExecStart=@GHOSTTY@ --gtk-single-instance=true --initial-window=false
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical-session.target
|
||||
|
@@ -31,9 +31,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"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"
|
||||
"url": "https://github.com/jcollie/ghostty-gobject/releases/download/0.15.1-2025-09-04-48-1/ghostty-gobject-0.15.1-2025-09-04-48-1.tar.zst",
|
||||
"dest": "vendor/p/gobject-0.3.0-Skun7ET3nQAc0LzvO0NAvTiGGnmkF36cnmbeCAF6MB7Z",
|
||||
"sha256": "87a68a51eac6957d804d511ea0dd377966aa0eabd498a75e760a71ad2a07beb6"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
@@ -61,9 +61,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e4c0090a654e081e953b63d0fa50dee075726023.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AADBlXwQadkTTsE4iezfphTwqGtFpvJbzMub5lpri",
|
||||
"sha256": "2d021af6c88d2025f9cf36a3beb24d2819a00ea02504363b4049878a1b3ae5dd"
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/6cdbc8501d48601302e32d6b53e9e7934bf354b4.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAAtjXwSdhZq_xYbCXo0SZMqoNoQuHFkC07sijQME",
|
||||
"sha256": "3655177013a6680f16fbb457b97727f532219bdeb87573b63a9e10090c610f27"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
@@ -419,6 +419,7 @@ typedef struct {
|
||||
ghostty_env_var_s* env_vars;
|
||||
size_t env_var_count;
|
||||
const char* initial_input;
|
||||
bool wait_after_command;
|
||||
} ghostty_surface_config_s;
|
||||
|
||||
typedef struct {
|
||||
@@ -721,15 +722,15 @@ typedef enum {
|
||||
GHOSTTY_PROGRESS_STATE_ERROR,
|
||||
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
|
||||
GHOSTTY_PROGRESS_STATE_PAUSE,
|
||||
} ghostty_terminal_osc_command_progressreport_state_e;
|
||||
} ghostty_action_progress_report_state_e;
|
||||
|
||||
// terminal.osc.Command.ProgressReport.C
|
||||
typedef struct {
|
||||
ghostty_terminal_osc_command_progressreport_state_e state;
|
||||
ghostty_action_progress_report_state_e state;
|
||||
// -1 if no progress was reported, otherwise 0-100 indicating percent
|
||||
// completeness.
|
||||
int8_t progress;
|
||||
} ghostty_terminal_osc_command_progressreport_s;
|
||||
} ghostty_action_progress_report_s;
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
@@ -816,7 +817,7 @@ typedef union {
|
||||
ghostty_action_open_url_s open_url;
|
||||
ghostty_action_close_tab_mode_e close_tab_mode;
|
||||
ghostty_surface_message_childexited_s child_exited;
|
||||
ghostty_terminal_osc_command_progressreport_s progress_report;
|
||||
ghostty_action_progress_report_s progress_report;
|
||||
} ghostty_action_u;
|
||||
|
||||
typedef struct {
|
||||
@@ -963,7 +964,7 @@ void ghostty_surface_mouse_scroll(ghostty_surface_t,
|
||||
double,
|
||||
ghostty_input_scroll_mods_t);
|
||||
void ghostty_surface_mouse_pressure(ghostty_surface_t, uint32_t, double);
|
||||
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*);
|
||||
void ghostty_surface_ime_point(ghostty_surface_t, double*, double*, double*, double*);
|
||||
void ghostty_surface_request_close(ghostty_surface_t);
|
||||
void ghostty_surface_split(ghostty_surface_t, ghostty_action_split_direction_e);
|
||||
void ghostty_surface_split_focus(ghostty_surface_t,
|
||||
|
@@ -127,6 +127,7 @@
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
|
||||
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; };
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
||||
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
||||
@@ -987,6 +988,7 @@
|
||||
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
|
||||
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */,
|
||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
||||
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,
|
||||
|
@@ -398,6 +398,10 @@ class AppDelegate: NSObject,
|
||||
var isDirectory = ObjCBool(true)
|
||||
guard FileManager.default.fileExists(atPath: filename, isDirectory: &isDirectory) else { return false }
|
||||
|
||||
// Set to true if confirmation is required before starting up the
|
||||
// new terminal.
|
||||
var requiresConfirm: Bool = false
|
||||
|
||||
// Initialize the surface config which will be used to create the tab or window for the opened file.
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
|
||||
@@ -406,6 +410,13 @@ class AppDelegate: NSObject,
|
||||
// whether to open in a new tab or new window.
|
||||
config.workingDirectory = filename
|
||||
} else {
|
||||
// Unconditionally require confirmation in the file execution case.
|
||||
// In the future I have ideas about making this more fine-grained if
|
||||
// we can not inherit of unsandboxed state. For now, we need to confirm
|
||||
// because there is a sandbox escape possible if a sandboxed application
|
||||
// somehow is tricked into `open`-ing a non-sandboxed application.
|
||||
requiresConfirm = true
|
||||
|
||||
// When opening a file, we want to execute the file. To do this, we
|
||||
// don't override the command directly, because it won't load the
|
||||
// profile/rc files for the shell, which is super important on macOS
|
||||
@@ -413,11 +424,34 @@ class AppDelegate: NSObject,
|
||||
// `<filename>; exit` which is what Terminal and iTerm2 do.
|
||||
config.initialInput = "\(filename); exit\n"
|
||||
|
||||
// For commands executed directly, we want to ensure we wait after exit
|
||||
// because in most cases scripts don't block on exit and we don't want
|
||||
// the window to just flash closed once complete.
|
||||
config.waitAfterCommand = true
|
||||
|
||||
// Set the parent directory to our working directory so that relative
|
||||
// paths in scripts work.
|
||||
config.workingDirectory = (filename as NSString).deletingLastPathComponent
|
||||
}
|
||||
|
||||
if requiresConfirm {
|
||||
// Confirmation required. We use an app-wide NSAlert for now. In the future we
|
||||
// may want to show this as a sheet on the focused window (especially if we're
|
||||
// opening a tab). I'm not sure.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Allow Ghostty to execute \"\(filename)\"?"
|
||||
alert.addButton(withTitle: "Allow")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
switch (alert.runModal()) {
|
||||
case .alertFirstButtonReturn:
|
||||
break
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch ghostty.config.macosDockDropBehavior {
|
||||
case .new_tab: _ = TerminalController.newTab(ghostty, withBaseConfig: config)
|
||||
case .new_window: _ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
||||
@@ -903,7 +937,7 @@ class AppDelegate: NSObject,
|
||||
func findSurface(forUUID uuid: UUID) -> Ghostty.SurfaceView? {
|
||||
for c in TerminalController.all {
|
||||
for view in c.surfaceTree {
|
||||
if view.uuid == uuid {
|
||||
if view.id == uuid {
|
||||
return view
|
||||
}
|
||||
}
|
||||
|
@@ -34,7 +34,7 @@ struct TerminalEntity: AppEntity {
|
||||
/// Returns the view associated with this entity. This may no longer exist.
|
||||
@MainActor
|
||||
var surfaceView: Ghostty.SurfaceView? {
|
||||
Self.defaultQuery.all.first { $0.uuid == self.id }
|
||||
Self.defaultQuery.all.first { $0.id == self.id }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -46,7 +46,7 @@ struct TerminalEntity: AppEntity {
|
||||
|
||||
@MainActor
|
||||
init(_ view: Ghostty.SurfaceView) {
|
||||
self.id = view.uuid
|
||||
self.id = view.id
|
||||
self.title = view.title
|
||||
self.workingDirectory = view.pwd
|
||||
if let nsImage = ImageRenderer(content: view.screenshot()).nsImage {
|
||||
@@ -80,7 +80,7 @@ struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery {
|
||||
@MainActor
|
||||
func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] {
|
||||
return all.filter {
|
||||
identifiers.contains($0.uuid)
|
||||
identifiers.contains($0.id)
|
||||
}.map {
|
||||
TerminalEntity($0)
|
||||
}
|
||||
|
@@ -481,6 +481,12 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
|
||||
// If we are in fullscreen, then we exit fullscreen. We do this immediately so
|
||||
// we have th correct window.frame for the save state below.
|
||||
if let fullscreenStyle, fullscreenStyle.isFullscreen {
|
||||
fullscreenStyle.exit()
|
||||
}
|
||||
|
||||
// Save the current window frame before animating out. This preserves
|
||||
// the user's preferred window size and position for when the quick
|
||||
// terminal is reactivated with a new surface. Without this, SwiftUI
|
||||
@@ -503,11 +509,6 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// We always animate out to whatever screen the window is actually on.
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
// If we are in fullscreen, then we exit fullscreen.
|
||||
if let fullscreenStyle, fullscreenStyle.isFullscreen {
|
||||
fullscreenStyle.exit()
|
||||
}
|
||||
|
||||
// If we have a previously active application, restore focus to it. We
|
||||
// do this BEFORE the animation below because when the animation completes
|
||||
// macOS will bring forward another window.
|
||||
|
@@ -13,7 +13,7 @@ enum QuickTerminalPosition : String {
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
window.setFrame(.init(
|
||||
origin: window.frame.origin,
|
||||
size: size.calculate(position: self, screenDimensions: screen.frame.size)
|
||||
size: size.calculate(position: self, screenDimensions: screen.visibleFrame.size)
|
||||
), display: false)
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ enum QuickTerminalPosition : String {
|
||||
|
||||
/// Get the configured frame size for initial positioning and animations.
|
||||
func configuredFrameSize(on screen: NSScreen, terminalSize: QuickTerminalSize) -> NSSize {
|
||||
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.frame.size)
|
||||
let dimensions = terminalSize.calculate(position: self, screenDimensions: screen.visibleFrame.size)
|
||||
return NSSize(width: dimensions.width, height: dimensions.height)
|
||||
}
|
||||
|
||||
@@ -66,16 +66,24 @@ enum QuickTerminalPosition : String {
|
||||
func initialOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
|
||||
switch (self) {
|
||||
case .top:
|
||||
return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.frame.maxY)
|
||||
return .init(
|
||||
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
|
||||
y: screen.visibleFrame.maxY)
|
||||
|
||||
case .bottom:
|
||||
return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: -window.frame.height)
|
||||
return .init(
|
||||
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
|
||||
y: -window.frame.height)
|
||||
|
||||
case .left:
|
||||
return .init(x: screen.frame.minX-window.frame.width, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2))
|
||||
return .init(
|
||||
x: screen.visibleFrame.minX-window.frame.width,
|
||||
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.frame.maxX, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2))
|
||||
return .init(
|
||||
x: screen.visibleFrame.maxX,
|
||||
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
|
||||
|
||||
case .center:
|
||||
return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: screen.visibleFrame.height - window.frame.width)
|
||||
@@ -86,16 +94,24 @@ enum QuickTerminalPosition : String {
|
||||
func finalOrigin(for window: NSWindow, on screen: NSScreen) -> CGPoint {
|
||||
switch (self) {
|
||||
case .top:
|
||||
return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.visibleFrame.maxY - window.frame.height)
|
||||
return .init(
|
||||
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
|
||||
y: screen.visibleFrame.maxY - window.frame.height)
|
||||
|
||||
case .bottom:
|
||||
return .init(x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2), y: screen.frame.minY)
|
||||
return .init(
|
||||
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
|
||||
y: screen.visibleFrame.minY)
|
||||
|
||||
case .left:
|
||||
return .init(x: screen.frame.minX, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2))
|
||||
return .init(
|
||||
x: screen.visibleFrame.minX,
|
||||
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2))
|
||||
return .init(
|
||||
x: screen.visibleFrame.maxX - window.frame.width,
|
||||
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
|
||||
|
||||
case .center:
|
||||
return .init(x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2), y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2))
|
||||
@@ -125,13 +141,13 @@ enum QuickTerminalPosition : String {
|
||||
switch self {
|
||||
case .top:
|
||||
return CGPoint(
|
||||
x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2),
|
||||
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
|
||||
y: window.frame.origin.y // Keep the same Y position
|
||||
)
|
||||
|
||||
case .bottom:
|
||||
return CGPoint(
|
||||
x: round(screen.frame.origin.x + (screen.frame.width - window.frame.width) / 2),
|
||||
x: round(screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2),
|
||||
y: window.frame.origin.y // Keep the same Y position
|
||||
)
|
||||
|
||||
@@ -153,13 +169,13 @@ enum QuickTerminalPosition : String {
|
||||
case .left:
|
||||
return CGPoint(
|
||||
x: window.frame.origin.x, // Keep the same X position
|
||||
y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)
|
||||
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
|
||||
)
|
||||
|
||||
case .right:
|
||||
return CGPoint(
|
||||
x: window.frame.origin.x, // Keep the same X position
|
||||
y: round(screen.frame.origin.y + (screen.frame.height - window.frame.height) / 2)
|
||||
y: round(screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
|
||||
)
|
||||
|
||||
case .top, .bottom, .center:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import AppKit
|
||||
|
||||
/// SplitTree represents a tree of views that can be divided.
|
||||
struct SplitTree<ViewType: NSView & Codable>: Codable {
|
||||
struct SplitTree<ViewType: NSView & Codable & Identifiable> {
|
||||
/// The root of the tree. This can be nil to indicate the tree is empty.
|
||||
let root: Node?
|
||||
|
||||
@@ -29,12 +29,12 @@ struct SplitTree<ViewType: NSView & Codable>: Codable {
|
||||
}
|
||||
|
||||
/// The path to a specific node in the tree.
|
||||
struct Path {
|
||||
struct Path: Codable {
|
||||
let path: [Component]
|
||||
|
||||
var isEmpty: Bool { path.isEmpty }
|
||||
|
||||
enum Component {
|
||||
enum Component: Codable {
|
||||
case left
|
||||
case right
|
||||
}
|
||||
@@ -127,6 +127,13 @@ extension SplitTree {
|
||||
root: try root.insert(view: view, at: at, direction: direction),
|
||||
zoomed: nil)
|
||||
}
|
||||
/// Find a node containing a view with the specified ID.
|
||||
/// - Parameter id: The ID of the view to find
|
||||
/// - Returns: The node containing the view if found, nil otherwise
|
||||
func find(id: ViewType.ID) -> Node? {
|
||||
guard let root else { return nil }
|
||||
return root.find(id: id)
|
||||
}
|
||||
|
||||
/// Remove a node from the tree. If the node being removed is part of a split,
|
||||
/// the sibling node takes the place of the parent split.
|
||||
@@ -334,6 +341,60 @@ extension SplitTree {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree Codable
|
||||
|
||||
fileprivate enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case root
|
||||
case zoomed
|
||||
|
||||
static let currentVersion: Int = 1
|
||||
}
|
||||
|
||||
extension SplitTree: Codable {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Check version
|
||||
let version = try container.decode(Int.self, forKey: .version)
|
||||
guard version == CodingKeys.currentVersion else {
|
||||
throw DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription: "Unsupported SplitTree version: \(version)"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Decode root
|
||||
self.root = try container.decodeIfPresent(Node.self, forKey: .root)
|
||||
|
||||
// Zoomed is encoded as its path. Get the path and then find it.
|
||||
if let zoomedPath = try container.decodeIfPresent(Path.self, forKey: .zoomed),
|
||||
let root = self.root {
|
||||
self.zoomed = root.node(at: zoomedPath)
|
||||
} else {
|
||||
self.zoomed = nil
|
||||
}
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Encode version
|
||||
try container.encode(CodingKeys.currentVersion, forKey: .version)
|
||||
|
||||
// Encode root
|
||||
try container.encodeIfPresent(root, forKey: .root)
|
||||
|
||||
// Zoomed is encoded as its path since its a reference type. This lets us
|
||||
// map it on decode back to the correct node in root.
|
||||
if let zoomed, let path = root?.path(to: zoomed) {
|
||||
try container.encode(path, forKey: .zoomed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: SplitTree.Node
|
||||
|
||||
extension SplitTree.Node {
|
||||
@@ -342,6 +403,23 @@ extension SplitTree.Node {
|
||||
typealias SplitError = SplitTree.SplitError
|
||||
typealias Path = SplitTree.Path
|
||||
|
||||
/// Find a node containing a view with the specified ID.
|
||||
/// - Parameter id: The ID of the view to find
|
||||
/// - Returns: The node containing the view if found, nil otherwise
|
||||
func find(id: ViewType.ID) -> Node? {
|
||||
switch self {
|
||||
case .leaf(let view):
|
||||
return view.id == id ? self : nil
|
||||
|
||||
case .split(let split):
|
||||
if let found = split.left.find(id: id) {
|
||||
return found
|
||||
}
|
||||
|
||||
return split.right.find(id: id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the node in the tree that contains the given view.
|
||||
func node(view: ViewType) -> Node? {
|
||||
switch (self) {
|
||||
|
@@ -439,8 +439,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
continue
|
||||
}
|
||||
|
||||
let action = "goto_tab:\(tab)"
|
||||
if let equiv = ghostty.config.keyboardShortcut(for: action) {
|
||||
if let equiv = ghostty.config.keyboardShortcut(for: "goto_tab:\(tab)") {
|
||||
window.keyEquivalent = "\(equiv)"
|
||||
} else {
|
||||
window.keyEquivalent = ""
|
||||
@@ -861,7 +860,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
|
||||
// Restore focus to the previously focused surface
|
||||
if let focusedUUID = undoState.focusedSurface,
|
||||
let focusTarget = surfaceTree.first(where: { $0.uuid == focusedUUID }) {
|
||||
let focusTarget = surfaceTree.first(where: { $0.id == focusedUUID }) {
|
||||
DispatchQueue.main.async {
|
||||
Ghostty.moveFocus(to: focusTarget, from: nil)
|
||||
}
|
||||
@@ -876,7 +875,7 @@ class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Contr
|
||||
return .init(
|
||||
frame: window.frame,
|
||||
surfaceTree: surfaceTree,
|
||||
focusedSurface: focusedSurface?.uuid,
|
||||
focusedSurface: focusedSurface?.id,
|
||||
tabIndex: window.tabGroup?.windows.firstIndex(of: window),
|
||||
tabGroup: window.tabGroup)
|
||||
}
|
||||
|
@@ -4,13 +4,13 @@ import Cocoa
|
||||
class TerminalRestorableState: Codable {
|
||||
static let selfKey = "state"
|
||||
static let versionKey = "version"
|
||||
static let version: Int = 4
|
||||
static let version: Int = 5
|
||||
|
||||
let focusedSurface: String?
|
||||
let surfaceTree: SplitTree<Ghostty.SurfaceView>
|
||||
|
||||
init(from controller: TerminalController) {
|
||||
self.focusedSurface = controller.focusedSurface?.uuid.uuidString
|
||||
self.focusedSurface = controller.focusedSurface?.id.uuidString
|
||||
self.surfaceTree = controller.surfaceTree
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ class TerminalWindowRestoration: NSObject, NSWindowRestoration {
|
||||
if let focusedStr = state.focusedSurface {
|
||||
var foundView: Ghostty.SurfaceView?
|
||||
for view in c.surfaceTree {
|
||||
if view.uuid.uuidString == focusedStr {
|
||||
if view.id.uuidString == focusedStr {
|
||||
foundView = view
|
||||
break
|
||||
}
|
||||
|
@@ -34,6 +34,13 @@ class HiddenTitlebarTerminalWindow: TerminalWindow {
|
||||
|
||||
/// Apply the hidden titlebar style.
|
||||
private func reapplyHiddenStyle() {
|
||||
// If our window is fullscreen then we don't reapply the hidden style because
|
||||
// it can result in messing up non-native fullscreen. See:
|
||||
// https://github.com/ghostty-org/ghostty/issues/8415
|
||||
if terminalController?.fullscreenStyle?.isFullscreen ?? false {
|
||||
return
|
||||
}
|
||||
|
||||
// Apply our style mask while preserving the .fullScreen option
|
||||
if styleMask.contains(.fullScreen) {
|
||||
styleMask = Self.hiddenStyleMask.union([.fullScreen])
|
||||
|
@@ -70,4 +70,39 @@ extension Ghostty.Action {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProgressReport {
|
||||
enum State {
|
||||
case remove
|
||||
case set
|
||||
case error
|
||||
case indeterminate
|
||||
case pause
|
||||
|
||||
init(_ c: ghostty_action_progress_report_state_e) {
|
||||
switch c {
|
||||
case GHOSTTY_PROGRESS_STATE_REMOVE:
|
||||
self = .remove
|
||||
case GHOSTTY_PROGRESS_STATE_SET:
|
||||
self = .set
|
||||
case GHOSTTY_PROGRESS_STATE_ERROR:
|
||||
self = .error
|
||||
case GHOSTTY_PROGRESS_STATE_INDETERMINATE:
|
||||
self = .indeterminate
|
||||
case GHOSTTY_PROGRESS_STATE_PAUSE:
|
||||
self = .pause
|
||||
default:
|
||||
self = .remove
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let state: State
|
||||
let progress: UInt8?
|
||||
|
||||
init(c: ghostty_action_progress_report_s) {
|
||||
self.state = State(c.state)
|
||||
self.progress = c.progress >= 0 ? UInt8(c.progress) : nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -544,6 +544,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||
keySequence(app, target: target, v: action.action.key_sequence)
|
||||
|
||||
case GHOSTTY_ACTION_PROGRESS_REPORT:
|
||||
progressReport(app, target: target, v: action.action.progress_report)
|
||||
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||
configChange(app, target: target, v: action.action.config_change)
|
||||
|
||||
@@ -1524,6 +1527,33 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func progressReport(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
v: ghostty_action_progress_report_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("progress report does nothing with an app target")
|
||||
return
|
||||
|
||||
case GHOSTTY_TARGET_SURFACE:
|
||||
guard let surface = target.target.surface else { return }
|
||||
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||
|
||||
let progressReport = Ghostty.Action.ProgressReport(c: v)
|
||||
DispatchQueue.main.async {
|
||||
if progressReport.state == .remove {
|
||||
surfaceView.progressReport = nil
|
||||
} else {
|
||||
surfaceView.progressReport = progressReport
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func configReload(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
@@ -114,6 +114,11 @@ extension Ghostty {
|
||||
}
|
||||
.ghosttySurfaceView(surfaceView)
|
||||
|
||||
// Progress report overlay
|
||||
if let progressReport = surfaceView.progressReport {
|
||||
ProgressReportOverlay(report: progressReport)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
// If we are in the middle of a key sequence, then we show a visual element. We only
|
||||
// support this on macOS currently although in theory we can support mobile with keyboards!
|
||||
@@ -267,6 +272,49 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// Progress report overlay that shows a progress bar at the top of the terminal
|
||||
struct ProgressReportOverlay: View {
|
||||
let report: Action.ProgressReport
|
||||
|
||||
@ViewBuilder
|
||||
private var progressBar: some View {
|
||||
if let progress = report.progress {
|
||||
// Determinate progress bar
|
||||
ProgressView(value: Double(progress), total: 100)
|
||||
.progressViewStyle(.linear)
|
||||
.tint(report.state == .error ? .red : report.state == .pause ? .orange : nil)
|
||||
.animation(.easeInOut(duration: 0.2), value: progress)
|
||||
} else {
|
||||
// Indeterminate states
|
||||
switch report.state {
|
||||
case .indeterminate:
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
case .error:
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
.tint(.red)
|
||||
case .pause:
|
||||
Rectangle().fill(Color.orange)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
progressBar
|
||||
.scaleEffect(x: 1, y: 0.5, anchor: .center)
|
||||
.frame(height: 2)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the resize overlay that shows on top of a surface to show the current
|
||||
// size during a resize operation.
|
||||
struct SurfaceResizeOverlay: View {
|
||||
@@ -425,6 +473,9 @@ extension Ghostty {
|
||||
/// Extra input to send as stdin
|
||||
var initialInput: String? = nil
|
||||
|
||||
/// Wait after the command
|
||||
var waitAfterCommand: Bool = false
|
||||
|
||||
init() {}
|
||||
|
||||
init(from config: ghostty_surface_config_s) {
|
||||
@@ -476,6 +527,9 @@ extension Ghostty {
|
||||
// Zero is our default value that means to inherit the font size.
|
||||
config.font_size = fontSize ?? 0
|
||||
|
||||
// Set wait after command
|
||||
config.wait_after_command = waitAfterCommand
|
||||
|
||||
// Use withCString to ensure strings remain valid for the duration of the closure
|
||||
return try workingDirectory.withCString { cWorkingDir in
|
||||
config.working_directory = cWorkingDir
|
||||
|
@@ -6,9 +6,11 @@ import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// The NSView implementation for a terminal surface.
|
||||
class SurfaceView: OSView, ObservableObject, Codable {
|
||||
class SurfaceView: OSView, ObservableObject, Codable, Identifiable {
|
||||
typealias ID = UUID
|
||||
|
||||
/// Unique ID per surface
|
||||
let uuid: UUID
|
||||
let id: UUID
|
||||
|
||||
// The current title of the surface as defined by the pty. This can be
|
||||
// changed with escape codes. This is public because the callbacks go
|
||||
@@ -42,6 +44,23 @@ extension Ghostty {
|
||||
// The hovered URL string
|
||||
@Published var hoverUrl: String? = nil
|
||||
|
||||
// The progress report (if any)
|
||||
@Published var progressReport: Action.ProgressReport? = nil {
|
||||
didSet {
|
||||
// Cancel any existing timer
|
||||
progressReportTimer?.invalidate()
|
||||
progressReportTimer = nil
|
||||
|
||||
// If we have a new progress report, start a timer to remove it after 15 seconds
|
||||
if progressReport != nil {
|
||||
progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
|
||||
self?.progressReport = nil
|
||||
self?.progressReportTimer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The currently active key sequence. The sequence is not active if this is empty.
|
||||
@Published var keySequence: [KeyboardShortcut] = []
|
||||
|
||||
@@ -143,6 +162,9 @@ extension Ghostty {
|
||||
// A timer to fallback to ghost emoji if no title is set within the grace period
|
||||
private var titleFallbackTimer: Timer?
|
||||
|
||||
// Timer to remove progress report after 15 seconds
|
||||
private var progressReportTimer: Timer?
|
||||
|
||||
// This is the title from the terminal. This is nil if we're currently using
|
||||
// the terminal title as the main title property. If the title is set manually
|
||||
// by the user, this is set to the prior value (which may be empty, but non-nil).
|
||||
@@ -160,7 +182,7 @@ extension Ghostty {
|
||||
|
||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
self.uuid = uuid ?? .init()
|
||||
self.id = uuid ?? .init()
|
||||
|
||||
// Our initial config always is our application wide config.
|
||||
if let appDelegate = NSApplication.shared.delegate as? AppDelegate {
|
||||
@@ -348,6 +370,9 @@ extension Ghostty {
|
||||
// Remove any notifications associated with this surface
|
||||
let identifiers = Array(self.notificationIdentifiers)
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
|
||||
|
||||
// Cancel progress report timer
|
||||
progressReportTimer?.invalidate()
|
||||
}
|
||||
|
||||
func focusDidChange(_ focused: Bool) {
|
||||
@@ -1445,7 +1470,7 @@ extension Ghostty {
|
||||
content.body = body
|
||||
content.sound = UNNotificationSound.default
|
||||
content.categoryIdentifier = Ghostty.userNotificationCategory
|
||||
content.userInfo = ["surface": self.uuid.uuidString]
|
||||
content.userInfo = ["surface": self.id.uuidString]
|
||||
|
||||
let uuid = UUID().uuidString
|
||||
let request = UNNotificationRequest(
|
||||
@@ -1553,7 +1578,7 @@ extension Ghostty {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(pwd, forKey: .pwd)
|
||||
try container.encode(uuid.uuidString, forKey: .uuid)
|
||||
try container.encode(id.uuidString, forKey: .uuid)
|
||||
try container.encode(title, forKey: .title)
|
||||
try container.encode(titleFromTerminal != nil, forKey: .isUserSetTitle)
|
||||
}
|
||||
@@ -1660,8 +1685,10 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
}
|
||||
|
||||
// Ghostty will tell us where it thinks an IME keyboard should render.
|
||||
var x: Double = 0;
|
||||
var y: Double = 0;
|
||||
var x: Double = 0
|
||||
var y: Double = 0
|
||||
var width: Double = cellSize.width
|
||||
var height: Double = cellSize.height
|
||||
|
||||
// QuickLook never gives us a matching range to our selection so if we detect
|
||||
// this then we return the top-left selection point rather than the cursor point.
|
||||
@@ -1679,15 +1706,19 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
// Free our text
|
||||
ghostty_surface_free_text(surface, &text)
|
||||
} else {
|
||||
ghostty_surface_ime_point(surface, &x, &y)
|
||||
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
|
||||
}
|
||||
} else {
|
||||
ghostty_surface_ime_point(surface, &x, &y)
|
||||
ghostty_surface_ime_point(surface, &x, &y, &width, &height)
|
||||
}
|
||||
|
||||
// Ghostty coordinates are in top-left (0, 0) so we have to convert to
|
||||
// bottom-left since that is what UIKit expects
|
||||
let viewRect = NSMakeRect(x, frame.size.height - y, 0, 0)
|
||||
let viewRect = NSMakeRect(
|
||||
x,
|
||||
frame.size.height - y,
|
||||
max(width, cellSize.width),
|
||||
max(height, cellSize.height))
|
||||
|
||||
// Convert the point to the window coordinates
|
||||
let winRect = self.convert(viewRect, to: nil)
|
||||
|
@@ -31,6 +31,9 @@ extension Ghostty {
|
||||
// The hovered URL
|
||||
@Published var hoverUrl: String? = nil
|
||||
|
||||
// The progress report (if any)
|
||||
@Published var progressReport: Action.ProgressReport? = nil
|
||||
|
||||
// The time this surface last became focused. This is a ContinuousClock.Instant
|
||||
// on supported platforms.
|
||||
@Published var focusInstant: ContinuousClock.Instant? = nil
|
||||
|
@@ -407,9 +407,15 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
self.styleMask = window.styleMask
|
||||
self.toolbar = window.toolbar
|
||||
self.toolbarStyle = window.toolbarStyle
|
||||
self.titlebarAccessoryViewControllers = window.titlebarAccessoryViewControllers
|
||||
self.dock = window.screen?.hasDock ?? false
|
||||
|
||||
self.titlebarAccessoryViewControllers = if (window.hasTitleBar) {
|
||||
// Accessing titlebarAccessoryViewControllers without a titlebar triggers a crash.
|
||||
window.titlebarAccessoryViewControllers
|
||||
} else {
|
||||
[]
|
||||
}
|
||||
|
||||
if let cgWindowId = window.cgWindowId {
|
||||
// We hide the menu only if this window is not on any fullscreen
|
||||
// spaces. We do this because fullscreen spaces already hide the
|
||||
|
@@ -3,6 +3,7 @@
|
||||
lib,
|
||||
stdenv,
|
||||
bashInteractive,
|
||||
nushell,
|
||||
appstream,
|
||||
flatpak-builder,
|
||||
gdb,
|
||||
@@ -60,6 +61,7 @@
|
||||
pandoc,
|
||||
pinact,
|
||||
hyperfine,
|
||||
poop,
|
||||
typos,
|
||||
shellcheck,
|
||||
uv,
|
||||
@@ -124,6 +126,9 @@ in
|
||||
# CI
|
||||
uv
|
||||
|
||||
# Scripting
|
||||
nushell
|
||||
|
||||
# We need these GTK-related deps on all platform so we can build
|
||||
# dist tarballs.
|
||||
blueprint-compiler
|
||||
@@ -183,6 +188,9 @@ in
|
||||
# developer shell
|
||||
glycin-loaders
|
||||
librsvg
|
||||
|
||||
# for benchmarking
|
||||
poop
|
||||
];
|
||||
|
||||
# This should be set onto the rpath of the ghostty binary if you want
|
||||
|
@@ -2,14 +2,15 @@
|
||||
# Copyright (C) 2025 Mitchell Hashimoto
|
||||
# This file is distributed under the same license as the com.mitchellh.ghostty package.
|
||||
# Damyan Bogoev <damyan.bogoev@gmail.com>, 2025.
|
||||
# reo101 <pavel.atanasov2001@gmail.com>, 2025.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
|
||||
"PO-Revision-Date: 2025-05-19 11:34+0300\n"
|
||||
"Last-Translator: Damyan Bogoev <damyan.bogoev@gmail.com>\n"
|
||||
"PO-Revision-Date: 2025-08-22 14:52+0300\n"
|
||||
"Last-Translator: reo101 <pavel.atanasov2001@gmail.com>\n"
|
||||
"Language-Team: Bulgarian <dict@ludost.net>\n"
|
||||
"Language: bg\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -208,12 +209,12 @@ msgstr "Позволи"
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr ""
|
||||
msgstr "Запомни избора за това разделяне"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr ""
|
||||
msgstr "За да покажеш това съобщение отново, презареди конфигурацията"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
@@ -278,15 +279,15 @@ msgstr "Копирано в клипборда"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1268
|
||||
msgid "Cleared clipboard"
|
||||
msgstr ""
|
||||
msgstr "Клипбордът е изчистен"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2525
|
||||
msgid "Command succeeded"
|
||||
msgstr ""
|
||||
msgstr "Командата завърши успешно"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2527
|
||||
msgid "Command failed"
|
||||
msgstr ""
|
||||
msgstr "Командата завърши неуспешно"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:216
|
||||
msgid "Main Menu"
|
||||
|
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
|
||||
"PO-Revision-Date: 2025-06-29 21:15+0100\n"
|
||||
"PO-Revision-Date: 2025-08-26 15:46+0100\n"
|
||||
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
|
||||
"Language-Team: Irish <gaeilge-gnulinux@lists.sourceforge.net>\n"
|
||||
"Language: ga\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n"
|
||||
"X-Generator: Poedit 3.4.4\n"
|
||||
"X-Generator: Poedit 3.4.2\n"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/prompt-title-dialog.blp:5
|
||||
msgid "Change Terminal Title"
|
||||
@@ -209,12 +209,12 @@ msgstr "Ceadaigh"
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr ""
|
||||
msgstr "Sábháil an rogha don scoilt seo"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr ""
|
||||
msgstr "Athlódáil an chumraíocht chun an teachtaireacht seo a thaispeáint arís"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
@@ -280,15 +280,15 @@ msgstr "Cóipeáilte chuig an ghearrthaisce"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1268
|
||||
msgid "Cleared clipboard"
|
||||
msgstr ""
|
||||
msgstr "Gearrthaisce glanta"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2525
|
||||
msgid "Command succeeded"
|
||||
msgstr ""
|
||||
msgstr "D'éirigh leis an ordú"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2527
|
||||
msgid "Command failed"
|
||||
msgstr ""
|
||||
msgstr "Theip ar an ordú"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:216
|
||||
msgid "Main Menu"
|
||||
|
@@ -3,14 +3,15 @@
|
||||
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
|
||||
# This file is distributed under the same license as the com.mitchellh.ghostty package.
|
||||
# Gustavo Peres <gsodevel@gmail.com>, 2025.
|
||||
# Guilherme Tiscoski <github@guilhermetiscoski.com>, 2025.
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
|
||||
"PO-Revision-Date: 2025-06-20 10:19-0300\n"
|
||||
"Last-Translator: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>\n"
|
||||
"PO-Revision-Date: 2025-08-25 11:46-0500\n"
|
||||
"Last-Translator: Guilherme Tiscoski <github@guihermetiscoski.com>\n"
|
||||
"Language-Team: Brazilian Portuguese <ldpbr-translation@lists.sourceforge."
|
||||
"net>\n"
|
||||
"Language: pt_BR\n"
|
||||
@@ -89,7 +90,7 @@ msgstr "Dividir à direita"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
|
||||
msgid "Execute a command…"
|
||||
msgstr ""
|
||||
msgstr "Executar um comando…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
|
||||
@@ -162,7 +163,7 @@ msgstr "Abrir configuração"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
|
||||
msgid "Command Palette"
|
||||
msgstr ""
|
||||
msgstr "Paleta de comandos"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
|
||||
msgid "Terminal Inspector"
|
||||
@@ -210,12 +211,12 @@ msgstr "Permitir"
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr ""
|
||||
msgstr "Lembrar escolha para esta divisão"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr ""
|
||||
msgstr "Recarregue a configuração para mostrar este aviso novamente"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
@@ -280,15 +281,15 @@ msgstr "Copiado para a área de transferência"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1268
|
||||
msgid "Cleared clipboard"
|
||||
msgstr ""
|
||||
msgstr "Área de transferência limpa"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2525
|
||||
msgid "Command succeeded"
|
||||
msgstr ""
|
||||
msgstr "Comando executado com sucesso"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2527
|
||||
msgid "Command failed"
|
||||
msgstr ""
|
||||
msgstr "Comando falhou"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:216
|
||||
msgid "Main Menu"
|
||||
|
@@ -3,15 +3,16 @@
|
||||
# Copyright (C) 2025 Mitchell Hashimoto, Ghostty contributors
|
||||
# This file is distributed under the same license as the com.mitchellh.ghostty package.
|
||||
# blackzeshi <sergey_zhuzhgov@mail.ru>, 2025.
|
||||
#
|
||||
# Ivan Bastrakov <bastaynav@proton.me>, 2025.
|
||||
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-07-22 17:18+0000\n"
|
||||
"PO-Revision-Date: 2025-03-24 00:01+0500\n"
|
||||
"Last-Translator: blackzeshi <sergey_zhuzhgov@mail.ru>\n"
|
||||
"Language-Team: Russian <gnu@d07.ru>\n"
|
||||
"PO-Revision-Date: 2025-09-03 01:50+0300\n"
|
||||
"Last-Translator: Ivan Bastrakov <bastaynav@proton.me>\n"
|
||||
"Language: ru\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -89,7 +90,7 @@ msgstr "Сплит вправо"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/command-palette.blp:16
|
||||
msgid "Execute a command…"
|
||||
msgstr ""
|
||||
msgstr "Выполнить команду…"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-surface-context_menu.blp:6
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:6
|
||||
@@ -162,7 +163,7 @@ msgstr "Открыть конфигурационный файл"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:85
|
||||
msgid "Command Palette"
|
||||
msgstr ""
|
||||
msgstr "Палитра команд"
|
||||
|
||||
#: src/apprt/gtk/ui/1.0/menu-window-titlebar_menu.blp:90
|
||||
msgid "Terminal Inspector"
|
||||
@@ -210,12 +211,12 @@ msgstr "Разрешить"
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:81
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:77
|
||||
msgid "Remember choice for this split"
|
||||
msgstr ""
|
||||
msgstr "Запомнить выбор для этого сплита"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-read.blp:82
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:78
|
||||
msgid "Reload configuration to show this prompt again"
|
||||
msgstr ""
|
||||
msgstr "Перезагрузите конфигурацию, чтобы снова увидеть это сообщение"
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-osc-52-write.blp:7
|
||||
#: src/apprt/gtk/ui/1.2/ccw-osc-52-write.blp:7
|
||||
@@ -223,7 +224,8 @@ msgid ""
|
||||
"An application is attempting to write to the clipboard. The current "
|
||||
"clipboard contents are shown below."
|
||||
msgstr ""
|
||||
"Приложение пытается записать данные в буфер обмена. Эти данные показаны ниже."
|
||||
"Приложение пытается записать данные в буфер обмена. Текущее содержимое "
|
||||
"буфера обмена показано ниже."
|
||||
|
||||
#: src/apprt/gtk/ui/1.5/ccw-paste.blp:6 src/apprt/gtk/ui/1.2/ccw-paste.blp:6
|
||||
msgid "Warning: Potentially Unsafe Paste"
|
||||
@@ -279,15 +281,15 @@ msgstr "Скопировано в буфер обмена"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:1268
|
||||
msgid "Cleared clipboard"
|
||||
msgstr ""
|
||||
msgstr "Буфер обмена очищен"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2525
|
||||
msgid "Command succeeded"
|
||||
msgstr ""
|
||||
msgstr "Команда выполнена успешно"
|
||||
|
||||
#: src/apprt/gtk/Surface.zig:2527
|
||||
msgid "Command failed"
|
||||
msgstr ""
|
||||
msgstr "Команда завершилась с ошибкой"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:216
|
||||
msgid "Main Menu"
|
||||
@@ -299,7 +301,7 @@ msgstr "Просмотреть открытые вкладки"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:266
|
||||
msgid "New Split"
|
||||
msgstr ""
|
||||
msgstr "Новый сплит"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:329
|
||||
msgid ""
|
||||
|
@@ -258,6 +258,7 @@ const DerivedConfig = struct {
|
||||
mouse_shift_capture: configpkg.MouseShiftCapture,
|
||||
macos_non_native_fullscreen: configpkg.NonNativeFullscreen,
|
||||
macos_option_as_alt: ?configpkg.OptionAsAlt,
|
||||
selection_clear_on_copy: bool,
|
||||
selection_clear_on_typing: bool,
|
||||
vt_kam_allowed: bool,
|
||||
wait_after_command: bool,
|
||||
@@ -272,6 +273,7 @@ const DerivedConfig = struct {
|
||||
title_report: bool,
|
||||
links: []Link,
|
||||
link_previews: configpkg.LinkPreviews,
|
||||
scroll_to_bottom: configpkg.Config.ScrollToBottom,
|
||||
|
||||
const Link = struct {
|
||||
regex: oni.Regex,
|
||||
@@ -326,6 +328,7 @@ const DerivedConfig = struct {
|
||||
.mouse_shift_capture = config.@"mouse-shift-capture",
|
||||
.macos_non_native_fullscreen = config.@"macos-non-native-fullscreen",
|
||||
.macos_option_as_alt = config.@"macos-option-as-alt",
|
||||
.selection_clear_on_copy = config.@"selection-clear-on-copy",
|
||||
.selection_clear_on_typing = config.@"selection-clear-on-typing",
|
||||
.vt_kam_allowed = config.@"vt-kam-allowed",
|
||||
.wait_after_command = config.@"wait-after-command",
|
||||
@@ -340,6 +343,7 @@ const DerivedConfig = struct {
|
||||
.title_report = config.@"title-report",
|
||||
.links = links,
|
||||
.link_previews = config.@"link-previews",
|
||||
.scroll_to_bottom = config.@"scroll-to-bottom",
|
||||
|
||||
// Assignments happen sequentially so we have to do this last
|
||||
// so that the memory is captured from allocs above.
|
||||
@@ -1728,6 +1732,7 @@ pub fn pwd(
|
||||
pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
self.renderer_state.mutex.lock();
|
||||
const cursor = self.renderer_state.terminal.screen.cursor;
|
||||
const preedit_width: usize = if (self.renderer_state.preedit) |preedit| preedit.width() else 0;
|
||||
self.renderer_state.mutex.unlock();
|
||||
|
||||
// TODO: need to handle when scrolling and the cursor is not
|
||||
@@ -1762,7 +1767,38 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
break :y y;
|
||||
};
|
||||
|
||||
return .{ .x = x, .y = y };
|
||||
// Our height for now is always just the cell height because our preedit
|
||||
// rendering only renders in a single line.
|
||||
const height: f64 = height: {
|
||||
var height: f64 = @floatFromInt(self.size.cell.height);
|
||||
height /= content_scale.y;
|
||||
break :height height;
|
||||
};
|
||||
const width: f64 = width: {
|
||||
var width: f64 = @floatFromInt(preedit_width * self.size.cell.width);
|
||||
|
||||
// Our max width is the remaining screen width after the cursor.
|
||||
// We don't have to deal with wrapping because the preedit doesn't
|
||||
// wrap right now.
|
||||
const screen_width: f64 = @floatFromInt(self.size.terminal().width);
|
||||
const x_offset: f64 = @floatFromInt((cursor.x + 1) * self.size.cell.width);
|
||||
const max = screen_width - x_offset;
|
||||
width = @min(width, max);
|
||||
|
||||
// Note: we don't apply content scale here because it looks like
|
||||
// for some reason in macOS its already scaled. I'm not sure why
|
||||
// that is so I'm going to just leave this comment here so its known
|
||||
// that I left this out on purpose pending more investigation.
|
||||
|
||||
break :width width;
|
||||
};
|
||||
|
||||
return .{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
};
|
||||
}
|
||||
|
||||
fn clipboardWrite(self: *const Surface, data: []const u8, loc: apprt.Clipboard) !void {
|
||||
@@ -2280,7 +2316,8 @@ pub fn keyCallback(
|
||||
try self.setSelection(null);
|
||||
}
|
||||
|
||||
try self.io.terminal.scrollViewport(.{ .bottom = {} });
|
||||
if (self.config.scroll_to_bottom.keystroke) try self.io.terminal.scrollViewport(.bottom);
|
||||
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
@@ -2766,8 +2803,21 @@ pub fn scrollCallback(
|
||||
// that a wheel tick of 1 results in single scroll event.
|
||||
const yoff_adjusted: f64 = if (scroll_mods.precision)
|
||||
yoff
|
||||
else
|
||||
yoff * cell_size * self.config.mouse_scroll_multiplier;
|
||||
else yoff_adjusted: {
|
||||
// Round out the yoff to an absolute minimum of 1. macos tries to
|
||||
// simulate precision scrolling with non precision events by
|
||||
// ramping up the magnitude of the offsets as it detects faster
|
||||
// scrolling. Single click (very slow) scrolls are reported with a
|
||||
// magnitude of 0.1 which would normally require a few clicks
|
||||
// before we register an actual scroll event (depending on cell
|
||||
// height and the mouse_scroll_multiplier setting).
|
||||
const yoff_max: f64 = if (yoff > 0)
|
||||
@max(yoff, 1)
|
||||
else
|
||||
@min(yoff, -1);
|
||||
|
||||
break :yoff_adjusted yoff_max * cell_size * self.config.mouse_scroll_multiplier;
|
||||
};
|
||||
|
||||
// Add our previously saved pending amount to the offset to get the
|
||||
// new offset value. The signs of the pending and yoff should match
|
||||
@@ -4496,6 +4546,17 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
return true;
|
||||
};
|
||||
|
||||
// Clear the selection if configured to do so.
|
||||
if (self.config.selection_clear_on_copy) {
|
||||
if (self.setSelection(null)) {
|
||||
self.queueRender() catch |err| {
|
||||
log.warn("failed to queue render after clear selection err={}", .{err});
|
||||
};
|
||||
} else |err| {
|
||||
log.warn("failed to clear selection after copy err={}", .{err});
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@@ -17,7 +17,6 @@ const structs = @import("apprt/structs.zig");
|
||||
pub const action = @import("apprt/action.zig");
|
||||
pub const ipc = @import("apprt/ipc.zig");
|
||||
pub const gtk = @import("apprt/gtk.zig");
|
||||
pub const gtk_ng = @import("apprt/gtk-ng.zig");
|
||||
pub const none = @import("apprt/none.zig");
|
||||
pub const browser = @import("apprt/browser.zig");
|
||||
pub const embedded = @import("apprt/embedded.zig");
|
||||
@@ -44,7 +43,6 @@ pub const runtime = switch (build_config.artifact) {
|
||||
.exe => switch (build_config.app_runtime) {
|
||||
.none => none,
|
||||
.gtk => gtk,
|
||||
.@"gtk-ng" => gtk_ng,
|
||||
},
|
||||
.lib => embedded,
|
||||
.wasm_module => browser,
|
||||
@@ -62,21 +60,18 @@ pub const Runtime = enum {
|
||||
|
||||
/// GTK4. Rich windowed application. This uses a full GObject-based
|
||||
/// approach to building the application.
|
||||
@"gtk-ng",
|
||||
|
||||
/// GTK-backed. Rich windowed application. GTK is dynamically linked.
|
||||
/// WARNING: Deprecated. This will be removed very soon. All bug fixes
|
||||
/// and features should go into the gtk-ng backend.
|
||||
gtk,
|
||||
|
||||
pub fn default(target: std.Target) Runtime {
|
||||
// The Linux default is GTK because it is a full featured application.
|
||||
if (target.os.tag == .linux) return .@"gtk-ng";
|
||||
|
||||
// Otherwise, we do NONE so we don't create an exe and we
|
||||
// create libghostty. On macOS, Xcode is used to build the app
|
||||
// that links to libghostty.
|
||||
return .none;
|
||||
return switch (target.os.tag) {
|
||||
// The Linux and FreeBSD default is GTK because it is a full
|
||||
// featured application.
|
||||
.linux, .freebsd => .gtk,
|
||||
// Otherwise, we do NONE so we don't create an exe and we create
|
||||
// libghostty. On macOS, Xcode is used to build the app that links
|
||||
// to libghostty.
|
||||
else => .none,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -542,7 +542,7 @@ pub const InitialSize = extern struct {
|
||||
|
||||
/// 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(
|
||||
.gtk => @import("gobject").ext.defineBoxed(
|
||||
InitialSize,
|
||||
.{ .name = "GhosttyApprtInitialSize" },
|
||||
),
|
||||
|
@@ -447,6 +447,9 @@ pub const Surface = struct {
|
||||
|
||||
/// Input to send to the command after it is started.
|
||||
initial_input: ?[*:0]const u8 = null,
|
||||
|
||||
/// Wait after the command exits
|
||||
wait_after_command: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
@@ -540,6 +543,11 @@ pub const Surface = struct {
|
||||
);
|
||||
}
|
||||
|
||||
// Wait after command
|
||||
if (opts.wait_after_command) {
|
||||
config.@"wait-after-command" = true;
|
||||
}
|
||||
|
||||
// Initialize our surface right away. We're given a view that is
|
||||
// ready to use.
|
||||
try self.core_surface.init(
|
||||
@@ -901,10 +909,7 @@ pub const Surface = struct {
|
||||
// our translation settings for Ghostty. If we aren't from
|
||||
// the desktop then we didn't set our LANGUAGE var so we
|
||||
// don't need to remove it.
|
||||
switch (self.app.config.@"launched-from".?) {
|
||||
.desktop => env.remove("LANGUAGE"),
|
||||
.dbus, .systemd, .cli => {},
|
||||
}
|
||||
if (internal_os.launchedFromDesktop()) env.remove("LANGUAGE");
|
||||
}
|
||||
|
||||
return env;
|
||||
@@ -1814,10 +1819,18 @@ pub const CAPI = struct {
|
||||
surface.mousePressureCallback(stage, pressure);
|
||||
}
|
||||
|
||||
export fn ghostty_surface_ime_point(surface: *Surface, x: *f64, y: *f64) void {
|
||||
export fn ghostty_surface_ime_point(
|
||||
surface: *Surface,
|
||||
x: *f64,
|
||||
y: *f64,
|
||||
width: *f64,
|
||||
height: *f64,
|
||||
) void {
|
||||
const pos = surface.core_surface.imePoint();
|
||||
x.* = pos.x;
|
||||
y.* = pos.y;
|
||||
width.* = pos.width;
|
||||
height.* = pos.height;
|
||||
}
|
||||
|
||||
/// Request that the surface become closed. This will go through the
|
||||
|
@@ -1,15 +0,0 @@
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
// The required comptime API for any apprt.
|
||||
pub const App = @import("gtk-ng/App.zig");
|
||||
pub const Surface = @import("gtk-ng/Surface.zig");
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
|
||||
// The exported API, custom for the apprt.
|
||||
pub const class = @import("gtk-ng/class.zig");
|
||||
pub const WeakRef = @import("gtk-ng/weak_ref.zig").WeakRef;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
_ = @import("gtk-ng/ext.zig");
|
||||
}
|
@@ -1,104 +0,0 @@
|
||||
/// This is the main entrypoint to the apprt for Ghostty. Ghostty will
|
||||
/// initialize this in main to start the application..
|
||||
const App = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const Config = configpkg.Config;
|
||||
const CoreApp = @import("../../App.zig");
|
||||
|
||||
const Application = @import("class/application.zig").Application;
|
||||
const Surface = @import("Surface.zig");
|
||||
const gtk_version = @import("gtk_version.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
const ipcNewWindow = @import("ipc/new_window.zig").newWindow;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// This is detected by the Renderer, in which case it sends a `redraw_surface`
|
||||
/// message so that we can call `drawFrame` ourselves from the app thread,
|
||||
/// because GTK's `GLArea` does not support drawing from a different thread.
|
||||
pub const must_draw_from_app_thread = true;
|
||||
|
||||
/// GTK application ID
|
||||
pub const application_id = switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => "com.mitchellh.ghostty-debug",
|
||||
.ReleaseFast, .ReleaseSmall => "com.mitchellh.ghostty",
|
||||
};
|
||||
|
||||
/// GTK object path
|
||||
pub const object_path = switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => "/com/mitchellh/ghostty_debug",
|
||||
.ReleaseFast, .ReleaseSmall => "/com/mitchellh/ghostty",
|
||||
};
|
||||
|
||||
/// The GObject Application instance
|
||||
app: *Application,
|
||||
|
||||
pub fn init(
|
||||
self: *App,
|
||||
core_app: *CoreApp,
|
||||
|
||||
// Required by the apprt interface but we don't use it.
|
||||
opts: struct {},
|
||||
) !void {
|
||||
_ = opts;
|
||||
|
||||
const app: *Application = try .new(self, core_app);
|
||||
errdefer app.unref();
|
||||
self.* = .{ .app = app };
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn run(self: *App) !void {
|
||||
try self.app.run();
|
||||
}
|
||||
|
||||
pub fn terminate(self: *App) void {
|
||||
// We force deinitialize the app. We don't unref because other things
|
||||
// tend to have a reference at this point, so this just forces the
|
||||
// disposal now.
|
||||
self.app.deinit();
|
||||
}
|
||||
|
||||
/// Called by CoreApp to wake up the event loop.
|
||||
pub fn wakeup(self: *App) void {
|
||||
self.app.wakeup();
|
||||
}
|
||||
|
||||
pub fn performAction(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
comptime action: apprt.Action.Key,
|
||||
value: apprt.Action.Value(action),
|
||||
) !bool {
|
||||
return try self.app.performAction(target, action, value);
|
||||
}
|
||||
|
||||
/// Send the given IPC to a running Ghostty. Returns `true` if the action was
|
||||
/// able to be performed, `false` otherwise.
|
||||
///
|
||||
/// Note that this is a static function. Since this is called from a CLI app (or
|
||||
/// some other process that is not Ghostty) there is no full-featured apprt App
|
||||
/// to use.
|
||||
pub fn performIpc(
|
||||
alloc: Allocator,
|
||||
target: apprt.ipc.Target,
|
||||
comptime action: apprt.ipc.Action.Key,
|
||||
value: apprt.ipc.Action.Value(action),
|
||||
) !bool {
|
||||
switch (action) {
|
||||
.new_window => return try ipcNewWindow(alloc, target, value),
|
||||
}
|
||||
}
|
||||
|
||||
/// Redraw the inspector for the given surface.
|
||||
pub fn redrawInspector(_: *App, surface: *Surface) void {
|
||||
surface.redrawInspector();
|
||||
}
|
@@ -1,103 +0,0 @@
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const ApprtApp = @import("App.zig");
|
||||
const Application = @import("class/application.zig").Application;
|
||||
const Surface = @import("class/surface.zig").Surface;
|
||||
|
||||
/// The GObject Surface
|
||||
surface: *Surface,
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
_ = self;
|
||||
}
|
||||
|
||||
/// Returns the GObject surface for this apprt surface. This is a function
|
||||
/// so we can add some extra logic if we ever have to here.
|
||||
pub fn gobj(self: *Self) *Surface {
|
||||
return self.surface;
|
||||
}
|
||||
|
||||
pub fn core(self: *Self) *CoreSurface {
|
||||
// This asserts the non-optional because libghostty should only
|
||||
// be calling this for initialized surfaces.
|
||||
return self.surface.core().?;
|
||||
}
|
||||
|
||||
pub fn rtApp(self: *Self) *ApprtApp {
|
||||
_ = self;
|
||||
return Application.default().rt();
|
||||
}
|
||||
|
||||
pub fn close(self: *Self, process_active: bool) void {
|
||||
_ = process_active;
|
||||
self.surface.close();
|
||||
}
|
||||
|
||||
pub fn cgroup(self: *Self) ?[]const u8 {
|
||||
return self.surface.cgroupPath();
|
||||
}
|
||||
|
||||
pub fn getTitle(self: *Self) ?[:0]const u8 {
|
||||
return self.surface.getTitle();
|
||||
}
|
||||
|
||||
pub fn getContentScale(self: *const Self) !apprt.ContentScale {
|
||||
return self.surface.getContentScale();
|
||||
}
|
||||
|
||||
pub fn getSize(self: *const Self) !apprt.SurfaceSize {
|
||||
return self.surface.getSize();
|
||||
}
|
||||
|
||||
pub fn getCursorPos(self: *const Self) !apprt.CursorPos {
|
||||
return self.surface.getCursorPos();
|
||||
}
|
||||
|
||||
pub fn supportsClipboard(
|
||||
self: *const Self,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
) bool {
|
||||
_ = self;
|
||||
return switch (clipboard_type) {
|
||||
.standard,
|
||||
.selection,
|
||||
.primary,
|
||||
=> true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clipboardRequest(
|
||||
self: *Self,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
state: apprt.ClipboardRequest,
|
||||
) !void {
|
||||
try self.surface.clipboardRequest(
|
||||
clipboard_type,
|
||||
state,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setClipboardString(
|
||||
self: *Self,
|
||||
val: [:0]const u8,
|
||||
clipboard_type: apprt.Clipboard,
|
||||
confirm: bool,
|
||||
) !void {
|
||||
self.surface.setClipboardString(
|
||||
val,
|
||||
clipboard_type,
|
||||
confirm,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn defaultTermioEnv(self: *Self) !std.process.EnvMap {
|
||||
return try self.surface.defaultTermioEnv();
|
||||
}
|
||||
|
||||
/// Redraw the inspector for our surface.
|
||||
pub fn redrawInspector(self: *Self) void {
|
||||
self.surface.redrawInspector();
|
||||
}
|
@@ -1,122 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Until the gobject bindings are built at the same time we are building
|
||||
// Ghostty, we need to import `adwaita.h` directly to ensure that the version
|
||||
// macros match the version of `libadwaita` that we are building/linking
|
||||
// against.
|
||||
const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
const adw = @import("adw");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const comptime_version: std.SemanticVersion = .{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
.patch = c.ADW_MICRO_VERSION,
|
||||
};
|
||||
|
||||
pub fn getRuntimeVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = adw.getMajorVersion(),
|
||||
.minor = adw.getMinorVersion(),
|
||||
.patch = adw.getMicroVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logVersion() void {
|
||||
log.info("libadwaita version build={} runtime={}", .{
|
||||
comptime_version,
|
||||
getRuntimeVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies that the running libadwaita version is at least the given
|
||||
/// version. This will return false if Ghostty is configured to not build with
|
||||
/// libadwaita.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it is run in a
|
||||
/// comptime context, it will only check the version in the headers. If it is
|
||||
/// run in a runtime context, it will check the actual version of the library we
|
||||
/// are linked against. So generally you probably want to do both checks!
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the runtime checks
|
||||
/// if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version, we can return
|
||||
// false immediately. This prevents us from compiling against unknown
|
||||
// symbols and makes runtime checks very slightly faster.
|
||||
if (comptime comptime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
return runtimeAtLeast(major, minor, micro);
|
||||
}
|
||||
|
||||
/// Verifies that the libadwaita version at runtime is at least the given version.
|
||||
///
|
||||
/// This function should be used in cases where the only the runtime behavior
|
||||
/// is affected by the version check. For checks which would affect code
|
||||
/// generation, use `atLeast`.
|
||||
pub inline fn runtimeAtLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
|
||||
// because the function gets the actual runtime version.
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
test "versionAtLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.ADW_MAJOR_VERSION + 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION + 1, c.ADW_MICRO_VERSION));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION - 1, c.ADW_MINOR_VERSION, c.ADW_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.ADW_MAJOR_VERSION, c.ADW_MINOR_VERSION - 1, c.ADW_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Whether AdwDialog, AdwAlertDialog, etc. are supported (1.5+)
|
||||
pub inline fn supportsDialogs() bool {
|
||||
return atLeast(1, 5, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsTabOverview() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsSwitchRow() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsToolbarView() bool {
|
||||
return atLeast(1, 4, 0);
|
||||
}
|
||||
|
||||
pub inline fn supportsBanner() bool {
|
||||
return atLeast(1, 3, 0);
|
||||
}
|
@@ -1,213 +0,0 @@
|
||||
/// Contains all the logic for putting the Ghostty process and
|
||||
/// each individual surface into its own cgroup.
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_systemd_cgroup);
|
||||
|
||||
pub const Options = struct {
|
||||
memory_high: ?u64 = null,
|
||||
pids_max: ?u64 = null,
|
||||
};
|
||||
|
||||
/// Initialize the cgroup for the app. This will create our
|
||||
/// transient scope, initialize the cgroups we use for the app,
|
||||
/// configure them, and return the cgroup path for the app.
|
||||
///
|
||||
/// Returns the path of the current cgroup for the app, which is
|
||||
/// allocated with the given allocator.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
dbus: *gio.DBusConnection,
|
||||
opts: Options,
|
||||
) ![]const u8 {
|
||||
const pid = std.os.linux.getpid();
|
||||
|
||||
// Get our initial cgroup. We need this so we can compare
|
||||
// and detect when we've switched to our transient group.
|
||||
const original = try internal_os.cgroup.current(
|
||||
alloc,
|
||||
pid,
|
||||
) orelse "";
|
||||
defer alloc.free(original);
|
||||
|
||||
// Create our transient scope. If this succeeds then the unit
|
||||
// was created, but we may not have moved into it yet, so we need
|
||||
// to do a dumb busy loop to wait for the move to complete.
|
||||
try createScope(dbus, pid);
|
||||
const transient = transient: while (true) {
|
||||
const current = try internal_os.cgroup.current(
|
||||
alloc,
|
||||
pid,
|
||||
) orelse "";
|
||||
if (!std.mem.eql(u8, original, current)) break :transient current;
|
||||
alloc.free(current);
|
||||
std.time.sleep(25 * std.time.ns_per_ms);
|
||||
};
|
||||
errdefer alloc.free(transient);
|
||||
log.info("transient scope created cgroup={s}", .{transient});
|
||||
|
||||
// Create the app cgroup and put ourselves in it. This is
|
||||
// required because controllers can't be configured while a
|
||||
// process is in a cgroup.
|
||||
try internal_os.cgroup.create(transient, "app", pid);
|
||||
|
||||
// Create a cgroup that will contain all our surfaces. We will
|
||||
// enable the controllers and configure resource limits for surfaces
|
||||
// only on this cgroup so that it doesn't affect our main app.
|
||||
try internal_os.cgroup.create(transient, "surfaces", null);
|
||||
const surfaces = try std.fmt.allocPrint(alloc, "{s}/surfaces", .{transient});
|
||||
defer alloc.free(surfaces);
|
||||
|
||||
// Enable all of our cgroup controllers. If these fail then
|
||||
// we just log. We can't reasonably undo what we've done above
|
||||
// so we log the warning and still return the transient group.
|
||||
// I don't know a scenario where this fails yet.
|
||||
try enableControllers(alloc, transient);
|
||||
try enableControllers(alloc, surfaces);
|
||||
|
||||
// Configure the "high" memory limit. This limit is used instead
|
||||
// of "max" because it's a soft limit that can be exceeded and
|
||||
// can be monitored by things like systemd-oomd to kill if needed,
|
||||
// versus an instant hard kill.
|
||||
if (opts.memory_high) |limit| {
|
||||
try internal_os.cgroup.configureLimit(surfaces, .{
|
||||
.memory_high = limit,
|
||||
});
|
||||
}
|
||||
|
||||
// Configure the "max" pids limit. This is a hard limit and cannot be
|
||||
// exceeded.
|
||||
if (opts.pids_max) |limit| {
|
||||
try internal_os.cgroup.configureLimit(surfaces, .{
|
||||
.pids_max = limit,
|
||||
});
|
||||
}
|
||||
|
||||
return transient;
|
||||
}
|
||||
|
||||
/// Enable all the cgroup controllers for the given cgroup.
|
||||
fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
|
||||
const raw = try internal_os.cgroup.controllers(alloc, cgroup);
|
||||
defer alloc.free(raw);
|
||||
|
||||
// Build our string builder for enabling all controllers
|
||||
var builder = std.ArrayList(u8).init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
// Controllers are space-separated
|
||||
var it = std.mem.splitScalar(u8, raw, ' ');
|
||||
while (it.next()) |controller| {
|
||||
try builder.append('+');
|
||||
try builder.appendSlice(controller);
|
||||
if (it.rest().len > 0) try builder.append(' ');
|
||||
}
|
||||
|
||||
// Enable them all
|
||||
try internal_os.cgroup.configureControllers(
|
||||
cgroup,
|
||||
builder.items,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a transient systemd scope unit for the current process and
|
||||
/// move our process into it.
|
||||
fn createScope(
|
||||
dbus: *gio.DBusConnection,
|
||||
pid_: std.os.linux.pid_t,
|
||||
) !void {
|
||||
const pid: u32 = @intCast(pid_);
|
||||
|
||||
// The unit name needs to be unique. We use the pid for this.
|
||||
var name_buf: [256]u8 = undefined;
|
||||
const name = std.fmt.bufPrintZ(
|
||||
&name_buf,
|
||||
"app-ghostty-transient-{}.scope",
|
||||
.{pid},
|
||||
) catch unreachable;
|
||||
|
||||
const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))");
|
||||
defer glib.free(builder_type);
|
||||
|
||||
// Initialize our builder to build up our parameters
|
||||
var builder: glib.VariantBuilder = undefined;
|
||||
builder.init(builder_type);
|
||||
|
||||
builder.add("s", name.ptr);
|
||||
builder.add("s", "fail");
|
||||
|
||||
{
|
||||
// Properties
|
||||
const properties_type = glib.VariantType.new("a(sv)");
|
||||
defer glib.free(properties_type);
|
||||
|
||||
builder.open(properties_type);
|
||||
defer builder.close();
|
||||
|
||||
// https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
|
||||
const pressure_value = glib.Variant.newString("kill");
|
||||
|
||||
builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value);
|
||||
|
||||
// Delegate
|
||||
const delegate_value = glib.Variant.newBoolean(1);
|
||||
builder.add("(sv)", "Delegate", delegate_value);
|
||||
|
||||
// Pid to move into the unit
|
||||
const pids_value_type = glib.VariantType.new("u");
|
||||
defer glib.free(pids_value_type);
|
||||
|
||||
const pids_value = glib.Variant.newFixedArray(pids_value_type, &pid, 1, @sizeOf(u32));
|
||||
|
||||
builder.add("(sv)", "PIDs", pids_value);
|
||||
}
|
||||
|
||||
{
|
||||
// Aux
|
||||
const aux_type = glib.VariantType.new("a(sa(sv))");
|
||||
defer glib.free(aux_type);
|
||||
|
||||
builder.open(aux_type);
|
||||
defer builder.close();
|
||||
}
|
||||
|
||||
var err: ?*glib.Error = null;
|
||||
defer if (err) |e| e.free();
|
||||
|
||||
const reply_type = glib.VariantType.new("(o)");
|
||||
defer glib.free(reply_type);
|
||||
|
||||
const value = builder.end();
|
||||
|
||||
const reply = dbus.callSync(
|
||||
"org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager",
|
||||
"StartTransientUnit",
|
||||
value,
|
||||
reply_type,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| log.err(
|
||||
"creating transient cgroup scope failed code={} err={s}",
|
||||
.{
|
||||
e.f_code,
|
||||
if (e.f_message) |msg| msg else "(no message)",
|
||||
},
|
||||
);
|
||||
return error.DbusCallFailed;
|
||||
};
|
||||
defer reply.unref();
|
||||
}
|
@@ -1,140 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Until the gobject bindings are built at the same time we are building
|
||||
// Ghostty, we need to import `gtk/gtk.h` directly to ensure that the version
|
||||
// macros match the version of `gtk4` that we are building/linking against.
|
||||
const c = @cImport({
|
||||
@cInclude("gtk/gtk.h");
|
||||
});
|
||||
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const comptime_version: std.SemanticVersion = .{
|
||||
.major = c.GTK_MAJOR_VERSION,
|
||||
.minor = c.GTK_MINOR_VERSION,
|
||||
.patch = c.GTK_MICRO_VERSION,
|
||||
};
|
||||
|
||||
pub fn getRuntimeVersion() std.SemanticVersion {
|
||||
return .{
|
||||
.major = gtk.getMajorVersion(),
|
||||
.minor = gtk.getMinorVersion(),
|
||||
.patch = gtk.getMicroVersion(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn logVersion() void {
|
||||
log.info("GTK version build={} runtime={}", .{
|
||||
comptime_version,
|
||||
getRuntimeVersion(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Verifies that the GTK version is at least the given version.
|
||||
///
|
||||
/// This can be run in both a comptime and runtime context. If it is run in a
|
||||
/// comptime context, it will only check the version in the headers. If it is
|
||||
/// run in a runtime context, it will check the actual version of the library we
|
||||
/// are linked against.
|
||||
///
|
||||
/// This function should be used in cases where the version check would affect
|
||||
/// code generation, such as using symbols that are only available beyond a
|
||||
/// certain version. For checks which only depend on GTK's runtime behavior,
|
||||
/// use `runtimeAtLeast`.
|
||||
///
|
||||
/// This is inlined so that the comptime checks will disable the runtime checks
|
||||
/// if the comptime checks fail.
|
||||
pub inline fn atLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// If our header has lower versions than the given version,
|
||||
// we can return false immediately. This prevents us from
|
||||
// compiling against unknown symbols and makes runtime checks
|
||||
// very slightly faster.
|
||||
if (comptime comptime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt) return false;
|
||||
|
||||
// If we're in comptime then we can't check the runtime version.
|
||||
if (@inComptime()) return true;
|
||||
|
||||
return runtimeAtLeast(major, minor, micro);
|
||||
}
|
||||
|
||||
/// Verifies that the GTK version at runtime is at least the given version.
|
||||
///
|
||||
/// This function should be used in cases where the only the runtime behavior
|
||||
/// is affected by the version check. For checks which would affect code
|
||||
/// generation, use `atLeast`.
|
||||
pub inline fn runtimeAtLeast(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
// We use the functions instead of the constants such as c.GTK_MINOR_VERSION
|
||||
// because the function gets the actual runtime version.
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) != .lt;
|
||||
}
|
||||
|
||||
pub inline fn runtimeUntil(
|
||||
comptime major: u16,
|
||||
comptime minor: u16,
|
||||
comptime micro: u16,
|
||||
) bool {
|
||||
const runtime_version = getRuntimeVersion();
|
||||
return runtime_version.order(.{
|
||||
.major = major,
|
||||
.minor = minor,
|
||||
.patch = micro,
|
||||
}) == .lt;
|
||||
}
|
||||
|
||||
test "atLeast" {
|
||||
const testing = std.testing;
|
||||
|
||||
const funs = &.{ atLeast, runtimeAtLeast };
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
||||
test "runtimeUntil" {
|
||||
const testing = std.testing;
|
||||
|
||||
// This is an array in case we add a comptime variant.
|
||||
const funs = &.{runtimeUntil};
|
||||
inline for (funs) |fun| {
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
|
||||
try testing.expect(!fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
@@ -1,62 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const glib = @import("glib");
|
||||
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const DBus = @import("DBus.zig");
|
||||
|
||||
// Use a D-Bus method call to open a new window on GTK.
|
||||
// See: https://wiki.gnome.org/Projects/GLib/GApplication/DBusAPI
|
||||
//
|
||||
// `ghostty +new-window` is equivalent to the following command (on a release build):
|
||||
//
|
||||
// ```
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window [] []
|
||||
// ```
|
||||
//
|
||||
// `ghostty +new-window -e echo hello` would be equivalent to the following command (on a release build):
|
||||
//
|
||||
// ```
|
||||
// gdbus call --session --dest com.mitchellh.ghostty --object-path /com/mitchellh/ghostty --method org.gtk.Actions.Activate new-window-command '[<@as ["echo" "hello"]>]' []
|
||||
// ```
|
||||
pub fn newWindow(alloc: Allocator, target: apprt.ipc.Target, value: apprt.ipc.Action.NewWindow) (Allocator.Error || std.posix.WriteError || apprt.ipc.Errors)!bool {
|
||||
var dbus = try DBus.init(
|
||||
alloc,
|
||||
target,
|
||||
if (value.arguments == null)
|
||||
"new-window"
|
||||
else
|
||||
"new-window-command",
|
||||
);
|
||||
defer dbus.deinit(alloc);
|
||||
|
||||
if (value.arguments) |arguments| {
|
||||
// If `-e` was specified on the command line, the first
|
||||
// parameter is an array of strings that contain the arguments
|
||||
// that came after `-e`, which will be interpreted as a command
|
||||
// to run.
|
||||
const as_variant_type = glib.VariantType.new("as");
|
||||
defer as_variant_type.free();
|
||||
|
||||
const s_variant_type = glib.VariantType.new("s");
|
||||
defer s_variant_type.free();
|
||||
|
||||
var command: glib.VariantBuilder = undefined;
|
||||
command.init(as_variant_type);
|
||||
errdefer command.clear();
|
||||
|
||||
for (arguments) |argument| {
|
||||
const bytes = glib.Bytes.new(argument.ptr, argument.len + 1);
|
||||
defer bytes.unref();
|
||||
const string = glib.Variant.newFromBytes(s_variant_type, bytes, @intFromBool(true));
|
||||
command.addValue(string);
|
||||
}
|
||||
|
||||
dbus.addParameter(command.end());
|
||||
}
|
||||
|
||||
try dbus.send();
|
||||
|
||||
return true;
|
||||
}
|
@@ -1,411 +0,0 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
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,
|
||||
) error{NoSpaceLeft}!?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("<Shift>");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("<Ctrl>");
|
||||
if (trigger.mods.alt) try writer.writeAll("<Alt>");
|
||||
if (trigger.mods.super) try writer.writeAll("<Super>");
|
||||
|
||||
// Write our key
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
/// 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,
|
||||
) error{NoSpaceLeft}!?[:0]const u8 {
|
||||
var buf_stream = std.io.fixedBufferStream(buf);
|
||||
const writer = buf_stream.writer();
|
||||
|
||||
// Modifiers
|
||||
if (trigger.mods.shift) try writer.writeAll("SHIFT+");
|
||||
if (trigger.mods.ctrl) try writer.writeAll("CTRL+");
|
||||
if (trigger.mods.alt) try writer.writeAll("ALT+");
|
||||
if (trigger.mods.super) try writer.writeAll("LOGO+");
|
||||
|
||||
// Write our key
|
||||
// NOTE: While the spec specifies that only libxkbcommon keysyms are
|
||||
// expected, using GTK's keysyms should still work as they are identical
|
||||
// to *X11's* keysyms (which I assume is a subset of libxkbcommon's).
|
||||
// I haven't been able to any evidence to back up that assumption but
|
||||
// this works for now
|
||||
if (!try writeTriggerKey(writer, trigger)) return null;
|
||||
|
||||
// We need to make the string null terminated.
|
||||
try writer.writeByte(0);
|
||||
const slice = buf_stream.getWritten();
|
||||
return slice[0 .. slice.len - 1 :0];
|
||||
}
|
||||
|
||||
fn writeTriggerKey(writer: anytype, trigger: input.Binding.Trigger) error{NoSpaceLeft}!bool {
|
||||
switch (trigger.key) {
|
||||
.physical => |k| {
|
||||
const keyval = keyvalFromKey(k) orelse return false;
|
||||
try writer.writeAll(std.mem.span(gdk.keyvalName(keyval) orelse return false));
|
||||
},
|
||||
|
||||
.unicode => |cp| {
|
||||
if (gdk.keyvalName(cp)) |name| {
|
||||
try writer.writeAll(std.mem.span(name));
|
||||
} else {
|
||||
try writer.print("{u}", .{cp});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn translateMods(state: gdk.ModifierType) input.Mods {
|
||||
return .{
|
||||
.shift = state.shift_mask,
|
||||
.ctrl = state.control_mask,
|
||||
.alt = state.alt_mask,
|
||||
.super = state.super_mask,
|
||||
// Lock is dependent on the X settings but we just assume caps lock.
|
||||
.caps_lock = state.lock_mask,
|
||||
};
|
||||
}
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
pub fn keyvalUnicodeUnshifted(
|
||||
widget: *gtk.Widget,
|
||||
event: *gdk.KeyEvent,
|
||||
keycode: u32,
|
||||
) u21 {
|
||||
const display = widget.getDisplay();
|
||||
|
||||
// We need to get the currently active keyboard layout so we know
|
||||
// what group to look at.
|
||||
const layout = event.getLayout();
|
||||
|
||||
// Get all the possible keyboard mappings for this keycode. A keycode is the
|
||||
// physical key pressed.
|
||||
var keys: [*]gdk.KeymapKey = undefined;
|
||||
var keyvals: [*]c_uint = undefined;
|
||||
var n_entries: c_int = 0;
|
||||
if (display.mapKeycode(keycode, &keys, &keyvals, &n_entries) == 0) return 0;
|
||||
|
||||
defer glib.free(keys);
|
||||
defer glib.free(keyvals);
|
||||
|
||||
// debugging:
|
||||
// std.log.debug("layout={}", .{layout});
|
||||
// for (0..@intCast(n_entries)) |i| {
|
||||
// std.log.debug("keymap key={} codepoint={x}", .{
|
||||
// keys[i],
|
||||
// gdk.keyvalToUnicode(keyvals[i]),
|
||||
// });
|
||||
// }
|
||||
|
||||
for (0..@intCast(n_entries)) |i| {
|
||||
if (keys[i].f_group == layout and
|
||||
keys[i].f_level == 0)
|
||||
{
|
||||
return std.math.cast(
|
||||
u21,
|
||||
gdk.keyvalToUnicode(keyvals[i]),
|
||||
) orelse 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Returns the mods to use a key event from a GTK event.
|
||||
/// This requires a lot of context because the GdkEvent
|
||||
/// doesn't contain enough on its own.
|
||||
pub fn eventMods(
|
||||
event: *gdk.Event,
|
||||
physical_key: input.Key,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
action: input.Action,
|
||||
app_winproto: *winproto.App,
|
||||
) input.Mods {
|
||||
const device = event.getDevice();
|
||||
|
||||
var mods = app_winproto.eventMods(device, gtk_mods);
|
||||
mods.num_lock = if (device) |d| d.getNumLockState() != 0 else false;
|
||||
|
||||
// We use the physical key to determine sided modifiers. As
|
||||
// far as I can tell there's no other way to reliably determine
|
||||
// this.
|
||||
//
|
||||
// We also set the main modifier to true if either side is true,
|
||||
// since on both X11/Wayland, GTK doesn't set the main modifier
|
||||
// if only the modifier key is pressed, but our core logic
|
||||
// relies on it.
|
||||
switch (physical_key) {
|
||||
.shift_left => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .left;
|
||||
},
|
||||
|
||||
.shift_right => {
|
||||
mods.shift = action != .release;
|
||||
mods.sides.shift = .right;
|
||||
},
|
||||
|
||||
.control_left => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .left;
|
||||
},
|
||||
|
||||
.control_right => {
|
||||
mods.ctrl = action != .release;
|
||||
mods.sides.ctrl = .right;
|
||||
},
|
||||
|
||||
.alt_left => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .left;
|
||||
},
|
||||
|
||||
.alt_right => {
|
||||
mods.alt = action != .release;
|
||||
mods.sides.alt = .right;
|
||||
},
|
||||
|
||||
.meta_left => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .left;
|
||||
},
|
||||
|
||||
.meta_right => {
|
||||
mods.super = action != .release;
|
||||
mods.sides.super = .right;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
/// Returns an input key from a keyval or null if we don't have a mapping.
|
||||
pub fn keyFromKeyval(keyval: c_uint) ?input.Key {
|
||||
for (keymap) |entry| {
|
||||
if (entry[0] == keyval) return entry[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Returns a keyval from an input key or null if we don't have a mapping.
|
||||
pub fn keyvalFromKey(key: input.Key) ?c_uint {
|
||||
switch (key) {
|
||||
inline else => |key_comptime| {
|
||||
return comptime value: {
|
||||
@setEvalBranchQuota(50_000);
|
||||
for (keymap) |entry| {
|
||||
if (entry[1] == key_comptime) break :value entry[0];
|
||||
}
|
||||
|
||||
break :value null;
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test "accelFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("<Super>q", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("<Shift><Ctrl><Alt><Super>backslash", (try accelFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
test "xdgShortcutFromTrigger" {
|
||||
const testing = std.testing;
|
||||
var buf: [256]u8 = undefined;
|
||||
|
||||
try testing.expectEqualStrings("LOGO+q", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .super = true },
|
||||
.key = .{ .unicode = 'q' },
|
||||
})).?);
|
||||
|
||||
try testing.expectEqualStrings("SHIFT+CTRL+ALT+LOGO+backslash", (try xdgShortcutFromTrigger(&buf, .{
|
||||
.mods = .{ .ctrl = true, .alt = true, .super = true, .shift = true },
|
||||
.key = .{ .unicode = 92 },
|
||||
})).?);
|
||||
}
|
||||
|
||||
/// A raw entry in the keymap. Our keymap contains mappings between
|
||||
/// GDK keys and our own key enum.
|
||||
const RawEntry = struct { c_uint, input.Key };
|
||||
|
||||
const keymap: []const RawEntry = &.{
|
||||
.{ gdk.KEY_a, .key_a },
|
||||
.{ gdk.KEY_b, .key_b },
|
||||
.{ gdk.KEY_c, .key_c },
|
||||
.{ gdk.KEY_d, .key_d },
|
||||
.{ gdk.KEY_e, .key_e },
|
||||
.{ gdk.KEY_f, .key_f },
|
||||
.{ gdk.KEY_g, .key_g },
|
||||
.{ gdk.KEY_h, .key_h },
|
||||
.{ gdk.KEY_i, .key_i },
|
||||
.{ gdk.KEY_j, .key_j },
|
||||
.{ gdk.KEY_k, .key_k },
|
||||
.{ gdk.KEY_l, .key_l },
|
||||
.{ gdk.KEY_m, .key_m },
|
||||
.{ gdk.KEY_n, .key_n },
|
||||
.{ gdk.KEY_o, .key_o },
|
||||
.{ gdk.KEY_p, .key_p },
|
||||
.{ gdk.KEY_q, .key_q },
|
||||
.{ gdk.KEY_r, .key_r },
|
||||
.{ gdk.KEY_s, .key_s },
|
||||
.{ gdk.KEY_t, .key_t },
|
||||
.{ gdk.KEY_u, .key_u },
|
||||
.{ gdk.KEY_v, .key_v },
|
||||
.{ gdk.KEY_w, .key_w },
|
||||
.{ gdk.KEY_x, .key_x },
|
||||
.{ gdk.KEY_y, .key_y },
|
||||
.{ gdk.KEY_z, .key_z },
|
||||
|
||||
.{ gdk.KEY_0, .digit_0 },
|
||||
.{ gdk.KEY_1, .digit_1 },
|
||||
.{ gdk.KEY_2, .digit_2 },
|
||||
.{ gdk.KEY_3, .digit_3 },
|
||||
.{ gdk.KEY_4, .digit_4 },
|
||||
.{ gdk.KEY_5, .digit_5 },
|
||||
.{ gdk.KEY_6, .digit_6 },
|
||||
.{ gdk.KEY_7, .digit_7 },
|
||||
.{ gdk.KEY_8, .digit_8 },
|
||||
.{ gdk.KEY_9, .digit_9 },
|
||||
|
||||
.{ gdk.KEY_semicolon, .semicolon },
|
||||
.{ gdk.KEY_space, .space },
|
||||
.{ gdk.KEY_apostrophe, .quote },
|
||||
.{ gdk.KEY_comma, .comma },
|
||||
.{ gdk.KEY_grave, .backquote },
|
||||
.{ gdk.KEY_period, .period },
|
||||
.{ gdk.KEY_slash, .slash },
|
||||
.{ gdk.KEY_minus, .minus },
|
||||
.{ gdk.KEY_equal, .equal },
|
||||
.{ gdk.KEY_bracketleft, .bracket_left },
|
||||
.{ gdk.KEY_bracketright, .bracket_right },
|
||||
.{ gdk.KEY_backslash, .backslash },
|
||||
|
||||
.{ gdk.KEY_Up, .arrow_up },
|
||||
.{ gdk.KEY_Down, .arrow_down },
|
||||
.{ gdk.KEY_Right, .arrow_right },
|
||||
.{ gdk.KEY_Left, .arrow_left },
|
||||
.{ gdk.KEY_Home, .home },
|
||||
.{ gdk.KEY_End, .end },
|
||||
.{ gdk.KEY_Insert, .insert },
|
||||
.{ gdk.KEY_Delete, .delete },
|
||||
.{ gdk.KEY_Caps_Lock, .caps_lock },
|
||||
.{ gdk.KEY_Scroll_Lock, .scroll_lock },
|
||||
.{ gdk.KEY_Num_Lock, .num_lock },
|
||||
.{ gdk.KEY_Page_Up, .page_up },
|
||||
.{ gdk.KEY_Page_Down, .page_down },
|
||||
.{ gdk.KEY_Escape, .escape },
|
||||
.{ gdk.KEY_Return, .enter },
|
||||
.{ gdk.KEY_Tab, .tab },
|
||||
.{ gdk.KEY_BackSpace, .backspace },
|
||||
.{ gdk.KEY_Print, .print_screen },
|
||||
.{ gdk.KEY_Pause, .pause },
|
||||
|
||||
.{ gdk.KEY_F1, .f1 },
|
||||
.{ gdk.KEY_F2, .f2 },
|
||||
.{ gdk.KEY_F3, .f3 },
|
||||
.{ gdk.KEY_F4, .f4 },
|
||||
.{ gdk.KEY_F5, .f5 },
|
||||
.{ gdk.KEY_F6, .f6 },
|
||||
.{ gdk.KEY_F7, .f7 },
|
||||
.{ gdk.KEY_F8, .f8 },
|
||||
.{ gdk.KEY_F9, .f9 },
|
||||
.{ gdk.KEY_F10, .f10 },
|
||||
.{ gdk.KEY_F11, .f11 },
|
||||
.{ gdk.KEY_F12, .f12 },
|
||||
.{ gdk.KEY_F13, .f13 },
|
||||
.{ gdk.KEY_F14, .f14 },
|
||||
.{ gdk.KEY_F15, .f15 },
|
||||
.{ gdk.KEY_F16, .f16 },
|
||||
.{ gdk.KEY_F17, .f17 },
|
||||
.{ gdk.KEY_F18, .f18 },
|
||||
.{ gdk.KEY_F19, .f19 },
|
||||
.{ gdk.KEY_F20, .f20 },
|
||||
.{ gdk.KEY_F21, .f21 },
|
||||
.{ gdk.KEY_F22, .f22 },
|
||||
.{ gdk.KEY_F23, .f23 },
|
||||
.{ gdk.KEY_F24, .f24 },
|
||||
.{ gdk.KEY_F25, .f25 },
|
||||
|
||||
.{ gdk.KEY_KP_0, .numpad_0 },
|
||||
.{ gdk.KEY_KP_1, .numpad_1 },
|
||||
.{ gdk.KEY_KP_2, .numpad_2 },
|
||||
.{ gdk.KEY_KP_3, .numpad_3 },
|
||||
.{ gdk.KEY_KP_4, .numpad_4 },
|
||||
.{ gdk.KEY_KP_5, .numpad_5 },
|
||||
.{ gdk.KEY_KP_6, .numpad_6 },
|
||||
.{ gdk.KEY_KP_7, .numpad_7 },
|
||||
.{ gdk.KEY_KP_8, .numpad_8 },
|
||||
.{ gdk.KEY_KP_9, .numpad_9 },
|
||||
.{ gdk.KEY_KP_Decimal, .numpad_decimal },
|
||||
.{ gdk.KEY_KP_Divide, .numpad_divide },
|
||||
.{ gdk.KEY_KP_Multiply, .numpad_multiply },
|
||||
.{ gdk.KEY_KP_Subtract, .numpad_subtract },
|
||||
.{ gdk.KEY_KP_Add, .numpad_add },
|
||||
.{ gdk.KEY_KP_Enter, .numpad_enter },
|
||||
.{ gdk.KEY_KP_Equal, .numpad_equal },
|
||||
|
||||
.{ gdk.KEY_KP_Separator, .numpad_separator },
|
||||
.{ gdk.KEY_KP_Left, .numpad_left },
|
||||
.{ gdk.KEY_KP_Right, .numpad_right },
|
||||
.{ gdk.KEY_KP_Up, .numpad_up },
|
||||
.{ gdk.KEY_KP_Down, .numpad_down },
|
||||
.{ gdk.KEY_KP_Page_Up, .numpad_page_up },
|
||||
.{ gdk.KEY_KP_Page_Down, .numpad_page_down },
|
||||
.{ gdk.KEY_KP_Home, .numpad_home },
|
||||
.{ gdk.KEY_KP_End, .numpad_end },
|
||||
.{ gdk.KEY_KP_Insert, .numpad_insert },
|
||||
.{ gdk.KEY_KP_Delete, .numpad_delete },
|
||||
.{ gdk.KEY_KP_Begin, .numpad_begin },
|
||||
|
||||
.{ gdk.KEY_Copy, .copy },
|
||||
.{ gdk.KEY_Cut, .cut },
|
||||
.{ gdk.KEY_Paste, .paste },
|
||||
|
||||
.{ gdk.KEY_Shift_L, .shift_left },
|
||||
.{ gdk.KEY_Control_L, .control_left },
|
||||
.{ gdk.KEY_Alt_L, .alt_left },
|
||||
.{ gdk.KEY_Super_L, .meta_left },
|
||||
.{ gdk.KEY_Shift_R, .shift_right },
|
||||
.{ gdk.KEY_Control_R, .control_right },
|
||||
.{ gdk.KEY_Alt_R, .alt_right },
|
||||
.{ gdk.KEY_Super_R, .meta_right },
|
||||
|
||||
// TODO: media keys
|
||||
};
|
@@ -1,28 +0,0 @@
|
||||
using Gtk 4.0;
|
||||
// This is unused but if we remove it we get a blueprint-compiler error.
|
||||
using Adw 1;
|
||||
|
||||
template $GhosttyConfigErrorsDialog: $GhosttyDialog {
|
||||
heading: _("Configuration Errors");
|
||||
body: _("One or more configuration errors were found. Please review the errors below, and either reload your configuration or ignore these errors.");
|
||||
|
||||
responses [
|
||||
ignore: _("Ignore"),
|
||||
reload: _("Reload Configuration") suggested,
|
||||
]
|
||||
|
||||
extra-child: ScrolledWindow {
|
||||
min-content-width: 500;
|
||||
min-content-height: 100;
|
||||
|
||||
TextView {
|
||||
editable: false;
|
||||
cursor-visible: false;
|
||||
top-margin: 8;
|
||||
bottom-margin: 8;
|
||||
left-margin: 8;
|
||||
right-margin: 8;
|
||||
buffer: bind (template.config as <$GhosttyConfig>).diagnostics-buffer;
|
||||
}
|
||||
};
|
||||
}
|
@@ -1,110 +0,0 @@
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,155 +0,0 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
|
||||
const Config = @import("../../config.zig").Config;
|
||||
const input = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
const ApprtWindow = @import("class/window.zig").Window;
|
||||
|
||||
pub const noop = @import("winproto/noop.zig");
|
||||
pub const x11 = @import("winproto/x11.zig");
|
||||
pub const wayland = @import("winproto/wayland.zig");
|
||||
|
||||
pub const Protocol = enum {
|
||||
none,
|
||||
wayland,
|
||||
x11,
|
||||
};
|
||||
|
||||
/// App-state for the underlying windowing protocol. There should be one
|
||||
/// instance of this struct per application.
|
||||
pub const App = union(Protocol) {
|
||||
none: noop.App,
|
||||
wayland: if (build_options.wayland) wayland.App else noop.App,
|
||||
x11: if (build_options.x11) x11.App else noop.App,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !App {
|
||||
inline for (@typeInfo(App).@"union".fields) |field| {
|
||||
if (try field.type.init(
|
||||
alloc,
|
||||
gdk_display,
|
||||
app_id,
|
||||
config,
|
||||
)) |v| {
|
||||
return @unionInit(App, field.name, v);
|
||||
}
|
||||
}
|
||||
|
||||
return .{ .none = .{} };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
self: *App,
|
||||
device: ?*gdk.Device,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) input.Mods {
|
||||
return switch (self.*) {
|
||||
inline else => |*v| v.eventMods(device, gtk_mods),
|
||||
} orelse key.translateMods(gtk_mods);
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.supportsQuickTerminal(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Set up necessary support for the quick terminal that must occur
|
||||
/// *before* the window-level winproto object is created.
|
||||
///
|
||||
/// Only has an effect on the Wayland backend, where the gtk4-layer-shell
|
||||
/// library is initialized.
|
||||
pub fn initQuickTerminal(self: *App, apprt_window: *ApprtWindow) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.initQuickTerminal(apprt_window),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-Window state for the underlying windowing protocol.
|
||||
///
|
||||
/// In Wayland, the terminology used is "Surface" and for it, this is
|
||||
/// really "Surface"-specific state. But Ghostty uses the term "Surface"
|
||||
/// heavily to mean something completely different, so we use "Window" here
|
||||
/// to better match what it generally maps to in the Ghostty codebase.
|
||||
pub const Window = union(Protocol) {
|
||||
none: noop.Window,
|
||||
wayland: if (build_options.wayland) wayland.Window else noop.Window,
|
||||
x11: if (build_options.x11) x11.Window else noop.Window,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
return switch (app.*) {
|
||||
inline else => |*v, tag| {
|
||||
inline for (@typeInfo(Window).@"union".fields) |field| {
|
||||
if (comptime std.mem.eql(
|
||||
u8,
|
||||
field.name,
|
||||
@tagName(tag),
|
||||
)) return @unionInit(
|
||||
Window,
|
||||
field.name,
|
||||
try field.type.init(
|
||||
alloc,
|
||||
v,
|
||||
apprt_window,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Window, alloc: Allocator) void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| v.deinit(alloc),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.resizeEvent(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.syncAppearance(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self) {
|
||||
inline else => |v| v.clientSideDecorationEnabled(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.addSubprocessEnv(env),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.setUrgent(urgent),
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,75 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
|
||||
const log = std.log.scoped(.winproto_noop);
|
||||
|
||||
pub const App = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *gdk.Display,
|
||||
_: [:0]const u8,
|
||||
_: *const Config,
|
||||
) !?App {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*gdk.Device,
|
||||
_: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
return false;
|
||||
}
|
||||
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *App,
|
||||
_: *ApprtWindow,
|
||||
) !Window {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
_: *Window,
|
||||
_: *const ApprtWindow.DerivedConfig,
|
||||
) !void {}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(_: *Window) !void {}
|
||||
|
||||
/// This returns true if CSD is enabled for this window. This
|
||||
/// should be the actual present state of the window, not the
|
||||
/// desired state.
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
_ = self;
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(_: *Window, _: *std.process.EnvMap) !void {}
|
||||
|
||||
pub fn setUrgent(_: *Window, _: bool) !void {}
|
||||
};
|
@@ -1,518 +0,0 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gdk_wayland = @import("gdk_wayland");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const layer_shell = @import("gtk4-layer-shell");
|
||||
const wayland = @import("wayland");
|
||||
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
const xdg = wayland.client.xdg;
|
||||
|
||||
const log = std.log.scoped(.winproto_wayland);
|
||||
|
||||
/// Wayland state that contains application-wide Wayland objects (e.g. wl_display).
|
||||
pub const App = struct {
|
||||
display: *wl.Display,
|
||||
context: *Context,
|
||||
|
||||
const Context = struct {
|
||||
kde_blur_manager: ?*org.KdeKwinBlurManager = null,
|
||||
|
||||
// FIXME: replace with `zxdg_decoration_v1` once GTK merges
|
||||
// https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/6398
|
||||
kde_decoration_manager: ?*org.KdeKwinServerDecorationManager = null,
|
||||
|
||||
kde_slide_manager: ?*org.KdeKwinSlideManager = null,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
|
||||
xdg_activation: ?*xdg.ActivationV1 = null,
|
||||
|
||||
/// Whether the xdg_wm_dialog_v1 protocol is present.
|
||||
///
|
||||
/// If it is present, gtk4-layer-shell < 1.0.4 may crash when the user
|
||||
/// creates a quick terminal, and we need to ensure this fails
|
||||
/// gracefully if this situation occurs.
|
||||
///
|
||||
/// FIXME: This is a temporary workaround - we should remove this when
|
||||
/// all of our supported distros drop support for affected old
|
||||
/// gtk4-layer-shell versions.
|
||||
///
|
||||
/// See https://github.com/wmww/gtk4-layer-shell/issues/50
|
||||
xdg_wm_dialog_present: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = config;
|
||||
_ = app_id;
|
||||
|
||||
const gdk_wayland_display = gobject.ext.cast(
|
||||
gdk_wayland.WaylandDisplay,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const display: *wl.Display = @ptrCast(@alignCast(
|
||||
gdk_wayland_display.getWlDisplay() orelse return error.NoWaylandDisplay,
|
||||
));
|
||||
|
||||
// Create our context for our callbacks so we have a stable pointer.
|
||||
// Note: at the time of writing this comment, we don't really need
|
||||
// a stable pointer, but it's too scary that we'd need one in the future
|
||||
// and not have it and corrupt memory or something so let's just do it.
|
||||
const context = try alloc.create(Context);
|
||||
errdefer alloc.destroy(context);
|
||||
context.* = .{};
|
||||
|
||||
// Get our display registry so we can get all the available interfaces
|
||||
// and bind to what we need.
|
||||
const registry = try display.getRegistry();
|
||||
registry.setListener(*Context, registryListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
|
||||
// Do another round-trip to get the default decoration mode
|
||||
if (context.kde_decoration_manager) |deco_manager| {
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
if (display.roundtrip() != .SUCCESS) return error.RoundtripFailed;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = display,
|
||||
.context = context,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
alloc.destroy(self.context);
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*gdk.Device,
|
||||
_: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(self: App) bool {
|
||||
if (!layer_shell.isSupported()) {
|
||||
log.warn("your compositor does not support the wlr-layer-shell protocol; disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self.context.xdg_wm_dialog_present and layer_shell.getLibraryVersion().order(.{
|
||||
.major = 1,
|
||||
.minor = 0,
|
||||
.patch = 4,
|
||||
}) == .lt) {
|
||||
log.warn("the version of gtk4-layer-shell installed on your system is too old (must be 1.0.4 or newer); disabling quick terminal", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, apprt_window: *ApprtWindow) !void {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
|
||||
layer_shell.initForWindow(window);
|
||||
layer_shell.setLayer(window, .top);
|
||||
layer_shell.setNamespace(window, "ghostty-quick-terminal");
|
||||
}
|
||||
|
||||
fn getInterfaceType(comptime field: std.builtin.Type.StructField) ?type {
|
||||
// Globals should be optional pointers
|
||||
const T = switch (@typeInfo(field.type)) {
|
||||
.optional => |o| switch (@typeInfo(o.child)) {
|
||||
.pointer => |v| v.child,
|
||||
else => return null,
|
||||
},
|
||||
else => return null,
|
||||
};
|
||||
|
||||
// Only process Wayland interfaces
|
||||
if (!@hasDecl(T, "interface")) return null;
|
||||
return T;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
const ctx_fields = @typeInfo(Context).@"struct".fields;
|
||||
|
||||
switch (event) {
|
||||
.global => |v| {
|
||||
log.debug("found global {s}", .{v.interface});
|
||||
|
||||
// We don't actually do anything with this other than checking
|
||||
// for its existence, so we process this separately.
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
"xdg_wm_dialog_v1",
|
||||
) == .eq) {
|
||||
context.xdg_wm_dialog_present = true;
|
||||
return;
|
||||
}
|
||||
|
||||
inline for (ctx_fields) |field| {
|
||||
const T = getInterfaceType(field) orelse continue;
|
||||
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
v.interface,
|
||||
T.interface.name,
|
||||
) == .eq) {
|
||||
log.debug("matched {}", .{T});
|
||||
|
||||
@field(context, field.name) = registry.bind(
|
||||
v.name,
|
||||
T,
|
||||
T.generated_version,
|
||||
) catch |err| {
|
||||
log.warn(
|
||||
"error binding interface {s} error={}",
|
||||
.{ v.interface, err },
|
||||
);
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// This should be a rare occurrence, but in case a global
|
||||
// is suddenly no longer available, we destroy and unset it
|
||||
// as the protocol mandates.
|
||||
.global_remove => |v| remove: {
|
||||
inline for (ctx_fields) |field| {
|
||||
if (getInterfaceType(field) == null) continue;
|
||||
const global = @field(context, field.name) orelse break :remove;
|
||||
if (global.getId() == v.name) {
|
||||
global.destroy();
|
||||
@field(context, field.name) = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn decoManagerListener(
|
||||
_: *org.KdeKwinServerDecorationManager,
|
||||
event: org.KdeKwinServerDecorationManager.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
.default_mode => |mode| {
|
||||
context.default_deco_mode = @enumFromInt(mode.mode);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Per-window (wl_surface) state for the Wayland protocol.
|
||||
pub const Window = struct {
|
||||
apprt_window: *ApprtWindow,
|
||||
|
||||
/// The Wayland surface for this window.
|
||||
surface: *wl.Surface,
|
||||
|
||||
/// The context from the app where we can load our Wayland interfaces.
|
||||
app_context: *App.Context,
|
||||
|
||||
/// A token that, when present, indicates that the window is blurred.
|
||||
blur_token: ?*org.KdeKwinBlur = null,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
decoration: ?*org.KdeKwinServerDecoration = null,
|
||||
|
||||
/// Object that controls the slide-in/slide-out animations of the
|
||||
/// quick terminal. Always null for windows other than the quick terminal.
|
||||
slide: ?*org.KdeKwinSlide = null,
|
||||
|
||||
/// Object that, when present, denotes that the window is currently
|
||||
/// requesting attention from the user.
|
||||
activation_token: ?*xdg.ActivationTokenV1 = null,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const gtk_native = apprt_window.as(gtk.Native);
|
||||
const gdk_surface = gtk_native.getSurface() orelse return error.NotWaylandSurface;
|
||||
|
||||
// This should never fail, because if we're being called at this point
|
||||
// then we've already asserted that our app state is Wayland.
|
||||
const gdk_wl_surface = gobject.ext.cast(
|
||||
gdk_wayland.WaylandSurface,
|
||||
gdk_surface,
|
||||
) orelse return error.NoWaylandSurface;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(@alignCast(
|
||||
gdk_wl_surface.getWlSurface() orelse return error.NoWaylandSurface,
|
||||
));
|
||||
|
||||
// Get our decoration object so we can control the
|
||||
// CSD vs SSD status of this surface.
|
||||
const deco: ?*org.KdeKwinServerDecoration = deco: {
|
||||
const mgr = app.context.kde_decoration_manager orelse
|
||||
break :deco null;
|
||||
|
||||
const deco: *org.KdeKwinServerDecoration = mgr.create(
|
||||
wl_surface,
|
||||
) catch |err| {
|
||||
log.warn("could not create decoration object={}", .{err});
|
||||
break :deco null;
|
||||
};
|
||||
|
||||
break :deco deco;
|
||||
};
|
||||
|
||||
if (apprt_window.isQuickTerminal()) {
|
||||
_ = gdk.Surface.signals.enter_monitor.connect(
|
||||
gdk_surface,
|
||||
*ApprtWindow,
|
||||
enteredMonitor,
|
||||
apprt_window,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
return .{
|
||||
.apprt_window = apprt_window,
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.decoration = deco,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
if (self.decoration) |deco| deco.release();
|
||||
if (self.slide) |slide| slide.release();
|
||||
}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
self.syncDecoration() catch |err| {
|
||||
log.err("failed to sync blur={}", .{err});
|
||||
};
|
||||
|
||||
if (self.apprt_window.isQuickTerminal()) {
|
||||
self.syncQuickTerminal() catch |err| {
|
||||
log.warn("failed to sync quick terminal appearance={}", .{err});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self.getDecorationMode()) {
|
||||
.Client => true,
|
||||
// If we support SSDs, then we should *not* enable CSDs if we prefer SSDs.
|
||||
// However, if we do not support SSDs (e.g. GNOME) then we should enable
|
||||
// CSDs even if the user prefers SSDs.
|
||||
.Server => if (self.app_context.kde_decoration_manager) |_| false else true,
|
||||
.None => false,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
_ = self;
|
||||
_ = env;
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
|
||||
// If there already is a token, destroy and unset it
|
||||
if (self.activation_token) |token| token.destroy();
|
||||
|
||||
self.activation_token = if (urgent) token: {
|
||||
const token = try activation.getActivationToken();
|
||||
token.setSurface(self.surface);
|
||||
token.setListener(*Window, onActivationTokenEvent, self);
|
||||
token.commit();
|
||||
break :token token;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn syncBlur(self: *Window) !void {
|
||||
const manager = self.app_context.kde_blur_manager orelse return;
|
||||
const config = if (self.apprt_window.getConfig()) |v|
|
||||
v.get()
|
||||
else
|
||||
return;
|
||||
const blur = config.@"background-blur";
|
||||
|
||||
if (self.blur_token) |tok| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blur.enabled()) {
|
||||
manager.unset(self.surface);
|
||||
tok.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blur.enabled()) {
|
||||
const tok = try manager.create(self.surface);
|
||||
tok.commit();
|
||||
self.blur_token = tok;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecoration(self: *Window) !void {
|
||||
const deco = self.decoration orelse return;
|
||||
|
||||
// The protocol requests uint instead of enum so we have
|
||||
// to convert it.
|
||||
deco.requestMode(@intCast(@intFromEnum(self.getDecorationMode())));
|
||||
}
|
||||
|
||||
fn getDecorationMode(self: Window) org.KdeKwinServerDecorationManager.Mode {
|
||||
return switch (self.apprt_window.getWindowDecoration()) {
|
||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
||||
.client => .Client,
|
||||
.server => .Server,
|
||||
.none => .None,
|
||||
};
|
||||
}
|
||||
|
||||
fn syncQuickTerminal(self: *Window) !void {
|
||||
const window = self.apprt_window.as(gtk.Window);
|
||||
const config = if (self.apprt_window.getConfig()) |v|
|
||||
v.get()
|
||||
else
|
||||
return;
|
||||
|
||||
layer_shell.setKeyboardMode(
|
||||
window,
|
||||
switch (config.@"quick-terminal-keyboard-interactivity") {
|
||||
.none => .none,
|
||||
.@"on-demand" => on_demand: {
|
||||
if (layer_shell.getProtocolVersion() < 4) {
|
||||
log.warn("your compositor does not support on-demand keyboard access; falling back to exclusive access", .{});
|
||||
break :on_demand .exclusive;
|
||||
}
|
||||
break :on_demand .on_demand;
|
||||
},
|
||||
.exclusive => .exclusive,
|
||||
},
|
||||
);
|
||||
|
||||
const anchored_edge: ?layer_shell.ShellEdge = switch (config.@"quick-terminal-position") {
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.center => null,
|
||||
};
|
||||
|
||||
for (std.meta.tags(layer_shell.ShellEdge)) |edge| {
|
||||
if (anchored_edge) |anchored| {
|
||||
if (edge == anchored) {
|
||||
layer_shell.setMargin(window, edge, 0);
|
||||
layer_shell.setAnchor(window, edge, true);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Arbitrary margin - could be made customizable?
|
||||
layer_shell.setMargin(window, edge, 20);
|
||||
layer_shell.setAnchor(window, edge, false);
|
||||
}
|
||||
|
||||
if (self.slide) |slide| slide.release();
|
||||
|
||||
self.slide = if (anchored_edge) |anchored| slide: {
|
||||
const mgr = self.app_context.kde_slide_manager orelse break :slide null;
|
||||
|
||||
const slide = mgr.create(self.surface) catch |err| {
|
||||
log.warn("could not create slide object={}", .{err});
|
||||
break :slide null;
|
||||
};
|
||||
|
||||
const slide_location: org.KdeKwinSlide.Location = switch (anchored) {
|
||||
.top => .top,
|
||||
.bottom => .bottom,
|
||||
.left => .left,
|
||||
.right => .right,
|
||||
};
|
||||
|
||||
slide.setLocation(@intCast(@intFromEnum(slide_location)));
|
||||
slide.commit();
|
||||
break :slide slide;
|
||||
} else null;
|
||||
}
|
||||
|
||||
/// Update the size of the quick terminal based on monitor dimensions.
|
||||
fn enteredMonitor(
|
||||
_: *gdk.Surface,
|
||||
monitor: *gdk.Monitor,
|
||||
apprt_window: *ApprtWindow,
|
||||
) callconv(.c) void {
|
||||
const window = apprt_window.as(gtk.Window);
|
||||
const config = if (apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
var monitor_size: gdk.Rectangle = undefined;
|
||||
monitor.getGeometry(&monitor_size);
|
||||
|
||||
const dims = config.@"quick-terminal-size".calculate(
|
||||
config.@"quick-terminal-position",
|
||||
.{
|
||||
.width = @intCast(monitor_size.f_width),
|
||||
.height = @intCast(monitor_size.f_height),
|
||||
},
|
||||
);
|
||||
|
||||
window.setDefaultSize(@intCast(dims.width), @intCast(dims.height));
|
||||
}
|
||||
|
||||
fn onActivationTokenEvent(
|
||||
token: *xdg.ActivationTokenV1,
|
||||
event: xdg.ActivationTokenV1.Event,
|
||||
self: *Window,
|
||||
) void {
|
||||
const activation = self.app_context.xdg_activation orelse return;
|
||||
const current_token = self.activation_token orelse return;
|
||||
|
||||
if (token.getId() != current_token.getId()) {
|
||||
log.warn("received event for unknown activation token; ignoring", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event) {
|
||||
.done => |done| {
|
||||
activation.activate(done.token, self.surface);
|
||||
token.destroy();
|
||||
self.activation_token = null;
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,505 +0,0 @@
|
||||
//! X11 window protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gdk = @import("gdk");
|
||||
const gdk_x11 = @import("gdk_x11");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
const xlib = @import("xlib");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("X11/Xlib.h");
|
||||
@cInclude("X11/Xatom.h");
|
||||
@cInclude("X11/XKBlib.h");
|
||||
});
|
||||
|
||||
const input = @import("../../../input.zig");
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const ApprtWindow = @import("../class/window.zig").Window;
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
pub const App = struct {
|
||||
display: *xlib.Display,
|
||||
base_event_code: c_int,
|
||||
atoms: Atoms,
|
||||
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
gdk_display: *gdk.Display,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
const gdk_x11_display = gobject.ext.cast(
|
||||
gdk_x11.X11Display,
|
||||
gdk_display,
|
||||
) orelse return null;
|
||||
|
||||
const xlib_display = gdk_x11_display.getXdisplay();
|
||||
|
||||
const x11_program_name: [:0]const u8 = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// display.
|
||||
//
|
||||
// Note that we also set the program name here using g_set_prgname.
|
||||
// This is how the instance name field for WM_CLASS is derived when
|
||||
// calling gdk_x11_display_set_program_class; there does not seem to be
|
||||
// a way to set it directly. It does not look like this is being set by
|
||||
// our other app initialization routines currently, but since we're
|
||||
// currently deriving its value from x11-instance-name effectively, I
|
||||
// feel like gating it behind an X11 check is better intent.
|
||||
//
|
||||
// This makes the property show up like so when using xprop:
|
||||
//
|
||||
// WM_CLASS(STRING) = "ghostty", "com.mitchellh.ghostty"
|
||||
//
|
||||
// Append "-debug" on both when using the debug build.
|
||||
glib.setPrgname(x11_program_name);
|
||||
gdk_x11.X11Display.setProgramClass(gdk_display, app_id);
|
||||
|
||||
// XKB
|
||||
log.debug("Xkb.init: initializing Xkb", .{});
|
||||
log.debug("Xkb.init: running XkbQueryExtension", .{});
|
||||
var opcode: c_int = 0;
|
||||
var base_event_code: c_int = 0;
|
||||
var base_error_code: c_int = 0;
|
||||
var major = c.XkbMajorVersion;
|
||||
var minor = c.XkbMinorVersion;
|
||||
if (c.XkbQueryExtension(
|
||||
@ptrCast(@alignCast(xlib_display)),
|
||||
&opcode,
|
||||
&base_event_code,
|
||||
&base_error_code,
|
||||
&major,
|
||||
&minor,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbQueryExtension", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
log.debug("Xkb.init: running XkbSelectEventDetails", .{});
|
||||
if (c.XkbSelectEventDetails(
|
||||
@ptrCast(@alignCast(xlib_display)),
|
||||
c.XkbUseCoreKbd,
|
||||
c.XkbStateNotify,
|
||||
c.XkbModifierStateMask,
|
||||
c.XkbModifierStateMask,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
return .{
|
||||
.display = xlib_display,
|
||||
.base_event_code = base_event_code,
|
||||
.atoms = .init(gdk_x11_display),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
/// Checks for an immediate pending XKB state update event, and returns the
|
||||
/// keyboard state based on if it finds any. This is necessary as the
|
||||
/// standard GTK X11 API (and X11 in general) does not include the current
|
||||
/// key pressed in any modifier state snapshot for that event (e.g. if the
|
||||
/// pressed key is a modifier, that is not necessarily reflected in the
|
||||
/// modifiers).
|
||||
///
|
||||
/// Returns null if there is no event. In this case, the caller should fall
|
||||
/// back to the standard GDK modifier state (this likely means the key
|
||||
/// event did not result in a modifier change).
|
||||
pub fn eventMods(
|
||||
self: App,
|
||||
device: ?*gdk.Device,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) ?input.Mods {
|
||||
_ = device;
|
||||
_ = gtk_mods;
|
||||
|
||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
||||
if (c.XEventsQueued(
|
||||
@ptrCast(@alignCast(self.display)),
|
||||
c.QueuedAfterReading,
|
||||
) == 0) return null;
|
||||
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = c.XPeekEvent(@ptrCast(@alignCast(self.display)), &nextEvent);
|
||||
if (nextEvent.type != self.base_event_code) return null;
|
||||
|
||||
const xkb_event: *c.XkbEvent = @ptrCast(&nextEvent);
|
||||
if (xkb_event.any.xkb_type != c.XkbStateNotify) return null;
|
||||
|
||||
const xkb_state_notify_event: *c.XkbStateNotifyEvent = @ptrCast(xkb_event);
|
||||
// Check the state according to XKB masks.
|
||||
const lookup_mods = xkb_state_notify_event.lookup_mods;
|
||||
var mods: input.Mods = .{};
|
||||
|
||||
log.debug("X11: found extra XkbStateNotify event w/lookup_mods: {b}", .{lookup_mods});
|
||||
if (lookup_mods & c.ShiftMask != 0) mods.shift = true;
|
||||
if (lookup_mods & c.ControlMask != 0) mods.ctrl = true;
|
||||
if (lookup_mods & c.Mod1Mask != 0) mods.alt = true;
|
||||
if (lookup_mods & c.Mod4Mask != 0) mods.super = true;
|
||||
if (lookup_mods & c.LockMask != 0) mods.caps_lock = true;
|
||||
|
||||
return mods;
|
||||
}
|
||||
|
||||
pub fn supportsQuickTerminal(_: App) bool {
|
||||
log.warn("quick terminal is not yet supported on X11", .{});
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn initQuickTerminal(_: *App, _: *ApprtWindow) !void {}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
x11_surface: *gdk_x11.X11Surface,
|
||||
|
||||
blur_region: Region = .{},
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
apprt_window: *ApprtWindow,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const surface = apprt_window.as(gtk.Native).getSurface() orelse
|
||||
return error.NotX11Surface;
|
||||
|
||||
const x11_surface = gobject.ext.cast(
|
||||
gdk_x11.X11Surface,
|
||||
surface,
|
||||
) orelse return error.NotX11Surface;
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.apprt_window = apprt_window,
|
||||
.x11_surface = x11_surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
// The user could have toggled between CSDs and SSDs,
|
||||
// therefore we need to recalculate the blur region offset.
|
||||
self.blur_region = blur: {
|
||||
// NOTE(pluiedev): CSDs are a f--king mistake.
|
||||
// Please, GNOME, stop this nonsense of making a window ~30% bigger
|
||||
// internally than how they really are just for your shadows and
|
||||
// rounded corners and all that fluff. Please. I beg of you.
|
||||
var x: f64 = 0;
|
||||
var y: f64 = 0;
|
||||
|
||||
self.apprt_window.as(gtk.Native).getSurfaceTransform(&x, &y);
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale: f64 = @floatFromInt(self.apprt_window.as(gtk.Widget).getScaleFactor());
|
||||
x *= scale;
|
||||
y *= scale;
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
};
|
||||
};
|
||||
self.syncBlur() catch |err| {
|
||||
log.err("failed to synchronize blur={}", .{err});
|
||||
};
|
||||
self.syncDecorations() catch |err| {
|
||||
log.err("failed to synchronize decorations={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return switch (self.apprt_window.getWindowDecoration()) {
|
||||
.auto, .client => true,
|
||||
.server, .none => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn syncBlur(self: *Window) !void {
|
||||
// FIXME: This doesn't currently factor in rounded corners on Adwaita,
|
||||
// which means that the blur region will grow slightly outside of the
|
||||
// window borders. Unfortunately, actually calculating the rounded
|
||||
// region can be quite complex without having access to existing APIs
|
||||
// (cf. https://github.com/cutefishos/fishui/blob/41d4ba194063a3c7fff4675619b57e6ac0504f06/src/platforms/linux/blurhelper/windowblur.cpp#L134)
|
||||
// and I think it's not really noticeable enough to justify the effort.
|
||||
// (Wayland also has this visual artifact anyway...)
|
||||
|
||||
const gtk_widget = self.apprt_window.as(gtk.Widget);
|
||||
const config = if (self.apprt_window.getConfig()) |v| v.get() else return;
|
||||
|
||||
// Transform surface coordinates to device coordinates.
|
||||
const scale = gtk_widget.getScaleFactor();
|
||||
self.blur_region.width = gtk_widget.getWidth() * scale;
|
||||
self.blur_region.height = gtk_widget.getHeight() * scale;
|
||||
|
||||
const blur = config.@"background-blur";
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.x11_surface.getXid(),
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur.enabled()) {
|
||||
try self.changeProperty(
|
||||
Region,
|
||||
self.app.atoms.kde_blur,
|
||||
c.XA_CARDINAL,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&self.blur_region,
|
||||
);
|
||||
} else {
|
||||
try self.deleteProperty(self.app.atoms.kde_blur);
|
||||
}
|
||||
}
|
||||
|
||||
fn syncDecorations(self: *Window) !void {
|
||||
var hints: MotifWMHints = .{};
|
||||
|
||||
self.getWindowProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{},
|
||||
&hints,
|
||||
) catch |err| switch (err) {
|
||||
// motif_wm_hints is already initialized, so this is fine
|
||||
error.PropertyNotFound => {},
|
||||
|
||||
error.RequestFailed,
|
||||
error.PropertyTypeMismatch,
|
||||
error.PropertyFormatMismatch,
|
||||
=> return err,
|
||||
};
|
||||
|
||||
hints.flags.decorations = true;
|
||||
hints.decorations.all = switch (self.apprt_window.getWindowDecoration()) {
|
||||
.server => true,
|
||||
.auto, .client, .none => false,
|
||||
};
|
||||
|
||||
try self.changeProperty(
|
||||
MotifWMHints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
self.app.atoms.motif_wm_hints,
|
||||
._32,
|
||||
.{ .mode = .replace },
|
||||
&hints,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn addSubprocessEnv(self: *Window, env: *std.process.EnvMap) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const window_id = try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"{}",
|
||||
.{self.x11_surface.getXid()},
|
||||
);
|
||||
|
||||
try env.put("WINDOWID", window_id);
|
||||
}
|
||||
|
||||
pub fn setUrgent(self: *Window, urgent: bool) !void {
|
||||
self.x11_surface.setUrgencyHint(@intFromBool(urgent));
|
||||
}
|
||||
|
||||
fn getWindowProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
name: c.Atom,
|
||||
typ: c.Atom,
|
||||
comptime format: PropertyFormat,
|
||||
options: struct {
|
||||
offset: c_long = 0,
|
||||
length: c_long = std.math.maxInt(c_long),
|
||||
delete: bool = false,
|
||||
},
|
||||
result: *T,
|
||||
) GetWindowPropertyError!void {
|
||||
// FIXME: Maybe we should switch to libxcb one day.
|
||||
// Sounds like a much better idea than whatever this is
|
||||
var actual_type_return: c.Atom = undefined;
|
||||
var actual_format_return: c_int = undefined;
|
||||
var nitems_return: c_ulong = undefined;
|
||||
var bytes_after_return: c_ulong = undefined;
|
||||
var prop_return: ?format.bufferType() = null;
|
||||
|
||||
const code = c.XGetWindowProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
options.offset,
|
||||
options.length,
|
||||
@intFromBool(options.delete),
|
||||
typ,
|
||||
&actual_type_return,
|
||||
&actual_format_return,
|
||||
&nitems_return,
|
||||
&bytes_after_return,
|
||||
@ptrCast(&prop_return),
|
||||
);
|
||||
if (code != c.Success) return error.RequestFailed;
|
||||
|
||||
if (actual_type_return == c.None) return error.PropertyNotFound;
|
||||
if (typ != actual_type_return) return error.PropertyTypeMismatch;
|
||||
if (@intFromEnum(format) != actual_format_return) return error.PropertyFormatMismatch;
|
||||
|
||||
const data_ptr: *T = @ptrCast(prop_return);
|
||||
result.* = data_ptr.*;
|
||||
_ = c.XFree(prop_return);
|
||||
}
|
||||
|
||||
fn changeProperty(
|
||||
self: *Window,
|
||||
comptime T: type,
|
||||
name: c.Atom,
|
||||
typ: c.Atom,
|
||||
comptime format: PropertyFormat,
|
||||
options: struct {
|
||||
mode: PropertyChangeMode,
|
||||
},
|
||||
value: *T,
|
||||
) X11Error!void {
|
||||
const data: format.bufferType() = @ptrCast(value);
|
||||
|
||||
const status = c.XChangeProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
typ,
|
||||
@intFromEnum(format),
|
||||
@intFromEnum(options.mode),
|
||||
data,
|
||||
@divExact(@sizeOf(T), @sizeOf(format.elemType())),
|
||||
);
|
||||
|
||||
// For some godforsaken reason Xlib alternates between
|
||||
// error values (0 = success) and booleans (1 = success), and they look exactly
|
||||
// the same in the signature (just `int`, since Xlib is written in C89)...
|
||||
if (status == 0) return error.RequestFailed;
|
||||
}
|
||||
|
||||
fn deleteProperty(self: *Window, name: c.Atom) X11Error!void {
|
||||
const status = c.XDeleteProperty(
|
||||
@ptrCast(@alignCast(self.app.display)),
|
||||
self.x11_surface.getXid(),
|
||||
name,
|
||||
);
|
||||
if (status == 0) return error.RequestFailed;
|
||||
}
|
||||
};
|
||||
|
||||
const X11Error = error{
|
||||
RequestFailed,
|
||||
};
|
||||
|
||||
const GetWindowPropertyError = X11Error || error{
|
||||
PropertyNotFound,
|
||||
PropertyTypeMismatch,
|
||||
PropertyFormatMismatch,
|
||||
};
|
||||
|
||||
const Atoms = struct {
|
||||
kde_blur: c.Atom,
|
||||
motif_wm_hints: c.Atom,
|
||||
|
||||
fn init(display: *gdk_x11.X11Display) Atoms {
|
||||
return .{
|
||||
.kde_blur = gdk_x11.x11GetXatomByNameForDisplay(
|
||||
display,
|
||||
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||
),
|
||||
.motif_wm_hints = gdk_x11.x11GetXatomByNameForDisplay(
|
||||
display,
|
||||
"_MOTIF_WM_HINTS",
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const PropertyChangeMode = enum(c_int) {
|
||||
replace = c.PropModeReplace,
|
||||
prepend = c.PropModePrepend,
|
||||
append = c.PropModeAppend,
|
||||
};
|
||||
|
||||
const PropertyFormat = enum(c_int) {
|
||||
_8 = 8,
|
||||
_16 = 16,
|
||||
_32 = 32,
|
||||
|
||||
fn elemType(comptime self: PropertyFormat) type {
|
||||
return switch (self) {
|
||||
._8 => c_char,
|
||||
._16 => c_int,
|
||||
._32 => c_long,
|
||||
};
|
||||
}
|
||||
|
||||
fn bufferType(comptime self: PropertyFormat) type {
|
||||
// The buffer type has to be a multi-pointer to bytes
|
||||
// *aligned to the element type* (very important,
|
||||
// otherwise you'll read garbage!)
|
||||
//
|
||||
// I know this is really ugly. X11 is ugly. I consider it apropos.
|
||||
return [*]align(@alignOf(self.elemType())) u8;
|
||||
}
|
||||
};
|
||||
|
||||
const Region = extern struct {
|
||||
x: c_long = 0,
|
||||
y: c_long = 0,
|
||||
width: c_long = 0,
|
||||
height: c_long = 0,
|
||||
};
|
||||
|
||||
// See Xm/MwmUtil.h, packaged with the Motif Window Manager
|
||||
const MotifWMHints = extern struct {
|
||||
flags: packed struct(c_ulong) {
|
||||
_pad: u1 = 0,
|
||||
decorations: bool = false,
|
||||
|
||||
// We don't really care about the other flags
|
||||
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 2) = 0,
|
||||
} = .{},
|
||||
functions: c_ulong = 0,
|
||||
decorations: packed struct(c_ulong) {
|
||||
all: bool = false,
|
||||
|
||||
// We don't really care about the other flags
|
||||
_rest: std.meta.Int(.unsigned, @bitSizeOf(c_ulong) - 1) = 0,
|
||||
} = .{},
|
||||
input_mode: c_long = 0,
|
||||
status: c_ulong = 0,
|
||||
};
|
@@ -1,12 +1,15 @@
|
||||
//! Application runtime that uses GTK4.
|
||||
const internal_os = @import("../os/main.zig");
|
||||
|
||||
// The required comptime API for any apprt.
|
||||
pub const App = @import("gtk/App.zig");
|
||||
pub const Surface = @import("gtk/Surface.zig");
|
||||
pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
|
||||
pub const resourcesDir = internal_os.resourcesDir;
|
||||
|
||||
// The exported API, custom for the apprt.
|
||||
pub const class = @import("gtk/class.zig");
|
||||
pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
_ = @import("gtk/inspector.zig");
|
||||
_ = @import("gtk/key.zig");
|
||||
_ = @import("gtk/ext.zig");
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
||||
/// Wrapper around GTK's builder APIs that perform some comptime checks.
|
||||
const Builder = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
resource_name: [:0]const u8,
|
||||
builder: ?*gtk.Builder,
|
||||
|
||||
pub fn init(
|
||||
/// The "name" of the resource.
|
||||
comptime name: []const u8,
|
||||
/// The major version of the minimum Adwaita version that is required to use
|
||||
/// this resource.
|
||||
comptime major: u16,
|
||||
/// The minor version of the minimum Adwaita version that is required to use
|
||||
/// this resource.
|
||||
comptime minor: u16,
|
||||
) Builder {
|
||||
const resource_path = comptime resource_path: {
|
||||
const gresource = @import("gresource.zig");
|
||||
// Check to make sure that our file is listed as a
|
||||
// `blueprint_file` in `gresource.zig`. If it isn't Ghostty
|
||||
// could crash at runtime when we try and load a nonexistent
|
||||
// GResource.
|
||||
for (gresource.blueprint_files) |file| {
|
||||
if (major != file.major or minor != file.minor or !std.mem.eql(u8, file.name, name)) continue;
|
||||
// Use @embedFile to make sure that the `.blp` file exists
|
||||
// at compile time. Zig _should_ discard the data so that
|
||||
// it doesn't end up in the final executable. At runtime we
|
||||
// will load the data from a GResource.
|
||||
const blp_filename = std.fmt.comptimePrint(
|
||||
"ui/{d}.{d}/{s}.blp",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
_ = @embedFile(blp_filename);
|
||||
break :resource_path std.fmt.comptimePrint(
|
||||
"/com/mitchellh/ghostty/ui/{d}.{d}/{s}.ui",
|
||||
.{
|
||||
file.major,
|
||||
file.minor,
|
||||
file.name,
|
||||
},
|
||||
);
|
||||
} else @compileError("missing blueprint file '" ++ name ++ "' in gresource.zig");
|
||||
};
|
||||
|
||||
return .{
|
||||
.resource_name = resource_path,
|
||||
.builder = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setWidgetClassTemplate(self: *const Builder, class: *gtk.WidgetClass) void {
|
||||
class.setTemplateFromResource(self.resource_name);
|
||||
}
|
||||
|
||||
pub fn getObject(self: *Builder, comptime T: type, name: [:0]const u8) ?*T {
|
||||
const builder = builder: {
|
||||
if (self.builder) |builder| break :builder builder;
|
||||
const builder = gtk.Builder.newFromResource(self.resource_name);
|
||||
self.builder = builder;
|
||||
break :builder builder;
|
||||
};
|
||||
|
||||
return gobject.ext.cast(T, builder.getObject(name) orelse return null);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Builder) void {
|
||||
if (self.builder) |builder| builder.unref();
|
||||
}
|
@@ -1,212 +0,0 @@
|
||||
/// Clipboard Confirmation Window
|
||||
const ClipboardConfirmation = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
const App = @import("App.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
app: *App,
|
||||
dialog: *DialogType,
|
||||
data: [:0]u8,
|
||||
core_surface: *CoreSurface,
|
||||
pending_req: apprt.ClipboardRequest,
|
||||
text_view: *gtk.TextView,
|
||||
text_view_scroll: *gtk.ScrolledWindow,
|
||||
reveal_button: *gtk.Button,
|
||||
hide_button: *gtk.Button,
|
||||
remember_choice: if (adw_version.supportsSwitchRow()) ?*adw.SwitchRow else ?*anyopaque,
|
||||
|
||||
pub fn create(
|
||||
app: *App,
|
||||
data: []const u8,
|
||||
core_surface: *CoreSurface,
|
||||
request: apprt.ClipboardRequest,
|
||||
is_secure_input: bool,
|
||||
) !void {
|
||||
if (app.clipboard_confirmation_window != null) return error.WindowAlreadyExists;
|
||||
|
||||
const alloc = app.core_app.alloc;
|
||||
const self = try alloc.create(ClipboardConfirmation);
|
||||
errdefer alloc.destroy(self);
|
||||
|
||||
try self.init(
|
||||
app,
|
||||
data,
|
||||
core_surface,
|
||||
request,
|
||||
is_secure_input,
|
||||
);
|
||||
|
||||
app.clipboard_confirmation_window = self;
|
||||
}
|
||||
|
||||
/// Not public because this should be called by the GTK lifecycle.
|
||||
fn destroy(self: *ClipboardConfirmation) void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
self.app.clipboard_confirmation_window = null;
|
||||
alloc.free(self.data);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn init(
|
||||
self: *ClipboardConfirmation,
|
||||
app: *App,
|
||||
data: []const u8,
|
||||
core_surface: *CoreSurface,
|
||||
request: apprt.ClipboardRequest,
|
||||
is_secure_input: bool,
|
||||
) !void {
|
||||
var builder: Builder = switch (DialogType) {
|
||||
adw.AlertDialog => switch (request) {
|
||||
.osc_52_read => .init("ccw-osc-52-read", 1, 5),
|
||||
.osc_52_write => .init("ccw-osc-52-write", 1, 5),
|
||||
.paste => .init("ccw-paste", 1, 5),
|
||||
},
|
||||
adw.MessageDialog => switch (request) {
|
||||
.osc_52_read => .init("ccw-osc-52-read", 1, 2),
|
||||
.osc_52_write => .init("ccw-osc-52-write", 1, 2),
|
||||
.paste => .init("ccw-paste", 1, 2),
|
||||
},
|
||||
else => unreachable,
|
||||
};
|
||||
defer builder.deinit();
|
||||
|
||||
const dialog = builder.getObject(DialogType, "clipboard_confirmation_window").?;
|
||||
const text_view = builder.getObject(gtk.TextView, "text_view").?;
|
||||
const reveal_button = builder.getObject(gtk.Button, "reveal_button").?;
|
||||
const hide_button = builder.getObject(gtk.Button, "hide_button").?;
|
||||
const text_view_scroll = builder.getObject(gtk.ScrolledWindow, "text_view_scroll").?;
|
||||
const remember_choice = if (adw_version.supportsSwitchRow())
|
||||
builder.getObject(adw.SwitchRow, "remember_choice")
|
||||
else
|
||||
null;
|
||||
|
||||
const copy = try app.core_app.alloc.dupeZ(u8, data);
|
||||
errdefer app.core_app.alloc.free(copy);
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.dialog = dialog,
|
||||
.data = copy,
|
||||
.core_surface = core_surface,
|
||||
.pending_req = request,
|
||||
.text_view = text_view,
|
||||
.text_view_scroll = text_view_scroll,
|
||||
.reveal_button = reveal_button,
|
||||
.hide_button = hide_button,
|
||||
.remember_choice = remember_choice,
|
||||
};
|
||||
|
||||
const buffer = gtk.TextBuffer.new(null);
|
||||
errdefer buffer.unref();
|
||||
buffer.insertAtCursor(copy.ptr, @intCast(copy.len));
|
||||
text_view.setBuffer(buffer);
|
||||
|
||||
if (is_secure_input) {
|
||||
text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
|
||||
self.text_view.as(gtk.Widget).addCssClass("blurred");
|
||||
|
||||
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
|
||||
|
||||
_ = gtk.Button.signals.clicked.connect(
|
||||
reveal_button,
|
||||
*ClipboardConfirmation,
|
||||
gtkRevealButtonClicked,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.Button.signals.clicked.connect(
|
||||
hide_button,
|
||||
*ClipboardConfirmation,
|
||||
gtkHideButtonClicked,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
_ = DialogType.signals.response.connect(
|
||||
dialog,
|
||||
*ClipboardConfirmation,
|
||||
gtkResponse,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => {
|
||||
const parent: ?*gtk.Widget = widget: {
|
||||
const window = core_surface.rt_surface.container.window() orelse break :widget null;
|
||||
break :widget window.window.as(gtk.Widget);
|
||||
};
|
||||
dialog.as(adw.Dialog).present(parent);
|
||||
},
|
||||
adw.MessageDialog => dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn handleResponse(self: *ClipboardConfirmation, response: [*:0]const u8) void {
|
||||
const is_ok = std.mem.orderZ(u8, response, "ok") == .eq;
|
||||
|
||||
if (is_ok) {
|
||||
self.core_surface.completeClipboardRequest(
|
||||
self.pending_req,
|
||||
self.data,
|
||||
true,
|
||||
) catch |err| {
|
||||
log.err("Failed to requeue clipboard request: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
if (self.remember_choice) |remember| remember: {
|
||||
if (!adw_version.supportsSwitchRow()) break :remember;
|
||||
if (remember.getActive() == 0) break :remember;
|
||||
|
||||
switch (self.pending_req) {
|
||||
.osc_52_read => self.core_surface.config.clipboard_read = if (is_ok) .allow else .deny,
|
||||
.osc_52_write => self.core_surface.config.clipboard_write = if (is_ok) .allow else .deny,
|
||||
.paste => {},
|
||||
}
|
||||
}
|
||||
|
||||
self.destroy();
|
||||
}
|
||||
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 {
|
||||
self.handleResponse(response);
|
||||
}
|
||||
|
||||
fn gtkRevealButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(true));
|
||||
self.text_view.as(gtk.Widget).removeCssClass("blurred");
|
||||
|
||||
self.hide_button.as(gtk.Widget).setVisible(@intFromBool(true));
|
||||
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(false));
|
||||
}
|
||||
|
||||
fn gtkHideButtonClicked(_: *gtk.Button, self: *ClipboardConfirmation) callconv(.c) void {
|
||||
self.text_view_scroll.as(gtk.Widget).setSensitive(@intFromBool(false));
|
||||
self.text_view.as(gtk.Widget).addCssClass("blurred");
|
||||
|
||||
self.hide_button.as(gtk.Widget).setVisible(@intFromBool(false));
|
||||
self.reveal_button.as(gtk.Widget).setVisible(@intFromBool(true));
|
||||
}
|
@@ -1,151 +0,0 @@
|
||||
const CloseDialog = @This();
|
||||
const std = @import("std");
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
const adw = @import("adw");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const i18n = @import("../../os/main.zig").i18n;
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
const adwaita = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.close_dialog);
|
||||
|
||||
// We don't fall back to the GTK Message/AlertDialogs since
|
||||
// we don't plan to support libadw < 1.2 as of time of writing
|
||||
// TODO: Switch to just adw.AlertDialog when we drop Debian 12 support
|
||||
const DialogType = if (adwaita.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
/// Open the dialog when the user requests to close a window/tab/split/etc.
|
||||
/// but there's still one or more running processes inside the target that
|
||||
/// cannot be closed automatically. We then ask the user whether they want
|
||||
/// to terminate existing processes.
|
||||
pub fn show(target: Target) !void {
|
||||
// If we don't have a possible window to ask the user,
|
||||
// in most situations (e.g. when a split isn't attached to a window)
|
||||
// we should just close unconditionally.
|
||||
const dialog_window = target.dialogWindow() orelse {
|
||||
target.close();
|
||||
return;
|
||||
};
|
||||
|
||||
const dialog = switch (DialogType) {
|
||||
adw.AlertDialog => adw.AlertDialog.new(target.title(), target.body()),
|
||||
adw.MessageDialog => adw.MessageDialog.new(dialog_window, target.title(), target.body()),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// AlertDialog and MessageDialog have essentially the same API,
|
||||
// so we can cheat a little here
|
||||
dialog.addResponse("cancel", i18n._("Cancel"));
|
||||
dialog.setCloseResponse("cancel");
|
||||
|
||||
dialog.addResponse("close", i18n._("Close"));
|
||||
dialog.setResponseAppearance("close", .destructive);
|
||||
|
||||
// Need a stable pointer
|
||||
const target_ptr = try target.allocator().create(Target);
|
||||
target_ptr.* = target;
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *Target, responseCallback, target_ptr, .{});
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => dialog.as(adw.Dialog).present(dialog_window.as(gtk.Widget)),
|
||||
adw.MessageDialog => dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn responseCallback(
|
||||
_: *DialogType,
|
||||
response: [*:0]const u8,
|
||||
target: *Target,
|
||||
) callconv(.c) void {
|
||||
const alloc = target.allocator();
|
||||
defer alloc.destroy(target);
|
||||
|
||||
if (std.mem.orderZ(u8, response, "close") == .eq) target.close();
|
||||
}
|
||||
|
||||
/// The target of a close dialog.
|
||||
///
|
||||
/// This is here so that we can consolidate all logic related to
|
||||
/// prompting the user and closing windows/tabs/surfaces/etc.
|
||||
/// together into one struct that is the sole source of truth.
|
||||
pub const Target = union(enum) {
|
||||
app: *App,
|
||||
window: *Window,
|
||||
tab: *Tab,
|
||||
surface: *Surface,
|
||||
|
||||
pub fn title(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("Quit Ghostty?"),
|
||||
.window => i18n._("Close Window?"),
|
||||
.tab => i18n._("Close Tab?"),
|
||||
.surface => i18n._("Close Split?"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn body(self: Target) [*:0]const u8 {
|
||||
return switch (self) {
|
||||
.app => i18n._("All terminal sessions will be terminated."),
|
||||
.window => i18n._("All terminal sessions in this window will be terminated."),
|
||||
.tab => i18n._("All terminal sessions in this tab will be terminated."),
|
||||
.surface => i18n._("The currently running process in this split will be terminated."),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn dialogWindow(self: Target) ?*gtk.Window {
|
||||
return switch (self) {
|
||||
.app => {
|
||||
// Find the currently focused window. We don't store this
|
||||
// anywhere inside the App structure for some reason, so
|
||||
// we have to query every single open window and see which
|
||||
// one is active (focused and receiving keyboard input)
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
return @ptrCast(@alignCast(focused.f_data));
|
||||
},
|
||||
.window => |v| v.window.as(gtk.Window),
|
||||
.tab => |v| v.window.window.as(gtk.Window),
|
||||
.surface => |v| {
|
||||
const window_ = v.container.window() orelse return null;
|
||||
return window_.window.as(gtk.Window);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn allocator(self: Target) std.mem.Allocator {
|
||||
return switch (self) {
|
||||
.app => |v| v.core_app.alloc,
|
||||
.window => |v| v.app.core_app.alloc,
|
||||
.tab => |v| v.window.app.core_app.alloc,
|
||||
.surface => |v| v.app.core_app.alloc,
|
||||
};
|
||||
}
|
||||
|
||||
fn close(self: Target) void {
|
||||
switch (self) {
|
||||
.app => |v| v.quitNow(),
|
||||
.window => |v| v.window.as(gtk.Window).destroy(),
|
||||
.tab => |v| v.remove(),
|
||||
.surface => |v| v.container.remove(),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
fn findActiveWindow(data: ?*const anyopaque, _: ?*const anyopaque) callconv(.c) c_int {
|
||||
const window: *gtk.Window = @ptrCast(@alignCast(@constCast(data orelse return -1)));
|
||||
|
||||
// Confusingly, `isActive` returns 1 when active,
|
||||
// but we want to return 0 to indicate equality.
|
||||
// Abusing integers to be enums and booleans is a terrible idea, C.
|
||||
return if (window.isActive() != 0) 0 else -1;
|
||||
}
|
@@ -1,258 +0,0 @@
|
||||
const CommandPalette = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const adw = @import("adw");
|
||||
const gio = @import("gio");
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
const inputpkg = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const Window = @import("Window.zig");
|
||||
|
||||
const log = std.log.scoped(.command_palette);
|
||||
|
||||
window: *Window,
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
/// 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 fn init(self: *CommandPalette, window: *Window) !void {
|
||||
// Register the custom command type *before* initializing the builder
|
||||
// If we don't do this now, the builder will complain that it doesn't know
|
||||
// about this type and fail to initialize
|
||||
_ = Command.getGObjectType();
|
||||
|
||||
var builder = Builder.init("command-palette", 1, 5);
|
||||
defer builder.deinit();
|
||||
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.arena = .init(window.app.core_app.alloc),
|
||||
.dialog = builder.getObject(adw.Dialog, "command-palette").?,
|
||||
.search = builder.getObject(gtk.SearchEntry, "search").?,
|
||||
.view = builder.getObject(gtk.ListView, "view").?,
|
||||
.model = builder.getObject(gtk.SingleSelection, "model").?,
|
||||
.source = builder.getObject(gio.ListStore, "source").?,
|
||||
};
|
||||
|
||||
// Manually take a reference here so that the dialog
|
||||
// remains in memory after closing
|
||||
self.dialog.ref();
|
||||
errdefer self.dialog.unref();
|
||||
|
||||
_ = gtk.SearchEntry.signals.stop_search.connect(
|
||||
self.search,
|
||||
*CommandPalette,
|
||||
searchStopped,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
_ = gtk.SearchEntry.signals.activate.connect(
|
||||
self.search,
|
||||
*CommandPalette,
|
||||
searchActivated,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
_ = gtk.ListView.signals.activate.connect(
|
||||
self.view,
|
||||
*CommandPalette,
|
||||
rowActivated,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
try self.updateConfig(&self.window.app.config);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CommandPalette) void {
|
||||
self.arena.deinit();
|
||||
self.dialog.unref();
|
||||
}
|
||||
|
||||
pub fn toggle(self: *CommandPalette) void {
|
||||
self.dialog.present(self.window.window.as(gtk.Widget));
|
||||
// Focus on the search bar when opening the dialog
|
||||
_ = self.search.as(gtk.Widget).grabFocus();
|
||||
}
|
||||
|
||||
pub fn updateConfig(self: *CommandPalette, config: *const configpkg.Config) !void {
|
||||
// Clear existing binds and clear allocated data
|
||||
self.source.removeAll();
|
||||
_ = self.arena.reset(.retain_capacity);
|
||||
|
||||
for (config.@"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 = try Command.new(
|
||||
self.arena.allocator(),
|
||||
command,
|
||||
config.keybind.set,
|
||||
);
|
||||
const cmd_ref = cmd.as(gobject.Object);
|
||||
self.source.append(cmd_ref);
|
||||
cmd_ref.unref();
|
||||
}
|
||||
}
|
||||
|
||||
fn activated(self: *CommandPalette, pos: c_uint) void {
|
||||
// Use self.model and not self.source here to use the list of *visible* results
|
||||
const object = self.model.as(gio.ListModel).getObject(pos) orelse return;
|
||||
const cmd = gobject.ext.cast(Command, object) orelse return;
|
||||
|
||||
// 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.dialog.close();
|
||||
|
||||
const action = inputpkg.Binding.Action.parse(
|
||||
std.mem.span(cmd.cmd_c.action_key),
|
||||
) catch |err| {
|
||||
log.err("got invalid action={s} ({})", .{ cmd.cmd_c.action_key, err });
|
||||
return;
|
||||
};
|
||||
|
||||
self.window.performBindingAction(action);
|
||||
}
|
||||
|
||||
fn searchStopped(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
|
||||
// ESC was pressed - close the palette
|
||||
_ = self.dialog.close();
|
||||
}
|
||||
|
||||
fn searchActivated(_: *gtk.SearchEntry, self: *CommandPalette) callconv(.c) void {
|
||||
// If Enter is pressed, activate the selected entry
|
||||
self.activated(self.model.getSelected());
|
||||
}
|
||||
|
||||
fn rowActivated(_: *gtk.ListView, pos: c_uint, self: *CommandPalette) callconv(.c) void {
|
||||
self.activated(pos);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
parent: Parent,
|
||||
cmd_c: inputpkg.Command.C,
|
||||
|
||||
pub const getGObjectType = gobject.ext.defineClass(Command, .{
|
||||
.name = "GhosttyCommand",
|
||||
.classInit = Class.init,
|
||||
});
|
||||
|
||||
pub fn new(alloc: Allocator, cmd: inputpkg.Command, keybinds: inputpkg.Binding.Set) !*Command {
|
||||
const self = gobject.ext.newInstance(Command, .{});
|
||||
var buf: [64]u8 = undefined;
|
||||
|
||||
const action = action: {
|
||||
const trigger = keybinds.getTrigger(cmd.action) orelse break :action null;
|
||||
const accel = try key.accelFromTrigger(&buf, trigger) orelse break :action null;
|
||||
break :action try alloc.dupeZ(u8, accel);
|
||||
};
|
||||
|
||||
self.cmd_c = .{
|
||||
.title = cmd.title.ptr,
|
||||
.description = cmd.description.ptr,
|
||||
.action = if (action) |v| v.ptr else "",
|
||||
.action_key = try std.fmt.allocPrintZ(alloc, "{}", .{cmd.action}),
|
||||
};
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn as(self: *Command, comptime T: type) *T {
|
||||
return gobject.ext.as(T, self);
|
||||
}
|
||||
|
||||
pub const Parent = gobject.Object;
|
||||
|
||||
pub const Class = extern struct {
|
||||
parent: Parent.Class,
|
||||
|
||||
pub const Instance = Command;
|
||||
|
||||
pub fn init(class: *Class) callconv(.c) void {
|
||||
const info = @typeInfo(inputpkg.Command.C).@"struct";
|
||||
|
||||
// Expose all fields on the Command.C struct as properties
|
||||
// that can be accessed by the GObject type system
|
||||
// (and by extension, blueprints)
|
||||
const properties = comptime props: {
|
||||
var props: [info.fields.len]type = undefined;
|
||||
|
||||
for (info.fields, 0..) |field, i| {
|
||||
const accessor = struct {
|
||||
fn getter(cmd: *Command) ?[:0]const u8 {
|
||||
return std.mem.span(@field(cmd.cmd_c, field.name));
|
||||
}
|
||||
};
|
||||
|
||||
// "Canonicalize" field names into the format GObject expects
|
||||
const prop_name = prop_name: {
|
||||
var buf: [field.name.len:0]u8 = undefined;
|
||||
_ = std.mem.replace(u8, field.name, "_", "-", &buf);
|
||||
break :prop_name buf;
|
||||
};
|
||||
|
||||
props[i] = gobject.ext.defineProperty(
|
||||
&prop_name,
|
||||
Command,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.default = null,
|
||||
.accessor = gobject.ext.typedAccessor(
|
||||
Command,
|
||||
?[:0]const u8,
|
||||
.{
|
||||
.getter = &accessor.getter,
|
||||
},
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
break :props props;
|
||||
};
|
||||
|
||||
gobject.ext.registerProperties(class, &properties);
|
||||
}
|
||||
};
|
||||
};
|
@@ -1,102 +0,0 @@
|
||||
/// Configuration errors window.
|
||||
const ConfigErrorsDialog = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gio = @import("gio");
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
|
||||
const build_config = @import("../../build_config.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
|
||||
const App = @import("App.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const Builder = @import("Builder.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
const DialogType = if (adw_version.supportsDialogs()) adw.AlertDialog else adw.MessageDialog;
|
||||
|
||||
builder: Builder,
|
||||
dialog: *DialogType,
|
||||
error_message: *gtk.TextBuffer,
|
||||
|
||||
pub fn maybePresent(app: *App, window: ?*Window) void {
|
||||
if (app.config._diagnostics.empty()) return;
|
||||
|
||||
const config_errors_dialog = config_errors_dialog: {
|
||||
if (app.config_errors_dialog) |config_errors_dialog| break :config_errors_dialog config_errors_dialog;
|
||||
|
||||
var builder: Builder = switch (DialogType) {
|
||||
adw.AlertDialog => .init("config-errors-dialog", 1, 5),
|
||||
adw.MessageDialog => .init("config-errors-dialog", 1, 2),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
const dialog = builder.getObject(DialogType, "config_errors_dialog").?;
|
||||
const error_message = builder.getObject(gtk.TextBuffer, "error_message").?;
|
||||
|
||||
_ = DialogType.signals.response.connect(dialog, *App, onResponse, app, .{});
|
||||
|
||||
app.config_errors_dialog = .{
|
||||
.builder = builder,
|
||||
.dialog = dialog,
|
||||
.error_message = error_message,
|
||||
};
|
||||
|
||||
break :config_errors_dialog app.config_errors_dialog.?;
|
||||
};
|
||||
|
||||
{
|
||||
var start = std.mem.zeroes(gtk.TextIter);
|
||||
config_errors_dialog.error_message.getStartIter(&start);
|
||||
|
||||
var end = std.mem.zeroes(gtk.TextIter);
|
||||
config_errors_dialog.error_message.getEndIter(&end);
|
||||
|
||||
config_errors_dialog.error_message.delete(&start, &end);
|
||||
}
|
||||
|
||||
var msg_buf: [4095:0]u8 = undefined;
|
||||
var fbs = std.io.fixedBufferStream(&msg_buf);
|
||||
|
||||
for (app.config._diagnostics.items()) |diag| {
|
||||
fbs.reset();
|
||||
diag.write(fbs.writer()) catch |err| {
|
||||
log.warn(
|
||||
"error writing diagnostic to buffer err={}",
|
||||
.{err},
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
config_errors_dialog.error_message.insertAtCursor(&msg_buf, @intCast(fbs.pos));
|
||||
config_errors_dialog.error_message.insertAtCursor("\n", 1);
|
||||
}
|
||||
|
||||
switch (DialogType) {
|
||||
adw.AlertDialog => {
|
||||
const parent = if (window) |w| w.window.as(gtk.Widget) else null;
|
||||
config_errors_dialog.dialog.as(adw.Dialog).present(parent);
|
||||
},
|
||||
adw.MessageDialog => config_errors_dialog.dialog.as(gtk.Window).present(),
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
fn onResponse(_: *DialogType, response: [*:0]const u8, app: *App) callconv(.c) void {
|
||||
if (app.config_errors_dialog) |config_errors_dialog| config_errors_dialog.builder.deinit();
|
||||
app.config_errors_dialog = null;
|
||||
|
||||
if (std.mem.orderZ(u8, response, "reload") == .eq) {
|
||||
app.reloadConfig(.app, .{}) catch |err| {
|
||||
log.warn("error reloading config error={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
}
|
@@ -1,422 +0,0 @@
|
||||
const GlobalShortcuts = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const App = @import("App.zig");
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Binding = @import("../../input.zig").Binding;
|
||||
const key = @import("key.zig");
|
||||
|
||||
const log = std.log.scoped(.global_shortcuts);
|
||||
const Token = [16]u8;
|
||||
|
||||
app: *App,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
dbus: *gio.DBusConnection,
|
||||
|
||||
/// 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 fn init(alloc: Allocator, gio_app: *gio.Application) ?GlobalShortcuts {
|
||||
const dbus = gio_app.getDbusConnection() orelse return null;
|
||||
|
||||
return .{
|
||||
// To be initialized later
|
||||
.app = undefined,
|
||||
.arena = .init(alloc),
|
||||
.dbus = dbus,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *GlobalShortcuts) void {
|
||||
self.close();
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
fn close(self: *GlobalShortcuts) void {
|
||||
if (self.response_subscription != 0) {
|
||||
self.dbus.signalUnsubscribe(self.response_subscription);
|
||||
self.response_subscription = 0;
|
||||
}
|
||||
|
||||
if (self.activate_subscription != 0) {
|
||||
self.dbus.signalUnsubscribe(self.activate_subscription);
|
||||
self.activate_subscription = 0;
|
||||
}
|
||||
|
||||
if (self.handle) |handle| {
|
||||
// Close existing session
|
||||
self.dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
handle,
|
||||
"org.freedesktop.portal.Session",
|
||||
"Close",
|
||||
null,
|
||||
null,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
);
|
||||
self.handle = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refreshSession(self: *GlobalShortcuts, app: *App) !void {
|
||||
// Ensure we have a valid reference to the app
|
||||
// (it was left uninitialized in `init`)
|
||||
self.app = app;
|
||||
|
||||
// Close any existing sessions
|
||||
self.close();
|
||||
|
||||
// Update map
|
||||
var trigger_buf: [256]u8 = undefined;
|
||||
|
||||
self.map.clearRetainingCapacity();
|
||||
var it = self.app.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 = try key.xdgShortcutFromTrigger(
|
||||
&trigger_buf,
|
||||
entry.key_ptr.*,
|
||||
) orelse continue;
|
||||
|
||||
try self.map.put(
|
||||
self.arena.allocator(),
|
||||
try self.arena.allocator().dupeZ(u8, trigger),
|
||||
leaf.action,
|
||||
);
|
||||
}
|
||||
|
||||
if (self.map.count() > 0) {
|
||||
try self.request(.create_session);
|
||||
}
|
||||
}
|
||||
|
||||
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: *GlobalShortcuts = @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.map.get(std.mem.span(shortcut_id)) orelse return;
|
||||
|
||||
self.app.core_app.performAllAction(self.app, action) catch |err| {
|
||||
log.err("failed to perform action={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
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 handle = shortcuts.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 = shortcuts.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.err(
|
||||
"session handle not found in response={s}",
|
||||
.{vardict.print(@intFromBool(true))},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
shortcuts.handle = shortcuts.arena.allocator().dupeZ(u8, std.mem.span(handle.?)) catch {
|
||||
log.err("out of memory: failed to clone session handle", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
log.debug("session_handle={?s}", .{handle});
|
||||
|
||||
// Subscribe to keybind activations
|
||||
shortcuts.activate_subscription = shortcuts.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.err("failed to bind shortcuts={}", .{err});
|
||||
return;
|
||||
};
|
||||
},
|
||||
.bind_shortcuts => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// Submit a request to the global shortcuts portal.
|
||||
fn request(
|
||||
self: *GlobalShortcuts,
|
||||
comptime method: Method,
|
||||
) !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.err("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_: *GlobalShortcuts = @ptrCast(@alignCast(ud));
|
||||
|
||||
// Unsubscribe from the response signal
|
||||
if (self_.response_subscription != 0) {
|
||||
dbus.signalUnsubscribe(self_.response_subscription);
|
||||
self_.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_, vardict.?);
|
||||
},
|
||||
1 => log.debug("request was cancelled by user", .{}),
|
||||
2 => log.warn("request ended unexpectedly", .{}),
|
||||
else => log.err("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);
|
||||
|
||||
self.response_subscription = self.dbus.signalSubscribe(
|
||||
null,
|
||||
"org.freedesktop.portal.Request",
|
||||
"Response",
|
||||
request_path,
|
||||
null,
|
||||
.{},
|
||||
callbacks.responded,
|
||||
self,
|
||||
null,
|
||||
);
|
||||
|
||||
self.dbus.call(
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.GlobalShortcuts",
|
||||
method.name(),
|
||||
payload,
|
||||
null,
|
||||
.{},
|
||||
-1,
|
||||
null,
|
||||
callbacks.gotResponseHandle,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn getRequestPath(self: *GlobalShortcuts, token: [:0]const u8) ![:0]const u8 {
|
||||
// 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(self.dbus.getUniqueName().?);
|
||||
|
||||
const object_path = try std.mem.joinZ(self.arena.allocator(), "/", &.{
|
||||
"/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;
|
||||
}
|
@@ -1,470 +0,0 @@
|
||||
const ImguiWidget = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gdk = @import("gdk");
|
||||
const gtk = @import("gtk");
|
||||
const cimgui = @import("cimgui");
|
||||
const gl = @import("opengl");
|
||||
|
||||
const key = @import("key.zig");
|
||||
const input = @import("../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_imgui_widget);
|
||||
|
||||
/// This is called every frame to populate the ImGui frame.
|
||||
render_callback: ?*const fn (?*anyopaque) void = null,
|
||||
render_userdata: ?*anyopaque = null,
|
||||
|
||||
/// Our OpenGL widget
|
||||
gl_area: *gtk.GLArea,
|
||||
im_context: *gtk.IMContext,
|
||||
|
||||
/// ImGui Context
|
||||
ig_ctx: *cimgui.c.ImGuiContext,
|
||||
|
||||
/// Our previous instant used to calculate delta time for animations.
|
||||
instant: ?std.time.Instant = null,
|
||||
|
||||
/// Initialize the widget. This must have a stable pointer for events.
|
||||
pub fn init(self: *ImguiWidget) !void {
|
||||
// Each widget gets its own imgui context so we can have multiple
|
||||
// imgui views in the same application.
|
||||
const ig_ctx = cimgui.c.igCreateContext(null) orelse return error.OutOfMemory;
|
||||
errdefer cimgui.c.igDestroyContext(ig_ctx);
|
||||
cimgui.c.igSetCurrentContext(ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
io.BackendPlatformName = "ghostty_gtk";
|
||||
|
||||
// Our OpenGL area for drawing
|
||||
const gl_area = gtk.GLArea.new();
|
||||
gl_area.setAutoRender(@intFromBool(true));
|
||||
|
||||
// The GL area has to be focusable so that it can receive events
|
||||
gl_area.as(gtk.Widget).setFocusable(@intFromBool(true));
|
||||
gl_area.as(gtk.Widget).setFocusOnClick(@intFromBool(true));
|
||||
|
||||
// Clicks
|
||||
const gesture_click = gtk.GestureClick.new();
|
||||
errdefer gesture_click.unref();
|
||||
gesture_click.as(gtk.GestureSingle).setButton(0);
|
||||
gl_area.as(gtk.Widget).addController(gesture_click.as(gtk.EventController));
|
||||
|
||||
// Mouse movement
|
||||
const ec_motion = gtk.EventControllerMotion.new();
|
||||
errdefer ec_motion.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
|
||||
|
||||
// Scroll events
|
||||
const ec_scroll = gtk.EventControllerScroll.new(.flags_both_axes);
|
||||
errdefer ec_scroll.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_scroll.as(gtk.EventController));
|
||||
|
||||
// Focus controller will tell us about focus enter/exit events
|
||||
const ec_focus = gtk.EventControllerFocus.new();
|
||||
errdefer ec_focus.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_focus.as(gtk.EventController));
|
||||
|
||||
// Key event controller will tell us about raw keypress events.
|
||||
const ec_key = gtk.EventControllerKey.new();
|
||||
errdefer ec_key.unref();
|
||||
gl_area.as(gtk.Widget).addController(ec_key.as(gtk.EventController));
|
||||
errdefer gl_area.as(gtk.Widget).removeController(ec_key.as(gtk.EventController));
|
||||
|
||||
// The input method context that we use to translate key events into
|
||||
// characters. This doesn't have an event key controller attached because
|
||||
// we call it manually from our own key controller.
|
||||
const im_context = gtk.IMMulticontext.new();
|
||||
errdefer im_context.unref();
|
||||
|
||||
// Signals
|
||||
_ = gtk.Widget.signals.realize.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkRealize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.Widget.signals.unrealize.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkUnrealize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.Widget.signals.destroy.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkDestroy,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GLArea.signals.render.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkRender,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GLArea.signals.resize.connect(
|
||||
gl_area,
|
||||
*ImguiWidget,
|
||||
gtkResize,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerKey.signals.key_pressed.connect(
|
||||
ec_key,
|
||||
*ImguiWidget,
|
||||
gtkKeyPressed,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerKey.signals.key_released.connect(
|
||||
ec_key,
|
||||
*ImguiWidget,
|
||||
gtkKeyReleased,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerFocus.signals.enter.connect(
|
||||
ec_focus,
|
||||
*ImguiWidget,
|
||||
gtkFocusEnter,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerFocus.signals.leave.connect(
|
||||
ec_focus,
|
||||
*ImguiWidget,
|
||||
gtkFocusLeave,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GestureClick.signals.pressed.connect(
|
||||
gesture_click,
|
||||
*ImguiWidget,
|
||||
gtkMouseDown,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.GestureClick.signals.released.connect(
|
||||
gesture_click,
|
||||
*ImguiWidget,
|
||||
gtkMouseUp,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerMotion.signals.motion.connect(
|
||||
ec_motion,
|
||||
*ImguiWidget,
|
||||
gtkMouseMotion,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerScroll.signals.scroll.connect(
|
||||
ec_scroll,
|
||||
*ImguiWidget,
|
||||
gtkMouseScroll,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.IMContext.signals.commit.connect(
|
||||
im_context,
|
||||
*ImguiWidget,
|
||||
gtkInputCommit,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
self.* = .{
|
||||
.gl_area = gl_area,
|
||||
.im_context = im_context.as(gtk.IMContext),
|
||||
.ig_ctx = ig_ctx,
|
||||
};
|
||||
}
|
||||
|
||||
/// Deinitialize the widget. This should ONLY be called if the widget gl_area
|
||||
/// was never added to a parent. Otherwise, cleanup automatically happens
|
||||
/// when the widget is destroyed and this should NOT be called.
|
||||
pub fn deinit(self: *ImguiWidget) void {
|
||||
cimgui.c.igDestroyContext(self.ig_ctx);
|
||||
}
|
||||
|
||||
/// This should be called anytime the underlying data for the UI changes
|
||||
/// so that the UI can be refreshed.
|
||||
pub fn queueRender(self: *const ImguiWidget) void {
|
||||
self.gl_area.queueRender();
|
||||
}
|
||||
|
||||
/// Initialize the frame. Expects that the context is already current.
|
||||
fn newFrame(self: *ImguiWidget) !void {
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
// Determine our delta time
|
||||
const now = try std.time.Instant.now();
|
||||
io.DeltaTime = if (self.instant) |prev| delta: {
|
||||
const since_ns = now.since(prev);
|
||||
const since_s: f32 = @floatFromInt(since_ns / std.time.ns_per_s);
|
||||
break :delta @max(0.00001, since_s);
|
||||
} else (1 / 60);
|
||||
self.instant = now;
|
||||
}
|
||||
|
||||
fn translateMouseButton(button: c_uint) ?c_int {
|
||||
return switch (button) {
|
||||
1 => cimgui.c.ImGuiMouseButton_Left,
|
||||
2 => cimgui.c.ImGuiMouseButton_Middle,
|
||||
3 => cimgui.c.ImGuiMouseButton_Right,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
log.debug("imgui widget destroy", .{});
|
||||
self.deinit();
|
||||
}
|
||||
|
||||
fn gtkRealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
log.debug("gl surface realized", .{});
|
||||
|
||||
// We need to make the context current so we can call GL functions.
|
||||
area.makeCurrent();
|
||||
if (area.getError()) |err| {
|
||||
log.err("surface failed to realize: {s}", .{err.f_message orelse "(unknown)"});
|
||||
return;
|
||||
}
|
||||
|
||||
// realize means that our OpenGL context is ready, so we can now
|
||||
// initialize the ImgUI OpenGL backend for our context.
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
_ = cimgui.ImGui_ImplOpenGL3_Init(null);
|
||||
}
|
||||
|
||||
fn gtkUnrealize(area: *gtk.GLArea, self: *ImguiWidget) callconv(.c) void {
|
||||
_ = area;
|
||||
log.debug("gl surface unrealized", .{});
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
cimgui.ImGui_ImplOpenGL3_Shutdown();
|
||||
}
|
||||
|
||||
fn gtkResize(area: *gtk.GLArea, width: c_int, height: c_int, self: *ImguiWidget) callconv(.c) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor = area.as(gtk.Widget).getScaleFactor();
|
||||
log.debug("gl resize width={} height={} scale={}", .{
|
||||
width,
|
||||
height,
|
||||
scale_factor,
|
||||
});
|
||||
|
||||
// Our display size is always unscaled. We'll do the scaling in the
|
||||
// style instead. This creates crisper looking fonts.
|
||||
io.DisplaySize = .{ .x = @floatFromInt(width), .y = @floatFromInt(height) };
|
||||
io.DisplayFramebufferScale = .{ .x = 1, .y = 1 };
|
||||
|
||||
// Setup a new style and scale it appropriately.
|
||||
const style = cimgui.c.ImGuiStyle_ImGuiStyle();
|
||||
defer cimgui.c.ImGuiStyle_destroy(style);
|
||||
cimgui.c.ImGuiStyle_ScaleAllSizes(style, @floatFromInt(scale_factor));
|
||||
const active_style = cimgui.c.igGetStyle();
|
||||
active_style.* = style.*;
|
||||
}
|
||||
|
||||
fn gtkRender(_: *gtk.GLArea, _: *gdk.GLContext, self: *ImguiWidget) callconv(.c) c_int {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
|
||||
// Setup our frame. We render twice because some ImGui behaviors
|
||||
// take multiple renders to process. I don't know how to make this
|
||||
// more efficient.
|
||||
for (0..2) |_| {
|
||||
cimgui.ImGui_ImplOpenGL3_NewFrame();
|
||||
self.newFrame() catch |err| {
|
||||
log.err("failed to setup frame: {}", .{err});
|
||||
return 0;
|
||||
};
|
||||
cimgui.c.igNewFrame();
|
||||
|
||||
// Build our UI
|
||||
if (self.render_callback) |cb| cb(self.render_userdata);
|
||||
|
||||
// Render
|
||||
cimgui.c.igRender();
|
||||
}
|
||||
|
||||
// OpenGL final render
|
||||
gl.clearColor(0x28 / 0xFF, 0x2C / 0xFF, 0x34 / 0xFF, 1.0);
|
||||
gl.clear(gl.c.GL_COLOR_BUFFER_BIT);
|
||||
cimgui.ImGui_ImplOpenGL3_RenderDrawData(cimgui.c.igGetDrawData());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn gtkMouseMotion(
|
||||
_: *gtk.EventControllerMotion,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const scale_factor: f64 = @floatFromInt(self.gl_area.as(gtk.Widget).getScaleFactor());
|
||||
cimgui.c.ImGuiIO_AddMousePosEvent(
|
||||
io,
|
||||
@floatCast(x * scale_factor),
|
||||
@floatCast(y * scale_factor),
|
||||
);
|
||||
self.queueRender();
|
||||
}
|
||||
|
||||
fn gtkMouseDown(
|
||||
gesture: *gtk.GestureClick,
|
||||
_: c_int,
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
|
||||
if (translateMouseButton(gdk_button)) |button| {
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, true);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseUp(
|
||||
gesture: *gtk.GestureClick,
|
||||
_: c_int,
|
||||
_: f64,
|
||||
_: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
const gdk_button = gesture.as(gtk.GestureSingle).getCurrentButton();
|
||||
if (translateMouseButton(gdk_button)) |button| {
|
||||
cimgui.c.ImGuiIO_AddMouseButtonEvent(io, button, false);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkMouseScroll(
|
||||
_: *gtk.EventControllerScroll,
|
||||
x: f64,
|
||||
y: f64,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) c_int {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddMouseWheelEvent(
|
||||
io,
|
||||
@floatCast(x),
|
||||
@floatCast(-y),
|
||||
);
|
||||
|
||||
return @intFromBool(true);
|
||||
}
|
||||
|
||||
fn gtkFocusEnter(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, true);
|
||||
}
|
||||
|
||||
fn gtkFocusLeave(_: *gtk.EventControllerFocus, self: *ImguiWidget) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddFocusEvent(io, false);
|
||||
}
|
||||
|
||||
fn gtkInputCommit(
|
||||
_: *gtk.IMMulticontext,
|
||||
bytes: [*:0]u8,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
cimgui.c.ImGuiIO_AddInputCharactersUTF8(io, bytes);
|
||||
}
|
||||
|
||||
fn gtkKeyPressed(
|
||||
ec_key: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) c_int {
|
||||
return @intFromBool(self.keyEvent(
|
||||
.press,
|
||||
ec_key,
|
||||
keyval,
|
||||
keycode,
|
||||
gtk_mods,
|
||||
));
|
||||
}
|
||||
|
||||
fn gtkKeyReleased(
|
||||
ec_key: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
self: *ImguiWidget,
|
||||
) callconv(.c) void {
|
||||
_ = self.keyEvent(
|
||||
.release,
|
||||
ec_key,
|
||||
keyval,
|
||||
keycode,
|
||||
gtk_mods,
|
||||
);
|
||||
}
|
||||
|
||||
fn keyEvent(
|
||||
self: *ImguiWidget,
|
||||
action: input.Action,
|
||||
ec_key: *gtk.EventControllerKey,
|
||||
keyval: c_uint,
|
||||
keycode: c_uint,
|
||||
gtk_mods: gdk.ModifierType,
|
||||
) bool {
|
||||
_ = keycode;
|
||||
|
||||
self.queueRender();
|
||||
|
||||
cimgui.c.igSetCurrentContext(self.ig_ctx);
|
||||
const io: *cimgui.c.ImGuiIO = cimgui.c.igGetIO();
|
||||
|
||||
const mods = key.translateMods(gtk_mods);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftShift, mods.shift);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftCtrl, mods.ctrl);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftAlt, mods.alt);
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, cimgui.c.ImGuiKey_LeftSuper, mods.super);
|
||||
|
||||
// If our keyval has a key, then we send that key event
|
||||
if (key.keyFromKeyval(keyval)) |inputkey| {
|
||||
if (inputkey.imguiKey()) |imgui_key| {
|
||||
cimgui.c.ImGuiIO_AddKeyEvent(io, imgui_key, action == .press);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to process the event as text
|
||||
if (ec_key.as(gtk.EventController).getCurrentEvent()) |event| {
|
||||
_ = self.im_context.filterKeypress(event);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@@ -1,165 +0,0 @@
|
||||
//! Structure for managing GUI progress bar for a surface.
|
||||
const ProgressBar = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const Surface = @import("./Surface.zig");
|
||||
const terminal = @import("../../terminal/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_progress_bar);
|
||||
|
||||
/// The surface that we belong to.
|
||||
surface: *Surface,
|
||||
|
||||
/// Widget for showing progress bar.
|
||||
progress_bar: ?*gtk.ProgressBar = null,
|
||||
|
||||
/// Timer used to remove the progress bar if we have not received an update from
|
||||
/// the TUI in a while.
|
||||
progress_bar_timer: ?c_uint = null,
|
||||
|
||||
pub fn init(surface: *Surface) ProgressBar {
|
||||
return .{
|
||||
.surface = surface,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ProgressBar) void {
|
||||
self.stopProgressBarTimer();
|
||||
}
|
||||
|
||||
/// Show (or update if it already exists) a GUI progress bar.
|
||||
pub fn handleProgressReport(self: *ProgressBar, value: terminal.osc.Command.ProgressReport) error{}!bool {
|
||||
// Remove the progress bar.
|
||||
if (value.state == .remove) {
|
||||
self.stopProgressBarTimer();
|
||||
self.removeProgressBar();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const progress_bar = self.addProgressBar();
|
||||
self.startProgressBarTimer();
|
||||
|
||||
switch (value.state) {
|
||||
// already handled above
|
||||
.remove => unreachable,
|
||||
|
||||
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
|
||||
// Remove the `error` CSS class so that the progress bar shows as normal.
|
||||
.set => {
|
||||
progress_bar.as(gtk.Widget).removeCssClass("error");
|
||||
if (value.progress) |progress| {
|
||||
progress_bar.setFraction(computeFraction(progress));
|
||||
} else {
|
||||
progress_bar.pulse();
|
||||
}
|
||||
},
|
||||
|
||||
// Set the progress bar to a fixed value if one was provided, otherwise pulse.
|
||||
// Set the `error` CSS class so that the progress bar shows as an error color.
|
||||
.@"error" => {
|
||||
progress_bar.as(gtk.Widget).addCssClass("error");
|
||||
if (value.progress) |progress| {
|
||||
progress_bar.setFraction(computeFraction(progress));
|
||||
} else {
|
||||
progress_bar.pulse();
|
||||
}
|
||||
},
|
||||
|
||||
// The state of progress is unknown, so pulse the progress bar to
|
||||
// indicate that things are still happening.
|
||||
.indeterminate => {
|
||||
progress_bar.pulse();
|
||||
},
|
||||
|
||||
// If a progress value was provided, set the progress bar to that value.
|
||||
// Don't pulse the progress bar as that would indicate that things were
|
||||
// happening. Otherwise this is mainly used to keep the progress bar on
|
||||
// screen instead of timing out.
|
||||
.pause => {
|
||||
if (value.progress) |progress| {
|
||||
progress_bar.setFraction(computeFraction(progress));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Compute a fraction [0.0, 1.0] from the supplied progress, which is clamped
|
||||
/// to [0, 100].
|
||||
fn computeFraction(progress: u8) f64 {
|
||||
return @as(f64, @floatFromInt(std.math.clamp(progress, 0, 100))) / 100.0;
|
||||
}
|
||||
|
||||
test "computeFraction" {
|
||||
try std.testing.expectEqual(1.0, computeFraction(100));
|
||||
try std.testing.expectEqual(1.0, computeFraction(255));
|
||||
try std.testing.expectEqual(0.0, computeFraction(0));
|
||||
try std.testing.expectEqual(0.5, computeFraction(50));
|
||||
}
|
||||
|
||||
/// Add a progress bar to our overlay.
|
||||
fn addProgressBar(self: *ProgressBar) *gtk.ProgressBar {
|
||||
if (self.progress_bar) |progress_bar| return progress_bar;
|
||||
|
||||
const progress_bar = gtk.ProgressBar.new();
|
||||
self.progress_bar = progress_bar;
|
||||
|
||||
const progress_bar_widget = progress_bar.as(gtk.Widget);
|
||||
progress_bar_widget.setHalign(.fill);
|
||||
progress_bar_widget.setValign(.start);
|
||||
progress_bar_widget.addCssClass("osd");
|
||||
|
||||
self.surface.overlay.addOverlay(progress_bar_widget);
|
||||
|
||||
return progress_bar;
|
||||
}
|
||||
|
||||
/// Remove the progress bar from our overlay.
|
||||
fn removeProgressBar(self: *ProgressBar) void {
|
||||
if (self.progress_bar) |progress_bar| {
|
||||
const progress_bar_widget = progress_bar.as(gtk.Widget);
|
||||
self.surface.overlay.removeOverlay(progress_bar_widget);
|
||||
self.progress_bar = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Start a timer that will remove the progress bar if the TUI forgets to remove
|
||||
/// it.
|
||||
fn startProgressBarTimer(self: *ProgressBar) void {
|
||||
const progress_bar_timeout_seconds = 15;
|
||||
|
||||
// Remove an old timer that hasn't fired yet.
|
||||
self.stopProgressBarTimer();
|
||||
|
||||
self.progress_bar_timer = glib.timeoutAdd(
|
||||
progress_bar_timeout_seconds * std.time.ms_per_s,
|
||||
handleProgressBarTimeout,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
/// Stop any existing timer for removing the progress bar.
|
||||
fn stopProgressBarTimer(self: *ProgressBar) void {
|
||||
if (self.progress_bar_timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove progress bar timer", .{});
|
||||
}
|
||||
self.progress_bar_timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// The progress bar hasn't been updated by the TUI recently, remove it.
|
||||
fn handleProgressBarTimeout(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ProgressBar = @ptrCast(@alignCast(ud.?));
|
||||
|
||||
self.progress_bar_timer = null;
|
||||
self.removeProgressBar();
|
||||
|
||||
return @intFromBool(glib.SOURCE_REMOVE);
|
||||
}
|
@@ -1,206 +0,0 @@
|
||||
const ResizeOverlay = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const glib = @import("glib");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// local copy of configuration data
|
||||
const DerivedConfig = struct {
|
||||
resize_overlay: configpkg.Config.ResizeOverlay,
|
||||
resize_overlay_position: configpkg.Config.ResizeOverlayPosition,
|
||||
resize_overlay_duration: configpkg.Config.Duration,
|
||||
|
||||
pub fn init(config: *const configpkg.Config) DerivedConfig {
|
||||
return .{
|
||||
.resize_overlay = config.@"resize-overlay",
|
||||
.resize_overlay_position = config.@"resize-overlay-position",
|
||||
.resize_overlay_duration = config.@"resize-overlay-duration",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// the surface that we are attached to
|
||||
surface: *Surface,
|
||||
|
||||
/// a copy of the configuration that we need to operate
|
||||
config: DerivedConfig,
|
||||
|
||||
/// If non-null this is the widget on the overlay that shows the size of the
|
||||
/// surface when it is resized.
|
||||
label: ?*gtk.Label = null,
|
||||
|
||||
/// If non-null this is a timer for dismissing the resize overlay.
|
||||
timer: ?c_uint = null,
|
||||
|
||||
/// If non-null this is a timer for dismissing the resize overlay.
|
||||
idler: ?c_uint = null,
|
||||
|
||||
/// If true, the next resize event will be the first one.
|
||||
first: bool = true,
|
||||
|
||||
/// Initialize the ResizeOverlay. This doesn't do anything more than save a
|
||||
/// pointer to the surface that we are a part of as all of the widget creation
|
||||
/// is done later.
|
||||
pub fn init(self: *ResizeOverlay, surface: *Surface, config: *const configpkg.Config) void {
|
||||
self.* = .{
|
||||
.surface = surface,
|
||||
.config = .init(config),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateConfig(self: *ResizeOverlay, config: *const configpkg.Config) void {
|
||||
self.config = .init(config);
|
||||
}
|
||||
|
||||
/// De-initialize the ResizeOverlay. This removes any pending idlers/timers that
|
||||
/// may not have fired yet.
|
||||
pub fn deinit(self: *ResizeOverlay) void {
|
||||
if (self.idler) |idler| {
|
||||
if (glib.Source.remove(idler) == 0) {
|
||||
log.warn("unable to remove resize overlay idler", .{});
|
||||
}
|
||||
self.idler = null;
|
||||
}
|
||||
|
||||
if (self.timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove resize overlay timer", .{});
|
||||
}
|
||||
self.timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// If we're configured to do so, update the text in the resize overlay widget
|
||||
/// and make it visible. Schedule a timer to hide the widget after the delay
|
||||
/// expires.
|
||||
///
|
||||
/// If we're not configured to show the overlay, do nothing.
|
||||
pub fn maybeShow(self: *ResizeOverlay) void {
|
||||
switch (self.config.resize_overlay) {
|
||||
.never => return,
|
||||
.always => {},
|
||||
.@"after-first" => if (self.first) {
|
||||
self.first = false;
|
||||
return;
|
||||
},
|
||||
}
|
||||
|
||||
self.first = false;
|
||||
|
||||
// When updating a widget, wait until GTK is "idle", i.e. not in the middle
|
||||
// of doing any other updates. Since we are called in the middle of resizing
|
||||
// GTK is doing a lot of work rearranging all of the widgets. Not doing this
|
||||
// results in a lot of warnings from GTK and _horrible_ flickering of the
|
||||
// resize overlay.
|
||||
if (self.idler != null) return;
|
||||
self.idler = glib.idleAdd(gtkUpdate, self);
|
||||
}
|
||||
|
||||
/// Actually update the overlay widget. This should only be called from a GTK
|
||||
/// idle handler.
|
||||
fn gtkUpdate(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||
|
||||
// No matter what our idler is complete with this callback
|
||||
self.idler = null;
|
||||
|
||||
const grid_size = self.surface.core_surface.size.grid();
|
||||
var buf: [32]u8 = undefined;
|
||||
const text = std.fmt.bufPrintZ(
|
||||
&buf,
|
||||
"{d} x {d}",
|
||||
.{
|
||||
grid_size.columns,
|
||||
grid_size.rows,
|
||||
},
|
||||
) catch |err| {
|
||||
log.err("unable to format text: {}", .{err});
|
||||
return 0;
|
||||
};
|
||||
|
||||
if (self.label) |label| {
|
||||
// The resize overlay widget already exists, just update it.
|
||||
label.setText(text.ptr);
|
||||
setPosition(label, &self.config);
|
||||
show(label);
|
||||
} else {
|
||||
// Create the resize overlay widget.
|
||||
const label = gtk.Label.new(text.ptr);
|
||||
label.setJustify(gtk.Justification.center);
|
||||
label.setSelectable(0);
|
||||
setPosition(label, &self.config);
|
||||
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.addCssClass("view");
|
||||
widget.addCssClass("size-overlay");
|
||||
widget.setFocusable(0);
|
||||
widget.setCanTarget(0);
|
||||
|
||||
const overlay: *gtk.Overlay = @ptrCast(@alignCast(self.surface.overlay));
|
||||
overlay.addOverlay(widget);
|
||||
|
||||
self.label = label;
|
||||
}
|
||||
|
||||
if (self.timer) |timer| {
|
||||
if (glib.Source.remove(timer) == 0) {
|
||||
log.warn("unable to remove size overlay timer", .{});
|
||||
}
|
||||
}
|
||||
|
||||
self.timer = glib.timeoutAdd(
|
||||
self.surface.app.config.@"resize-overlay-duration".asMilliseconds(),
|
||||
gtkTimerExpired,
|
||||
self,
|
||||
);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// This should only be called from a GTK idle handler or timer.
|
||||
fn show(label: *gtk.Label) void {
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.removeCssClass("hidden");
|
||||
}
|
||||
|
||||
// This should only be called from a GTK idle handler or timer.
|
||||
fn hide(label: *gtk.Label) void {
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.addCssClass("hidden");
|
||||
}
|
||||
|
||||
/// Update the position of the resize overlay widget. It might seem excessive to
|
||||
/// do this often, but it should make hot config reloading of the position work.
|
||||
/// This should only be called from a GTK idle handler.
|
||||
fn setPosition(label: *gtk.Label, config: *DerivedConfig) void {
|
||||
const widget = label.as(gtk.Widget);
|
||||
widget.setHalign(
|
||||
switch (config.resize_overlay_position) {
|
||||
.center, .@"top-center", .@"bottom-center" => gtk.Align.center,
|
||||
.@"top-left", .@"bottom-left" => gtk.Align.start,
|
||||
.@"top-right", .@"bottom-right" => gtk.Align.end,
|
||||
},
|
||||
);
|
||||
widget.setValign(
|
||||
switch (config.resize_overlay_position) {
|
||||
.center => gtk.Align.center,
|
||||
.@"top-left", .@"top-center", .@"top-right" => gtk.Align.start,
|
||||
.@"bottom-left", .@"bottom-center", .@"bottom-right" => gtk.Align.end,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// If this fires, it means that the delay period has expired and the resize
|
||||
/// overlay widget should be hidden.
|
||||
fn gtkTimerExpired(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *ResizeOverlay = @ptrCast(@alignCast(ud orelse return 0));
|
||||
self.timer = null;
|
||||
if (self.label) |label| hide(label);
|
||||
return 0;
|
||||
}
|
@@ -1,441 +0,0 @@
|
||||
/// Split represents a surface split where two surfaces are shown side-by-side
|
||||
/// within the same window either vertically or horizontally.
|
||||
const Split = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const apprt = @import("../../apprt.zig");
|
||||
const font = @import("../../font/main.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// The split orientation.
|
||||
pub const Orientation = enum {
|
||||
horizontal,
|
||||
vertical,
|
||||
|
||||
pub fn fromDirection(direction: apprt.action.SplitDirection) Orientation {
|
||||
return switch (direction) {
|
||||
.right, .left => .horizontal,
|
||||
.down, .up => .vertical,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromResizeDirection(direction: apprt.action.ResizeSplit.Direction) Orientation {
|
||||
return switch (direction) {
|
||||
.up, .down => .vertical,
|
||||
.left, .right => .horizontal,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Our actual GtkPaned widget
|
||||
paned: *gtk.Paned,
|
||||
|
||||
/// The container for this split panel.
|
||||
container: Surface.Container,
|
||||
|
||||
/// The orientation of this split panel.
|
||||
orientation: Orientation,
|
||||
|
||||
/// The elements of this split panel.
|
||||
top_left: Surface.Container.Elem,
|
||||
bottom_right: Surface.Container.Elem,
|
||||
|
||||
/// Create a new split panel with the given sibling surface in the given
|
||||
/// direction. The direction is where the new surface will be initialized.
|
||||
///
|
||||
/// The sibling surface can be in a split already or it can be within a
|
||||
/// tab. This properly handles updating the surface container so that
|
||||
/// it represents the new split.
|
||||
pub fn create(
|
||||
alloc: Allocator,
|
||||
sibling: *Surface,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !*Split {
|
||||
var split = try alloc.create(Split);
|
||||
errdefer alloc.destroy(split);
|
||||
try split.init(sibling, direction);
|
||||
return split;
|
||||
}
|
||||
|
||||
pub fn init(
|
||||
self: *Split,
|
||||
sibling: *Surface,
|
||||
direction: apprt.action.SplitDirection,
|
||||
) !void {
|
||||
// If our sibling is too small to be split in half then we don't
|
||||
// allow the split to happen. This avoids a situation where the
|
||||
// split becomes too small.
|
||||
//
|
||||
// This is kind of a hack. Ideally we'd use gtk_widget_set_size_request
|
||||
// properly along the path to ensure minimum sizes. I don't know if
|
||||
// GTK even respects that all but any way GTK does this for us seems
|
||||
// better than this.
|
||||
{
|
||||
// This is the min size of the sibling split. This means the
|
||||
// smallest split is half of this.
|
||||
const multiplier = 4;
|
||||
|
||||
const size = &sibling.core_surface.size;
|
||||
const small = switch (direction) {
|
||||
.right, .left => size.screen.width < size.cell.width * multiplier,
|
||||
.down, .up => size.screen.height < size.cell.height * multiplier,
|
||||
};
|
||||
if (small) return error.SplitTooSmall;
|
||||
}
|
||||
|
||||
// Create the new child surface for the other direction.
|
||||
const alloc = sibling.app.core_app.alloc;
|
||||
var surface = try Surface.create(alloc, sibling.app, .{
|
||||
.parent = &sibling.core_surface,
|
||||
});
|
||||
errdefer surface.destroy(alloc);
|
||||
sibling.dimSurface();
|
||||
sibling.setSplitZoom(false);
|
||||
|
||||
// Create the actual GTKPaned, attach the proper children.
|
||||
const orientation: gtk.Orientation = switch (direction) {
|
||||
.right, .left => .horizontal,
|
||||
.down, .up => .vertical,
|
||||
};
|
||||
const paned = gtk.Paned.new(orientation);
|
||||
errdefer paned.unref();
|
||||
|
||||
// Keep a long-lived reference, which we unref in destroy.
|
||||
paned.ref();
|
||||
|
||||
// Update all of our containers to point to the right place.
|
||||
// The split has to point to where the sibling pointed to because
|
||||
// we're inheriting its parent. The sibling points to its location
|
||||
// in the split, and the surface points to the other location.
|
||||
const container = sibling.container;
|
||||
const tl: *Surface, const br: *Surface = switch (direction) {
|
||||
.right, .down => right_down: {
|
||||
sibling.container = .{ .split_tl = &self.top_left };
|
||||
surface.container = .{ .split_br = &self.bottom_right };
|
||||
break :right_down .{ sibling, surface };
|
||||
},
|
||||
|
||||
.left, .up => left_up: {
|
||||
sibling.container = .{ .split_br = &self.bottom_right };
|
||||
surface.container = .{ .split_tl = &self.top_left };
|
||||
break :left_up .{ surface, sibling };
|
||||
},
|
||||
};
|
||||
|
||||
self.* = .{
|
||||
.paned = paned,
|
||||
.container = container,
|
||||
.top_left = .{ .surface = tl },
|
||||
.bottom_right = .{ .surface = br },
|
||||
.orientation = .fromDirection(direction),
|
||||
};
|
||||
|
||||
// Replace the previous containers element with our split. This allows a
|
||||
// non-split to become a split, a split to become a nested split, etc.
|
||||
container.replace(.{ .split = self });
|
||||
|
||||
// Update our children so that our GL area is properly added to the paned.
|
||||
self.updateChildren();
|
||||
|
||||
// The new surface should always grab focus
|
||||
surface.grabFocus();
|
||||
}
|
||||
|
||||
pub fn destroy(self: *Split, alloc: Allocator) void {
|
||||
self.top_left.deinit(alloc);
|
||||
self.bottom_right.deinit(alloc);
|
||||
|
||||
// Clean up our GTK reference. This will trigger all the destroy callbacks
|
||||
// that are necessary for the surfaces to clean up.
|
||||
self.paned.unref();
|
||||
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
/// Remove the top left child.
|
||||
pub fn removeTopLeft(self: *Split) void {
|
||||
self.removeChild(self.top_left, self.bottom_right);
|
||||
}
|
||||
|
||||
/// Remove the top left child.
|
||||
pub fn removeBottomRight(self: *Split) void {
|
||||
self.removeChild(self.bottom_right, self.top_left);
|
||||
}
|
||||
|
||||
fn removeChild(
|
||||
self: *Split,
|
||||
remove: Surface.Container.Elem,
|
||||
keep: Surface.Container.Elem,
|
||||
) void {
|
||||
const window = self.container.window() orelse return;
|
||||
const alloc = window.app.core_app.alloc;
|
||||
|
||||
// Remove our children since we are going to no longer be a split anyways.
|
||||
// This prevents widgets with multiple parents.
|
||||
self.removeChildren();
|
||||
|
||||
// Our container must become whatever our top left is
|
||||
self.container.replace(keep);
|
||||
|
||||
// Grab focus of the left-over side
|
||||
keep.grabFocus();
|
||||
|
||||
// When a child is removed we are no longer a split, so destroy ourself
|
||||
remove.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
/// Move the divider in the given direction by the given amount.
|
||||
pub fn moveDivider(
|
||||
self: *Split,
|
||||
direction: apprt.action.ResizeSplit.Direction,
|
||||
amount: u16,
|
||||
) void {
|
||||
const min_pos = 10;
|
||||
|
||||
const pos = self.paned.getPosition();
|
||||
const new = switch (direction) {
|
||||
.up, .left => @max(pos - amount, min_pos),
|
||||
.down, .right => new_pos: {
|
||||
const max_pos: u16 = @as(u16, @intFromFloat(self.maxPosition())) - min_pos;
|
||||
break :new_pos @min(pos + amount, max_pos);
|
||||
},
|
||||
};
|
||||
|
||||
self.paned.setPosition(new);
|
||||
}
|
||||
|
||||
/// Equalize the splits in this split panel. Each split is equalized based on
|
||||
/// its weight, i.e. the number of Surfaces it contains.
|
||||
///
|
||||
/// It works recursively by equalizing the children of each split.
|
||||
///
|
||||
/// It returns this split's weight.
|
||||
pub fn equalize(self: *Split) f64 {
|
||||
// Calculate weights of top_left/bottom_right
|
||||
const top_left_weight = self.top_left.equalize();
|
||||
const bottom_right_weight = self.bottom_right.equalize();
|
||||
const weight = top_left_weight + bottom_right_weight;
|
||||
|
||||
// Ratio of top_left weight to overall weight, which gives the split ratio
|
||||
const ratio = top_left_weight / weight;
|
||||
|
||||
// Convert split ratio into new position for divider
|
||||
self.paned.setPosition(@intFromFloat(self.maxPosition() * ratio));
|
||||
|
||||
return weight;
|
||||
}
|
||||
|
||||
// maxPosition returns the maximum position of the GtkPaned, which is the
|
||||
// "max-position" attribute.
|
||||
fn maxPosition(self: *Split) f64 {
|
||||
var value: gobject.Value = std.mem.zeroes(gobject.Value);
|
||||
defer value.unset();
|
||||
|
||||
_ = value.init(gobject.ext.types.int);
|
||||
self.paned.as(gobject.Object).getProperty(
|
||||
"max-position",
|
||||
&value,
|
||||
);
|
||||
|
||||
return @floatFromInt(value.getInt());
|
||||
}
|
||||
|
||||
// This replaces the element at the given pointer with a new element.
|
||||
// The ptr must be either top_left or bottom_right (asserted in debug).
|
||||
// The memory of the old element must be freed or otherwise handled by
|
||||
// the caller.
|
||||
pub fn replace(
|
||||
self: *Split,
|
||||
ptr: *Surface.Container.Elem,
|
||||
new: Surface.Container.Elem,
|
||||
) void {
|
||||
// We can write our element directly. There's nothing special.
|
||||
assert(&self.top_left == ptr or &self.bottom_right == ptr);
|
||||
ptr.* = new;
|
||||
|
||||
// Update our paned children. This will reset the divider
|
||||
// position but we want to keep it in place so save and restore it.
|
||||
const pos = self.paned.getPosition();
|
||||
defer self.paned.setPosition(pos);
|
||||
self.updateChildren();
|
||||
}
|
||||
|
||||
// grabFocus grabs the focus of the top-left element.
|
||||
pub fn grabFocus(self: *Split) void {
|
||||
self.top_left.grabFocus();
|
||||
}
|
||||
|
||||
/// Update the paned children to represent the current state.
|
||||
/// This should be called anytime the top/left or bottom/right
|
||||
/// element is changed.
|
||||
pub fn updateChildren(self: *const Split) void {
|
||||
// We have to set both to null. If we overwrite the pane with
|
||||
// the same value, then GTK bugs out (the GL area unrealizes
|
||||
// and never rerealizes).
|
||||
self.removeChildren();
|
||||
|
||||
// Set our current children
|
||||
self.paned.setStartChild(self.top_left.widget());
|
||||
self.paned.setEndChild(self.bottom_right.widget());
|
||||
}
|
||||
|
||||
/// A mapping of direction to the element (if any) in that direction.
|
||||
pub const DirectionMap = std.EnumMap(
|
||||
apprt.action.GotoSplit,
|
||||
?*Surface,
|
||||
);
|
||||
|
||||
pub const Side = enum { top_left, bottom_right };
|
||||
|
||||
/// Returns the map that can be used to determine elements in various
|
||||
/// directions (primarily for gotoSplit).
|
||||
pub fn directionMap(self: *const Split, from: Side) DirectionMap {
|
||||
var result = DirectionMap.initFull(null);
|
||||
|
||||
if (self.directionPrevious(from)) |prev| {
|
||||
result.put(.previous, prev.surface);
|
||||
if (!prev.wrapped) {
|
||||
result.put(.up, prev.surface);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.directionNext(from)) |next| {
|
||||
result.put(.next, next.surface);
|
||||
if (!next.wrapped) {
|
||||
result.put(.down, next.surface);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.directionLeft(from)) |left| {
|
||||
result.put(.left, left);
|
||||
}
|
||||
|
||||
if (self.directionRight(from)) |right| {
|
||||
result.put(.right, right);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn directionLeft(self: *const Split, from: Side) ?*Surface {
|
||||
switch (from) {
|
||||
.bottom_right => {
|
||||
switch (self.orientation) {
|
||||
.horizontal => return self.top_left.deepestSurface(.bottom_right),
|
||||
.vertical => return directionLeft(
|
||||
self.container.split() orelse return null,
|
||||
.bottom_right,
|
||||
),
|
||||
}
|
||||
},
|
||||
.top_left => return directionLeft(
|
||||
self.container.split() orelse return null,
|
||||
.bottom_right,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn directionRight(self: *const Split, from: Side) ?*Surface {
|
||||
switch (from) {
|
||||
.top_left => {
|
||||
switch (self.orientation) {
|
||||
.horizontal => return self.bottom_right.deepestSurface(.top_left),
|
||||
.vertical => return directionRight(
|
||||
self.container.split() orelse return null,
|
||||
.top_left,
|
||||
),
|
||||
}
|
||||
},
|
||||
.bottom_right => return directionRight(
|
||||
self.container.split() orelse return null,
|
||||
.top_left,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn directionPrevious(self: *const Split, from: Side) ?struct {
|
||||
surface: *Surface,
|
||||
wrapped: bool,
|
||||
} {
|
||||
switch (from) {
|
||||
// From the bottom right, our previous is the deepest surface
|
||||
// in the top-left of our own split.
|
||||
.bottom_right => return .{
|
||||
.surface = self.top_left.deepestSurface(.bottom_right) orelse return null,
|
||||
.wrapped = false,
|
||||
},
|
||||
|
||||
// From the top left its more complicated. It is the de
|
||||
.top_left => {
|
||||
// If we have no parent split then there can be no unwrapped prev.
|
||||
// We can still have a wrapped previous.
|
||||
const parent = self.container.split() orelse return .{
|
||||
.surface = self.bottom_right.deepestSurface(.bottom_right) orelse return null,
|
||||
.wrapped = true,
|
||||
};
|
||||
|
||||
// The previous value is the previous of the side that we are.
|
||||
const side = self.container.splitSide() orelse return null;
|
||||
return switch (side) {
|
||||
.top_left => parent.directionPrevious(.top_left),
|
||||
.bottom_right => parent.directionPrevious(.bottom_right),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn directionNext(self: *const Split, from: Side) ?struct {
|
||||
surface: *Surface,
|
||||
wrapped: bool,
|
||||
} {
|
||||
switch (from) {
|
||||
// From the top left, our next is the earliest surface in the
|
||||
// top-left direction of the bottom-right side of our split. Fun!
|
||||
.top_left => return .{
|
||||
.surface = self.bottom_right.deepestSurface(.top_left) orelse return null,
|
||||
.wrapped = false,
|
||||
},
|
||||
|
||||
// From the bottom right is more compliated. It is the deepest
|
||||
// (last) surface in the
|
||||
.bottom_right => {
|
||||
// If we have no parent split then there can be no next.
|
||||
const parent = self.container.split() orelse return .{
|
||||
.surface = self.top_left.deepestSurface(.top_left) orelse return null,
|
||||
.wrapped = true,
|
||||
};
|
||||
|
||||
// The previous value is the previous of the side that we are.
|
||||
const side = self.container.splitSide() orelse return null;
|
||||
return switch (side) {
|
||||
.top_left => parent.directionNext(.top_left),
|
||||
.bottom_right => parent.directionNext(.bottom_right),
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn detachTopLeft(self: *const Split) void {
|
||||
self.paned.setStartChild(null);
|
||||
}
|
||||
|
||||
pub fn detachBottomRight(self: *const Split) void {
|
||||
self.paned.setEndChild(null);
|
||||
}
|
||||
|
||||
fn removeChildren(self: *const Split) void {
|
||||
self.detachTopLeft();
|
||||
self.detachBottomRight();
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,171 +0,0 @@
|
||||
//! The state associated with a single tab in the window.
|
||||
//!
|
||||
//! A tab can contain one or more terminals due to splits.
|
||||
const Tab = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const gobject = @import("gobject");
|
||||
const gtk = @import("gtk");
|
||||
|
||||
const font = @import("../../font/main.zig");
|
||||
const input = @import("../../input.zig");
|
||||
const CoreSurface = @import("../../Surface.zig");
|
||||
|
||||
const Surface = @import("Surface.zig");
|
||||
const Window = @import("Window.zig");
|
||||
const CloseDialog = @import("CloseDialog.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
pub const GHOSTTY_TAB = "ghostty_tab";
|
||||
|
||||
/// The window that owns this tab.
|
||||
window: *Window,
|
||||
|
||||
/// The tab label. The tab label is the text that appears on the tab.
|
||||
label_text: *gtk.Label,
|
||||
|
||||
/// We'll put our children into this box instead of packing them
|
||||
/// directly, so that we can send the box into `c.g_signal_connect_data`
|
||||
/// for the close button
|
||||
box: *gtk.Box,
|
||||
|
||||
/// The element of this tab so that we can handle splits and so on.
|
||||
elem: Surface.Container.Elem,
|
||||
|
||||
// We'll update this every time a Surface gains focus, so that we have it
|
||||
// when we switch to another Tab. Then when we switch back to this tab, we
|
||||
// can easily re-focus that terminal.
|
||||
focus_child: ?*Surface,
|
||||
|
||||
pub fn create(alloc: Allocator, window: *Window, parent_: ?*CoreSurface) !*Tab {
|
||||
var tab = try alloc.create(Tab);
|
||||
errdefer alloc.destroy(tab);
|
||||
try tab.init(window, parent_);
|
||||
return tab;
|
||||
}
|
||||
|
||||
/// Initialize the tab, create a surface, and add it to the window. "self" needs
|
||||
/// to be a stable pointer, since it is used for GTK events.
|
||||
pub fn init(self: *Tab, window: *Window, parent_: ?*CoreSurface) !void {
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.label_text = undefined,
|
||||
.box = undefined,
|
||||
.elem = undefined,
|
||||
.focus_child = null,
|
||||
};
|
||||
|
||||
// Create a Box in which we'll later keep either Surface or Split. Using a
|
||||
// box makes it easier to maintain the tab contents because we never need to
|
||||
// change the root widget of the notebook page (tab).
|
||||
const box = gtk.Box.new(.vertical, 0);
|
||||
errdefer box.unref();
|
||||
const box_widget = box.as(gtk.Widget);
|
||||
box_widget.setHexpand(1);
|
||||
box_widget.setVexpand(1);
|
||||
self.box = box;
|
||||
|
||||
// Create the initial surface since all tabs start as a single non-split
|
||||
var surface = try Surface.create(window.app.core_app.alloc, window.app, .{
|
||||
.parent = parent_,
|
||||
});
|
||||
errdefer surface.unref();
|
||||
surface.container = .{ .tab_ = self };
|
||||
self.elem = .{ .surface = surface };
|
||||
|
||||
// Add Surface to the Tab
|
||||
self.box.append(surface.primaryWidget());
|
||||
|
||||
// Set the userdata of the box to point to this tab.
|
||||
self.box.as(gobject.Object).setData(GHOSTTY_TAB, self);
|
||||
window.notebook.addTab(self, "Ghostty");
|
||||
|
||||
// Attach all events
|
||||
_ = gtk.Widget.signals.destroy.connect(
|
||||
self.box,
|
||||
*Tab,
|
||||
gtkDestroy,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
|
||||
// We need to grab focus after Surface and Tab is added to the window. When
|
||||
// creating a Tab we want to always focus on the widget.
|
||||
surface.grabFocus();
|
||||
}
|
||||
|
||||
/// Deinits tab by deiniting child elem.
|
||||
pub fn deinit(self: *Tab, alloc: Allocator) void {
|
||||
self.elem.deinit(alloc);
|
||||
}
|
||||
|
||||
/// Deinit and deallocate the tab.
|
||||
pub fn destroy(self: *Tab, alloc: Allocator) void {
|
||||
self.deinit(alloc);
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
// TODO: move this
|
||||
/// Replace the surface element that this tab is showing.
|
||||
pub fn replaceElem(self: *Tab, elem: Surface.Container.Elem) void {
|
||||
// Remove our previous widget
|
||||
self.box.remove(self.elem.widget());
|
||||
|
||||
// Add our new one
|
||||
self.box.append(elem.widget());
|
||||
self.elem = elem;
|
||||
}
|
||||
|
||||
pub fn setTitleText(self: *Tab, title: [:0]const u8) void {
|
||||
self.window.notebook.setTabTitle(self, title);
|
||||
}
|
||||
|
||||
pub fn setTooltipText(self: *Tab, tooltip: [:0]const u8) void {
|
||||
self.window.notebook.setTabTooltip(self, tooltip);
|
||||
}
|
||||
|
||||
/// Remove this tab from the window.
|
||||
pub fn remove(self: *Tab) void {
|
||||
self.window.closeTab(self);
|
||||
}
|
||||
|
||||
/// Helper function to check if any surface in the split hierarchy needs close confirmation
|
||||
fn needsConfirm(elem: Surface.Container.Elem) bool {
|
||||
return switch (elem) {
|
||||
.surface => |s| s.core_surface.needsConfirmQuit(),
|
||||
.split => |s| needsConfirm(s.top_left) or needsConfirm(s.bottom_right),
|
||||
};
|
||||
}
|
||||
|
||||
/// Close the tab, asking for confirmation if any surface requests it.
|
||||
pub fn closeWithConfirmation(tab: *Tab) void {
|
||||
switch (tab.elem) {
|
||||
.surface => |s| s.closeWithConfirmation(
|
||||
s.core_surface.needsConfirmQuit(),
|
||||
.{ .tab = tab },
|
||||
),
|
||||
.split => |s| {
|
||||
if (!needsConfirm(s.top_left) and !needsConfirm(s.bottom_right)) {
|
||||
tab.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
CloseDialog.show(.{ .tab = tab }) catch |err| {
|
||||
log.err("failed to open close dialog={}", .{err});
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkDestroy(_: *gtk.Box, self: *Tab) callconv(.c) void {
|
||||
log.debug("tab box destroy", .{});
|
||||
|
||||
const alloc = self.window.app.core_app.alloc;
|
||||
|
||||
// When our box is destroyed, we want to destroy our tab, too.
|
||||
self.destroy(alloc);
|
||||
}
|
@@ -1,284 +0,0 @@
|
||||
/// An abstraction over the Adwaita tab view to manage all the terminal tabs in
|
||||
/// a window.
|
||||
const TabView = @This();
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const gtk = @import("gtk");
|
||||
const adw = @import("adw");
|
||||
const gobject = @import("gobject");
|
||||
const glib = @import("glib");
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const Tab = @import("Tab.zig");
|
||||
const adw_version = @import("adw_version.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
/// our window
|
||||
window: *Window,
|
||||
|
||||
/// the tab view
|
||||
tab_view: *adw.TabView,
|
||||
|
||||
/// Set to true so that the adw close-page handler knows we're forcing
|
||||
/// and to allow a close to happen with no confirm. This is a bit of a hack
|
||||
/// because we currently use GTK alerts to confirm tab close and they
|
||||
/// don't carry with them the ADW state that we are confirming or not.
|
||||
/// Long term we should move to ADW alerts so we can know if we are
|
||||
/// confirming or not.
|
||||
forcing_close: bool = false,
|
||||
|
||||
pub fn init(self: *TabView, window: *Window) void {
|
||||
self.* = .{
|
||||
.window = window,
|
||||
.tab_view = adw.TabView.new(),
|
||||
};
|
||||
self.tab_view.as(gtk.Widget).addCssClass("notebook");
|
||||
|
||||
if (adw_version.atLeast(1, 2, 0)) {
|
||||
// Adwaita enables all of the shortcuts by default.
|
||||
// We want to manage keybindings ourselves.
|
||||
self.tab_view.removeShortcuts(.{
|
||||
.alt_digits = true,
|
||||
.alt_zero = true,
|
||||
.control_end = true,
|
||||
.control_home = true,
|
||||
.control_page_down = true,
|
||||
.control_page_up = true,
|
||||
.control_shift_end = true,
|
||||
.control_shift_home = true,
|
||||
.control_shift_page_down = true,
|
||||
.control_shift_page_up = true,
|
||||
.control_shift_tab = true,
|
||||
.control_tab = true,
|
||||
});
|
||||
}
|
||||
|
||||
_ = adw.TabView.signals.page_attached.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwPageAttached,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = adw.TabView.signals.close_page.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwClosePage,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = adw.TabView.signals.create_window.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwTabViewCreateWindow,
|
||||
self,
|
||||
.{},
|
||||
);
|
||||
_ = gobject.Object.signals.notify.connect(
|
||||
self.tab_view,
|
||||
*TabView,
|
||||
adwSelectPage,
|
||||
self,
|
||||
.{
|
||||
.detail = "selected-page",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn asWidget(self: *TabView) *gtk.Widget {
|
||||
return self.tab_view.as(gtk.Widget);
|
||||
}
|
||||
|
||||
pub fn nPages(self: *TabView) c_int {
|
||||
return self.tab_view.getNPages();
|
||||
}
|
||||
|
||||
/// Returns the index of the currently selected page.
|
||||
/// Returns null if the notebook has no pages.
|
||||
fn currentPage(self: *TabView) ?c_int {
|
||||
const page = self.tab_view.getSelectedPage() orelse return null;
|
||||
return self.tab_view.getPagePosition(page);
|
||||
}
|
||||
|
||||
/// Returns the currently selected tab or null if there are none.
|
||||
pub fn currentTab(self: *TabView) ?*Tab {
|
||||
const page = self.tab_view.getSelectedPage() orelse return null;
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
return @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return null));
|
||||
}
|
||||
|
||||
pub fn gotoNthTab(self: *TabView, position: c_int) bool {
|
||||
const page_to_select = self.tab_view.getNthPage(position);
|
||||
self.tab_view.setSelectedPage(page_to_select);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn getTabPage(self: *TabView, tab: *Tab) ?*adw.TabPage {
|
||||
return self.tab_view.getPage(tab.box.as(gtk.Widget));
|
||||
}
|
||||
|
||||
pub fn getTabPosition(self: *TabView, tab: *Tab) ?c_int {
|
||||
return self.tab_view.getPagePosition(self.getTabPage(tab) orelse return null);
|
||||
}
|
||||
|
||||
pub fn gotoPreviousTab(self: *TabView, tab: *Tab) bool {
|
||||
const page_idx = self.getTabPosition(tab) orelse return false;
|
||||
|
||||
// The next index is the previous or we wrap around.
|
||||
const next_idx = if (page_idx > 0) page_idx - 1 else next_idx: {
|
||||
const max = self.nPages();
|
||||
break :next_idx max -| 1;
|
||||
};
|
||||
|
||||
// Do nothing if we have one tab
|
||||
if (next_idx == page_idx) return false;
|
||||
|
||||
return self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn gotoNextTab(self: *TabView, tab: *Tab) bool {
|
||||
const page_idx = self.getTabPosition(tab) orelse return false;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
const next_idx = if (page_idx < max) page_idx + 1 else 0;
|
||||
if (next_idx == page_idx) return false;
|
||||
|
||||
return self.gotoNthTab(next_idx);
|
||||
}
|
||||
|
||||
pub fn moveTab(self: *TabView, tab: *Tab, position: c_int) void {
|
||||
const page_idx = self.getTabPosition(tab) orelse return;
|
||||
|
||||
const max = self.nPages() -| 1;
|
||||
var new_position: c_int = page_idx + position;
|
||||
|
||||
if (new_position < 0) {
|
||||
new_position = max + new_position + 1;
|
||||
} else if (new_position > max) {
|
||||
new_position = new_position - max - 1;
|
||||
}
|
||||
|
||||
if (new_position == page_idx) return;
|
||||
self.reorderPage(tab, new_position);
|
||||
}
|
||||
|
||||
pub fn reorderPage(self: *TabView, tab: *Tab, position: c_int) void {
|
||||
_ = self.tab_view.reorderPage(self.getTabPage(tab) orelse return, position);
|
||||
}
|
||||
|
||||
pub fn setTabTitle(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||
const page = self.getTabPage(tab) orelse return;
|
||||
page.setTitle(title.ptr);
|
||||
}
|
||||
|
||||
pub fn setTabTooltip(self: *TabView, tab: *Tab, tooltip: [:0]const u8) void {
|
||||
const page = self.getTabPage(tab) orelse return;
|
||||
page.setTooltip(tooltip.ptr);
|
||||
}
|
||||
|
||||
fn newTabInsertPosition(self: *TabView, tab: *Tab) c_int {
|
||||
const numPages = self.nPages();
|
||||
return switch (tab.window.app.config.@"window-new-tab-position") {
|
||||
.current => if (self.currentPage()) |page| page + 1 else numPages,
|
||||
.end => numPages,
|
||||
};
|
||||
}
|
||||
|
||||
/// Adds a new tab with the given title to the notebook.
|
||||
pub fn addTab(self: *TabView, tab: *Tab, title: [:0]const u8) void {
|
||||
const position = self.newTabInsertPosition(tab);
|
||||
const page = self.tab_view.insert(tab.box.as(gtk.Widget), position);
|
||||
self.setTabTitle(tab, title);
|
||||
self.tab_view.setSelectedPage(page);
|
||||
}
|
||||
|
||||
pub fn closeTab(self: *TabView, tab: *Tab) void {
|
||||
// closeTab always expects to close unconditionally so we mark this
|
||||
// as true so that the close_page call below doesn't request
|
||||
// confirmation.
|
||||
self.forcing_close = true;
|
||||
const n = self.nPages();
|
||||
defer {
|
||||
// self becomes invalid if we close the last page because we close
|
||||
// the whole window
|
||||
if (n > 1) self.forcing_close = false;
|
||||
}
|
||||
|
||||
if (self.getTabPage(tab)) |page| self.tab_view.closePage(page);
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
// libadw versions < 1.5.1 leak the final page view
|
||||
// which causes our surface to not properly cleanup. We
|
||||
// unref to force the cleanup. This will trigger a critical
|
||||
// warning from GTK, but I don't know any other workaround.
|
||||
if (!adw_version.atLeast(1, 5, 1)) {
|
||||
tab.box.unref();
|
||||
}
|
||||
|
||||
self.window.close();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn createWindow(window: *Window) !*Window {
|
||||
const new_window = try Window.create(window.app.core_app.alloc, window.app);
|
||||
new_window.present();
|
||||
return new_window;
|
||||
}
|
||||
|
||||
fn adwPageAttached(_: *adw.TabView, page: *adw.TabPage, _: c_int, self: *TabView) callconv(.c) void {
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return));
|
||||
tab.window = self.window;
|
||||
|
||||
self.window.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn adwClosePage(
|
||||
_: *adw.TabView,
|
||||
page: *adw.TabPage,
|
||||
self: *TabView,
|
||||
) callconv(.c) c_int {
|
||||
const child = page.getChild().as(gobject.Object);
|
||||
const tab: *Tab = @ptrCast(@alignCast(child.getData(Tab.GHOSTTY_TAB) orelse return 0));
|
||||
self.tab_view.closePageFinish(page, @intFromBool(self.forcing_close));
|
||||
if (!self.forcing_close) {
|
||||
// We cannot trigger a close directly in here as the page will stay
|
||||
// alive until this handler returns, breaking the assumption where
|
||||
// no pages means they are all destroyed.
|
||||
//
|
||||
// Schedule the close request to happen in the next event cycle.
|
||||
_ = glib.idleAddOnce(glibIdleOnceCloseTab, tab);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *adw.TabView,
|
||||
self: *TabView,
|
||||
) callconv(.c) ?*adw.TabView {
|
||||
const window = createWindow(self.window) catch |err| {
|
||||
log.warn("error creating new window error={}", .{err});
|
||||
return null;
|
||||
};
|
||||
return window.notebook.tab_view;
|
||||
}
|
||||
|
||||
fn adwSelectPage(_: *adw.TabView, _: *gobject.ParamSpec, self: *TabView) callconv(.c) void {
|
||||
const page = self.tab_view.getSelectedPage() orelse return;
|
||||
|
||||
// If the tab was previously marked as needing attention
|
||||
// (e.g. due to a bell character), we now unmark that
|
||||
page.setNeedsAttention(@intFromBool(false));
|
||||
|
||||
const title = page.getTitle();
|
||||
self.window.setTitle(std.mem.span(title));
|
||||
}
|
||||
|
||||
fn glibIdleOnceCloseTab(data: ?*anyopaque) callconv(.c) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(data orelse return));
|
||||
tab.closeWithConfirmation();
|
||||
}
|
@@ -1,115 +0,0 @@
|
||||
//! Represents the URL hover widgets that show the hovered URL.
|
||||
//!
|
||||
//! To explain a bit how this all works since its split across a few places:
|
||||
//! We create a left/right pair of labels. The left label is shown by default,
|
||||
//! and the right label is hidden. When the mouse enters the left label, we
|
||||
//! show the right label. When the mouse leaves the left label, we hide the
|
||||
//! right label.
|
||||
//!
|
||||
//! The hover and styling is done with a combination of GTK event controllers
|
||||
//! and CSS in style.css.
|
||||
const URLWidget = @This();
|
||||
|
||||
const gtk = @import("gtk");
|
||||
|
||||
/// The label that appears on the bottom left.
|
||||
left: *gtk.Label,
|
||||
|
||||
/// The label that appears on the bottom right.
|
||||
right: *gtk.Label,
|
||||
|
||||
pub fn init(
|
||||
/// The overlay that we will attach our labels to.
|
||||
overlay: *gtk.Overlay,
|
||||
/// The URL to display.
|
||||
str: [:0]const u8,
|
||||
) URLWidget {
|
||||
// Create the left
|
||||
const left = left: {
|
||||
const left = gtk.Label.new(str.ptr);
|
||||
left.setEllipsize(.middle);
|
||||
const widget = left.as(gtk.Widget);
|
||||
widget.addCssClass("view");
|
||||
widget.addCssClass("url-overlay");
|
||||
widget.addCssClass("left");
|
||||
widget.setHalign(.start);
|
||||
widget.setValign(.end);
|
||||
break :left left;
|
||||
};
|
||||
|
||||
// Create the right
|
||||
const right = right: {
|
||||
const right = gtk.Label.new(str.ptr);
|
||||
right.setEllipsize(.middle);
|
||||
const widget = right.as(gtk.Widget);
|
||||
widget.addCssClass("hidden");
|
||||
widget.addCssClass("view");
|
||||
widget.addCssClass("url-overlay");
|
||||
widget.addCssClass("right");
|
||||
widget.setHalign(.end);
|
||||
widget.setValign(.end);
|
||||
break :right right;
|
||||
};
|
||||
|
||||
// Setup our mouse hover event controller for the left label.
|
||||
const ec_motion = gtk.EventControllerMotion.new();
|
||||
errdefer ec_motion.unref();
|
||||
|
||||
left.as(gtk.Widget).addController(ec_motion.as(gtk.EventController));
|
||||
|
||||
_ = gtk.EventControllerMotion.signals.enter.connect(
|
||||
ec_motion,
|
||||
*gtk.Label,
|
||||
gtkLeftEnter,
|
||||
right,
|
||||
.{},
|
||||
);
|
||||
_ = gtk.EventControllerMotion.signals.leave.connect(
|
||||
ec_motion,
|
||||
*gtk.Label,
|
||||
gtkLeftLeave,
|
||||
right,
|
||||
.{},
|
||||
);
|
||||
|
||||
// Show it
|
||||
overlay.addOverlay(left.as(gtk.Widget));
|
||||
overlay.addOverlay(right.as(gtk.Widget));
|
||||
|
||||
return .{
|
||||
.left = left,
|
||||
.right = right,
|
||||
};
|
||||
}
|
||||
|
||||
/// Remove our labels from the overlay.
|
||||
pub fn deinit(self: *URLWidget, overlay: *gtk.Overlay) void {
|
||||
overlay.removeOverlay(self.left.as(gtk.Widget));
|
||||
overlay.removeOverlay(self.right.as(gtk.Widget));
|
||||
}
|
||||
|
||||
/// Change the URL that is displayed.
|
||||
pub fn setText(self: *const URLWidget, str: [:0]const u8) void {
|
||||
self.left.setText(str.ptr);
|
||||
self.right.setText(str.ptr);
|
||||
}
|
||||
|
||||
/// Callback for when the mouse enters the left label. That means that we should
|
||||
/// show the right label. CSS will handle hiding the left label.
|
||||
fn gtkLeftEnter(
|
||||
_: *gtk.EventControllerMotion,
|
||||
_: f64,
|
||||
_: f64,
|
||||
right: *gtk.Label,
|
||||
) callconv(.c) void {
|
||||
right.as(gtk.Widget).removeCssClass("hidden");
|
||||
}
|
||||
|
||||
/// Callback for when the mouse leaves the left label. That means that we should
|
||||
/// hide the right label. CSS will handle showing the left label.
|
||||
fn gtkLeftLeave(
|
||||
_: *gtk.EventControllerMotion,
|
||||
right: *gtk.Label,
|
||||
) callconv(.c) void {
|
||||
right.as(gtk.Widget).addCssClass("hidden");
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@@ -1,160 +0,0 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const c = @cImport({
|
||||
@cInclude("adwaita.h");
|
||||
});
|
||||
|
||||
const adwaita_version = std.SemanticVersion{
|
||||
.major = c.ADW_MAJOR_VERSION,
|
||||
.minor = c.ADW_MINOR_VERSION,
|
||||
.patch = c.ADW_MICRO_VERSION,
|
||||
};
|
||||
const required_blueprint_version = std.SemanticVersion{
|
||||
.major = 0,
|
||||
.minor = 16,
|
||||
.patch = 0,
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||
defer _ = debug_allocator.deinit();
|
||||
const alloc = debug_allocator.allocator();
|
||||
|
||||
var it = try std.process.argsWithAllocator(alloc);
|
||||
defer it.deinit();
|
||||
|
||||
_ = it.next();
|
||||
|
||||
const required_adwaita_version = std.SemanticVersion{
|
||||
.major = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMajorVersion, 10),
|
||||
.minor = try std.fmt.parseUnsigned(u8, it.next() orelse return error.NoMinorVersion, 10),
|
||||
.patch = 0,
|
||||
};
|
||||
const output = it.next() orelse return error.NoOutput;
|
||||
const input = it.next() orelse return error.NoInput;
|
||||
|
||||
if (adwaita_version.order(required_adwaita_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`libadwaita` is too old.
|
||||
\\
|
||||
\\Ghostty requires a version {} or newer of `libadwaita` to
|
||||
\\compile this blueprint. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
, .{required_adwaita_version});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
|
||||
{
|
||||
var stdout: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stdout.deinit(alloc);
|
||||
var stderr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stderr.deinit(alloc);
|
||||
|
||||
var blueprint_compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"--version",
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
blueprint_compiler.stdout_behavior = .Pipe;
|
||||
blueprint_compiler.stderr_behavior = .Pipe;
|
||||
try blueprint_compiler.spawn();
|
||||
try blueprint_compiler.collectOutput(
|
||||
alloc,
|
||||
&stdout,
|
||||
&stderr,
|
||||
std.math.maxInt(u16),
|
||||
);
|
||||
const term = blueprint_compiler.wait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) std.process.exit(1);
|
||||
},
|
||||
else => std.process.exit(1),
|
||||
}
|
||||
|
||||
const version = try std.SemanticVersion.parse(std.mem.trim(u8, stdout.items, &std.ascii.whitespace));
|
||||
if (version.order(required_blueprint_version) == .lt) {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` is the wrong version.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var stdout: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stdout.deinit(alloc);
|
||||
var stderr: std.ArrayListUnmanaged(u8) = .empty;
|
||||
defer stderr.deinit(alloc);
|
||||
|
||||
var blueprint_compiler = std.process.Child.init(
|
||||
&.{
|
||||
"blueprint-compiler",
|
||||
"compile",
|
||||
"--output",
|
||||
output,
|
||||
input,
|
||||
},
|
||||
alloc,
|
||||
);
|
||||
blueprint_compiler.stdout_behavior = .Pipe;
|
||||
blueprint_compiler.stderr_behavior = .Pipe;
|
||||
try blueprint_compiler.spawn();
|
||||
try blueprint_compiler.collectOutput(
|
||||
alloc,
|
||||
&stdout,
|
||||
&stderr,
|
||||
std.math.maxInt(u16),
|
||||
);
|
||||
const term = blueprint_compiler.wait() catch |err| switch (err) {
|
||||
error.FileNotFound => {
|
||||
std.debug.print(
|
||||
\\`blueprint-compiler` not found.
|
||||
\\
|
||||
\\Ghostty requires version {} or newer of
|
||||
\\`blueprint-compiler` as a build-time dependency starting
|
||||
\\from version 1.2. Please install it, ensure that it is
|
||||
\\available on your PATH, and then retry building Ghostty.
|
||||
\\
|
||||
, .{required_blueprint_version});
|
||||
std.posix.exit(1);
|
||||
},
|
||||
else => return err,
|
||||
};
|
||||
|
||||
switch (term) {
|
||||
.Exited => |rc| {
|
||||
if (rc != 0) {
|
||||
std.debug.print("{s}", .{stderr.items});
|
||||
std.process.exit(1);
|
||||
}
|
||||
},
|
||||
else => {
|
||||
std.debug.print("{s}", .{stderr.items});
|
||||
std.process.exit(1);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
@@ -14,10 +14,10 @@ pub const app_id = "com.mitchellh.ghostty";
|
||||
/// The path to the Blueprint files. The folder structure is expected to be
|
||||
/// `{version}/{name}.blp` where `version` is the major and minor
|
||||
/// minimum adwaita version.
|
||||
pub const ui_path = "src/apprt/gtk-ng/ui";
|
||||
pub const ui_path = "src/apprt/gtk/ui";
|
||||
|
||||
/// The path to the CSS files.
|
||||
pub const css_path = "src/apprt/gtk-ng/css";
|
||||
pub const css_path = "src/apprt/gtk/css";
|
||||
|
||||
/// The possible icon sizes we'll embed into the gresource file.
|
||||
/// If any size doesn't exist then it will be an error. We could
|
@@ -2,23 +2,34 @@
|
||||
/// each individual surface into its own cgroup.
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
const gobject = @import("gobject");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
const App = @import("App.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_systemd_cgroup);
|
||||
|
||||
pub const Options = struct {
|
||||
memory_high: ?u64 = null,
|
||||
pids_max: ?u64 = null,
|
||||
};
|
||||
|
||||
/// Initialize the cgroup for the app. This will create our
|
||||
/// transient scope, initialize the cgroups we use for the app,
|
||||
/// configure them, and return the cgroup path for the app.
|
||||
pub fn init(app: *App) ![]const u8 {
|
||||
///
|
||||
/// Returns the path of the current cgroup for the app, which is
|
||||
/// allocated with the given allocator.
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
dbus: *gio.DBusConnection,
|
||||
opts: Options,
|
||||
) ![]const u8 {
|
||||
const pid = std.os.linux.getpid();
|
||||
const alloc = app.core_app.alloc;
|
||||
|
||||
// Get our initial cgroup. We need this so we can compare
|
||||
// and detect when we've switched to our transient group.
|
||||
@@ -31,7 +42,7 @@ pub fn init(app: *App) ![]const u8 {
|
||||
// Create our transient scope. If this succeeds then the unit
|
||||
// was created, but we may not have moved into it yet, so we need
|
||||
// to do a dumb busy loop to wait for the move to complete.
|
||||
try createScope(app, pid);
|
||||
try createScope(dbus, pid);
|
||||
const transient = transient: while (true) {
|
||||
const current = try internal_os.cgroup.current(
|
||||
alloc,
|
||||
@@ -67,7 +78,7 @@ pub fn init(app: *App) ![]const u8 {
|
||||
// of "max" because it's a soft limit that can be exceeded and
|
||||
// can be monitored by things like systemd-oomd to kill if needed,
|
||||
// versus an instant hard kill.
|
||||
if (app.config.@"linux-cgroup-memory-limit") |limit| {
|
||||
if (opts.memory_high) |limit| {
|
||||
try internal_os.cgroup.configureLimit(surfaces, .{
|
||||
.memory_high = limit,
|
||||
});
|
||||
@@ -75,7 +86,7 @@ pub fn init(app: *App) ![]const u8 {
|
||||
|
||||
// Configure the "max" pids limit. This is a hard limit and cannot be
|
||||
// exceeded.
|
||||
if (app.config.@"linux-cgroup-processes-limit") |limit| {
|
||||
if (opts.pids_max) |limit| {
|
||||
try internal_os.cgroup.configureLimit(surfaces, .{
|
||||
.pids_max = limit,
|
||||
});
|
||||
@@ -108,15 +119,12 @@ fn enableControllers(alloc: Allocator, cgroup: []const u8) !void {
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a transient systemd scope unit for the current process.
|
||||
///
|
||||
/// On success this will return the name of the transient scope
|
||||
/// cgroup prefix, allocated with the given allocator.
|
||||
fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
|
||||
const gio_app = app.app.as(gio.Application);
|
||||
const connection = gio_app.getDbusConnection() orelse
|
||||
return error.DbusConnectionRequired;
|
||||
|
||||
/// Create a transient systemd scope unit for the current process and
|
||||
/// move our process into it.
|
||||
fn createScope(
|
||||
dbus: *gio.DBusConnection,
|
||||
pid_: std.os.linux.pid_t,
|
||||
) !void {
|
||||
const pid: u32 = @intCast(pid_);
|
||||
|
||||
// The unit name needs to be unique. We use the pid for this.
|
||||
@@ -180,7 +188,7 @@ fn createScope(app: *App, pid_: std.os.linux.pid_t) !void {
|
||||
|
||||
const value = builder.end();
|
||||
|
||||
const reply = connection.callSync(
|
||||
const reply = dbus.callSync(
|
||||
"org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager",
|
||||
|
@@ -116,6 +116,11 @@ pub const Application = extern struct {
|
||||
/// and initialization was successful.
|
||||
transient_cgroup_base: ?[]const u8 = null,
|
||||
|
||||
/// This is set to true so long as we request a window exactly
|
||||
/// once. This prevents quitting the app before we've shown one
|
||||
/// window.
|
||||
requested_window: bool = false,
|
||||
|
||||
/// This is set to false internally when the event loop
|
||||
/// should exit and the application should quit. This must
|
||||
/// only be set by the main loop thread.
|
||||
@@ -218,10 +223,8 @@ pub const Application = extern struct {
|
||||
const single_instance = switch (config.@"gtk-single-instance") {
|
||||
.true => true,
|
||||
.false => false,
|
||||
.desktop => switch (config.@"launched-from".?) {
|
||||
.desktop, .systemd, .dbus => true,
|
||||
.cli => false,
|
||||
},
|
||||
// This should have been resolved to true/false during config loading.
|
||||
.detect => unreachable,
|
||||
};
|
||||
|
||||
// Setup the flags for our application.
|
||||
@@ -413,9 +416,7 @@ pub const Application = extern struct {
|
||||
// This just calls the `activate` signal but its part of the normal startup
|
||||
// routine so we just call it, but only if the config allows it (this allows
|
||||
// for launching Ghostty in the "background" without immediately opening
|
||||
// a window). An initial window will not be immediately created if we were
|
||||
// launched by D-Bus activation or systemd. D-Bus activation will send it's
|
||||
// own `activate` or `new-window` signal later.
|
||||
// a window).
|
||||
//
|
||||
// https://gitlab.gnome.org/GNOME/glib/-/blob/bd2ccc2f69ecfd78ca3f34ab59e42e2b462bad65/gio/gapplication.c#L2302
|
||||
const priv = self.private();
|
||||
@@ -423,15 +424,11 @@ pub const Application = extern struct {
|
||||
// We need to scope any config access because once we run our
|
||||
// event loop, this can change out from underneath us.
|
||||
const config = priv.config.get();
|
||||
if (config.@"initial-window") switch (config.@"launched-from".?) {
|
||||
.desktop, .cli => self.as(gio.Application).activate(),
|
||||
.dbus, .systemd => {},
|
||||
};
|
||||
if (config.@"initial-window") self.as(gio.Application).activate();
|
||||
}
|
||||
|
||||
// If we are NOT the primary instance, then we never want to run.
|
||||
// This means that another instance of the GTK app is running and
|
||||
// our "activate" call above will open a window.
|
||||
// This means that another instance of the GTK app is running.
|
||||
if (self.as(gio.Application).getIsRemote() != 0) {
|
||||
log.debug(
|
||||
"application is remote, exiting run loop after activation",
|
||||
@@ -461,11 +458,24 @@ pub const Application = extern struct {
|
||||
// If the quit timer has expired, quit.
|
||||
if (priv.quit_timer == .expired) break :q true;
|
||||
|
||||
// There's no quit timer running, or it hasn't expired, don't quit.
|
||||
// If we have no windows attached to our app, also quit.
|
||||
if (priv.requested_window and @as(
|
||||
?*glib.List,
|
||||
self.as(gtk.Application).getWindows(),
|
||||
) == null) break :q true;
|
||||
|
||||
// No quit conditions met
|
||||
break :q false;
|
||||
};
|
||||
|
||||
if (must_quit) self.quit();
|
||||
if (must_quit) {
|
||||
// All must quit scenarios do not need confirmation.
|
||||
// Furthermore, must quit scenarios may result in a situation
|
||||
// where its unsafe to even access the app/surface memory
|
||||
// since its in the process of being freed. We must simply
|
||||
// begin our exit immediately.
|
||||
self.quitNow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +498,15 @@ pub const Application = extern struct {
|
||||
const parent: ?*gtk.Widget = parent: {
|
||||
const list = gtk.Window.listToplevels();
|
||||
defer list.free();
|
||||
const focused = list.findCustom(null, findActiveWindow);
|
||||
const focused = @as(?*glib.List, list.findCustom(
|
||||
null,
|
||||
findActiveWindow,
|
||||
)) orelse {
|
||||
// If we have an active surface then we should have
|
||||
// a window available but in the rare case we don't we
|
||||
// should exit so we don't crash.
|
||||
break :parent null;
|
||||
};
|
||||
break :parent @ptrCast(@alignCast(focused.f_data));
|
||||
};
|
||||
|
||||
@@ -713,27 +731,24 @@ pub const Application = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn loadRuntimeCss(
|
||||
self: *Self,
|
||||
) Allocator.Error!void {
|
||||
fn loadRuntimeCss(self: *Self) Allocator.Error!void {
|
||||
const alloc = self.allocator();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .empty;
|
||||
const config = self.private().config.get();
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = try .initCapacity(alloc, 2048);
|
||||
defer buf.deinit(alloc);
|
||||
|
||||
const writer = buf.writer(alloc);
|
||||
|
||||
const config = self.private().config.get();
|
||||
const window_theme = config.@"window-theme";
|
||||
const unfocused_fill: CoreConfig.Color = config.@"unfocused-split-fill" orelse config.background;
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
try writer.print(
|
||||
\\widget.unfocused-split {{
|
||||
\\ opacity: {d:.2};
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
\\
|
||||
, .{
|
||||
1.0 - config.@"unfocused-split-opacity",
|
||||
unfocused_fill.r,
|
||||
@@ -747,6 +762,7 @@ pub const Application = extern struct {
|
||||
\\ color: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\ background: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\}}
|
||||
\\
|
||||
, .{
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
@@ -759,9 +775,129 @@ pub const Application = extern struct {
|
||||
\\.window headerbar {{
|
||||
\\ font-family: "{[font_family]s}";
|
||||
\\}}
|
||||
\\
|
||||
, .{ .font_family = font_family });
|
||||
}
|
||||
|
||||
try loadRuntimeCss414(config, &writer);
|
||||
try loadRuntimeCss416(config, &writer);
|
||||
|
||||
// ensure that we have a sentinel
|
||||
try writer.writeByte(0);
|
||||
|
||||
const data = buf.items[0 .. buf.items.len - 1 :0];
|
||||
|
||||
log.debug("runtime CSS is {d} bytes", .{data.len + 1});
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
loadCssProviderFromData(
|
||||
self.private().css_provider,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
/// Load runtime CSS for older than GTK 4.16
|
||||
fn loadRuntimeCss414(
|
||||
config: *const CoreConfig,
|
||||
writer: *const std.ArrayListUnmanaged(u8).Writer,
|
||||
) Allocator.Error!void {
|
||||
if (gtk_version.runtimeAtLeast(4, 16, 0)) return;
|
||||
|
||||
const window_theme = config.@"window-theme";
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
\\windowhandle {{
|
||||
\\ background-color: rgb({d},{d},{d});
|
||||
\\ color: rgb({d},{d},{d});
|
||||
\\}}
|
||||
\\windowhandle:backdrop {{
|
||||
\\ background-color: oklab(from rgb({d},{d},{d}) calc(l * 0.9) a b / alpha);
|
||||
\\}}
|
||||
\\
|
||||
, .{
|
||||
headerbar_background.r,
|
||||
headerbar_background.g,
|
||||
headerbar_background.b,
|
||||
headerbar_foreground.r,
|
||||
headerbar_foreground.g,
|
||||
headerbar_foreground.b,
|
||||
headerbar_background.r,
|
||||
headerbar_background.g,
|
||||
headerbar_background.b,
|
||||
}),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Load runtime for GTK 4.16 and newer
|
||||
fn loadRuntimeCss416(
|
||||
config: *const CoreConfig,
|
||||
writer: *const std.ArrayListUnmanaged(u8).Writer,
|
||||
) Allocator.Error!void {
|
||||
if (gtk_version.runtimeUntil(4, 16, 0)) return;
|
||||
|
||||
const window_theme = config.@"window-theme";
|
||||
const headerbar_background = config.@"window-titlebar-background" orelse config.background;
|
||||
const headerbar_foreground = config.@"window-titlebar-foreground" orelse config.foreground;
|
||||
|
||||
try writer.writeAll(
|
||||
\\/*
|
||||
\\ * Child Exited Overlay
|
||||
\\ */
|
||||
\\
|
||||
\\.child-exited.normal revealer widget {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--success-bg-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\.child-exited.abnormal revealer widget {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--error-bg-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\/*
|
||||
\\ * Surface
|
||||
\\ */
|
||||
\\
|
||||
\\.surface progressbar.error trough progress {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--error-bg-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\.surface .bell-overlay {
|
||||
\\ border-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--accent-color),
|
||||
\\ transparent 50%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
\\/*
|
||||
\\ * Splits
|
||||
\\ */
|
||||
\\
|
||||
\\.window .split paned > separator {
|
||||
\\ background-color: color-mix(
|
||||
\\ in srgb,
|
||||
\\ var(--window-bg-color),
|
||||
\\ transparent 0%
|
||||
\\ );
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
\\:root {{
|
||||
@@ -794,15 +930,6 @@ pub const Application = extern struct {
|
||||
}),
|
||||
else => {},
|
||||
}
|
||||
|
||||
const data = try alloc.dupeZ(u8, buf.items);
|
||||
defer alloc.free(data);
|
||||
|
||||
// Clears any previously loaded CSS from this provider
|
||||
loadCssProviderFromData(
|
||||
self.private().css_provider,
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
fn loadCustomCss(self: *Self) !void {
|
||||
@@ -1858,6 +1985,13 @@ const Action = struct {
|
||||
self: *Application,
|
||||
parent: ?*CoreSurface,
|
||||
) !void {
|
||||
// Note that we've requested a window at least once. This is used
|
||||
// to trigger quit on no windows. Note I'm not sure if this is REALLY
|
||||
// necessary, but I don't want to risk a bug where on a slow machine
|
||||
// or something we quit immediately after starting up because there
|
||||
// was a delay in the event loop before we created a Window.
|
||||
self.private().requested_window = true;
|
||||
|
||||
const win = Window.new(self);
|
||||
initAndShowWindow(self, win, parent);
|
||||
}
|
@@ -10,7 +10,7 @@ const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Dialog = @import("dialog.zig").Dialog;
|
||||
|
||||
const log = std.log.scoped(.gtk_ghostty_config_errors_dialog);
|
||||
const log = std.log.scoped(.gtk_ghostty_close_confirmation_dialog);
|
||||
|
||||
pub const CloseConfirmationDialog = extern struct {
|
||||
const Self = @This();
|
@@ -105,6 +105,24 @@ pub const Surface = extern struct {
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"error" = struct {
|
||||
pub const name = "error";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
name,
|
||||
Self,
|
||||
bool,
|
||||
.{
|
||||
.default = false,
|
||||
.accessor = gobject.ext.privateFieldAccessor(
|
||||
Self,
|
||||
Private,
|
||||
&Private.offset,
|
||||
"error",
|
||||
),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
pub const @"font-size-request" = struct {
|
||||
pub const name = "font-size-request";
|
||||
const impl = gobject.ext.defineProperty(
|
||||
@@ -472,6 +490,15 @@ pub const Surface = extern struct {
|
||||
// false by a parent widget.
|
||||
bell_ringing: bool = false,
|
||||
|
||||
/// True if this surface is in an error state. This is currently
|
||||
/// a simple boolean with no additional information on WHAT the
|
||||
/// error state is, because we don't yet need it or use it. For now,
|
||||
/// if this is true, then it means the terminal is non-functional.
|
||||
@"error": bool = false,
|
||||
|
||||
/// The source that handles setting our child property.
|
||||
idle_rechild: ?c_uint = null,
|
||||
|
||||
/// A weak reference to an inspector window.
|
||||
inspector: ?*InspectorWindow = null,
|
||||
|
||||
@@ -480,6 +507,8 @@ pub const Surface = extern struct {
|
||||
context_menu: *gtk.PopoverMenu,
|
||||
drop_target: *gtk.DropTarget,
|
||||
progress_bar_overlay: *gtk.ProgressBar,
|
||||
error_page: *adw.StatusPage,
|
||||
terminal_page: *gtk.Overlay,
|
||||
|
||||
pub var offset: c_int = 0;
|
||||
};
|
||||
@@ -1335,6 +1364,19 @@ pub const Surface = extern struct {
|
||||
priv.progress_bar_timer = null;
|
||||
}
|
||||
|
||||
if (priv.idle_rechild) |v| {
|
||||
if (glib.Source.remove(v) == 0) {
|
||||
log.warn("unable to remove idle source", .{});
|
||||
}
|
||||
priv.idle_rechild = null;
|
||||
}
|
||||
|
||||
// This works around a GTK double-free bug where if you bind
|
||||
// to a top-level template child, it frees twice if the widget is
|
||||
// also the root child of the template. By unsetting the child here,
|
||||
// we avoid the double-free.
|
||||
self.as(adw.Bin).setChild(null);
|
||||
|
||||
gtk.Widget.disposeTemplate(
|
||||
self.as(gtk.Widget),
|
||||
getGObjectType(),
|
||||
@@ -1540,6 +1582,12 @@ pub const Surface = extern struct {
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"bell-ringing".impl.param_spec);
|
||||
}
|
||||
|
||||
pub fn setError(self: *Self, v: bool) void {
|
||||
const priv = self.private();
|
||||
priv.@"error" = v;
|
||||
self.as(gobject.Object).notifyByPspec(properties.@"error".impl.param_spec);
|
||||
}
|
||||
|
||||
fn propConfig(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1592,6 +1640,46 @@ pub const Surface = extern struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn propError(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.c) void {
|
||||
const priv = self.private();
|
||||
if (priv.@"error") {
|
||||
// Ensure we have an opaque background. The window will NOT set
|
||||
// this if we have transparency set and we need an opaque
|
||||
// background for the error message to be readable.
|
||||
self.as(gtk.Widget).addCssClass("background");
|
||||
} else {
|
||||
// Regardless of transparency setting, we remove the background
|
||||
// CSS class from this widget. Parent widgets will set it
|
||||
// appropriately (see window.zig for example).
|
||||
self.as(gtk.Widget).removeCssClass("background");
|
||||
}
|
||||
|
||||
// We need to set our child property on an idle tick, because the
|
||||
// error property can be triggered by signals that are in the middle
|
||||
// of widget mapping and changing our child during that time
|
||||
// results in a hard gtk crash.
|
||||
if (priv.idle_rechild == null) priv.idle_rechild = glib.idleAdd(
|
||||
onIdleRechild,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
fn onIdleRechild(ud: ?*anyopaque) callconv(.c) c_int {
|
||||
const self: *Self = @ptrCast(@alignCast(ud orelse return 0));
|
||||
const priv = self.private();
|
||||
priv.idle_rechild = null;
|
||||
if (priv.@"error") {
|
||||
self.as(adw.Bin).setChild(priv.error_page.as(gtk.Widget));
|
||||
} else {
|
||||
self.as(adw.Bin).setChild(priv.terminal_page.as(gtk.Widget));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
fn propMouseHoverUrl(
|
||||
self: *Self,
|
||||
_: *gobject.ParamSpec,
|
||||
@@ -1942,8 +2030,11 @@ pub const Surface = extern struct {
|
||||
// Bell stops ringing if any mouse button is pressed.
|
||||
self.setBellRinging(false);
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
// Get our surface. If we don't have one, ignore this.
|
||||
const priv = self.private();
|
||||
const core_surface = priv.core_surface orelse return;
|
||||
|
||||
// If we don't have focus, grab it.
|
||||
const gl_area_widget = priv.gl_area.as(gtk.Widget);
|
||||
if (gl_area_widget.hasFocus() == 0) {
|
||||
_ = gl_area_widget.grabFocus();
|
||||
@@ -1951,10 +2042,10 @@ pub const Surface = extern struct {
|
||||
|
||||
// Report the event
|
||||
const button = translateMouseButton(gesture.as(gtk.GestureSingle).getCurrentButton());
|
||||
const consumed = if (priv.core_surface) |surface| consumed: {
|
||||
const consumed = consumed: {
|
||||
const gtk_mods = event.getModifierState();
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
break :consumed surface.mouseButtonCallback(
|
||||
break :consumed core_surface.mouseButtonCallback(
|
||||
.press,
|
||||
button,
|
||||
mods,
|
||||
@@ -1962,7 +2053,7 @@ pub const Surface = extern struct {
|
||||
log.warn("error in key callback err={}", .{err});
|
||||
break :err false;
|
||||
};
|
||||
} else false;
|
||||
};
|
||||
|
||||
// If a right click isn't consumed, mouseButtonCallback selects the hovered
|
||||
// word and returns false. We can use this to handle the context menu
|
||||
@@ -2303,21 +2394,23 @@ pub const Surface = extern struct {
|
||||
) callconv(.c) void {
|
||||
log.debug("realize", .{});
|
||||
|
||||
// Make the GL area current so we can detect any OpenGL errors. If
|
||||
// we have errors here we can't render and we switch to the error
|
||||
// state.
|
||||
const priv = self.private();
|
||||
priv.gl_area.makeCurrent();
|
||||
if (priv.gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is almost always due to a library, driver, or GTK issue", .{});
|
||||
log.warn("this is a common cause of this issue: https://ghostty.org/docs/help/gtk-opengl-context", .{});
|
||||
self.setError(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have an initialized surface then we notify it.
|
||||
// If we don't, we'll initialize it on the first resize so we have
|
||||
// our proper initial dimensions.
|
||||
const priv = self.private();
|
||||
if (priv.core_surface) |v| realize: {
|
||||
// We need to make the context current so we can call GL functions.
|
||||
// This is required for all surface operations.
|
||||
priv.gl_area.makeCurrent();
|
||||
if (priv.gl_area.getError()) |err| {
|
||||
log.warn("failed to make GL context current: {s}", .{err.f_message orelse "(no message)"});
|
||||
log.warn("this error is usually due to a driver or gtk bug", .{});
|
||||
log.warn("this is a common cause of this issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/4950", .{});
|
||||
break :realize;
|
||||
}
|
||||
|
||||
v.renderer.displayRealized() catch |err| {
|
||||
log.warn("core displayRealized failed err={}", .{err});
|
||||
break :realize;
|
||||
@@ -2631,8 +2724,10 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateChildPrivate("url_right", .{});
|
||||
class.bindTemplateChildPrivate("child_exited_overlay", .{});
|
||||
class.bindTemplateChildPrivate("context_menu", .{});
|
||||
class.bindTemplateChildPrivate("error_page", .{});
|
||||
class.bindTemplateChildPrivate("progress_bar_overlay", .{});
|
||||
class.bindTemplateChildPrivate("resize_overlay", .{});
|
||||
class.bindTemplateChildPrivate("terminal_page", .{});
|
||||
class.bindTemplateChildPrivate("drop_target", .{});
|
||||
class.bindTemplateChildPrivate("im_context", .{});
|
||||
|
||||
@@ -2662,6 +2757,7 @@ pub const Surface = extern struct {
|
||||
class.bindTemplateCallback("child_exited_close", &childExitedClose);
|
||||
class.bindTemplateCallback("context_menu_closed", &contextMenuClosed);
|
||||
class.bindTemplateCallback("notify_config", &propConfig);
|
||||
class.bindTemplateCallback("notify_error", &propError);
|
||||
class.bindTemplateCallback("notify_mouse_hover_url", &propMouseHoverUrl);
|
||||
class.bindTemplateCallback("notify_mouse_hidden", &propMouseHidden);
|
||||
class.bindTemplateCallback("notify_mouse_shape", &propMouseShape);
|
||||
@@ -2674,6 +2770,7 @@ pub const Surface = extern struct {
|
||||
properties.config.impl,
|
||||
properties.@"child-exited".impl,
|
||||
properties.@"default-size".impl,
|
||||
properties.@"error".impl,
|
||||
properties.@"font-size-request".impl,
|
||||
properties.focused.impl,
|
||||
properties.@"min-size".impl,
|
@@ -18,7 +18,6 @@ const gresource = @import("../build/gresource.zig");
|
||||
const Common = @import("../class.zig").Common;
|
||||
const Config = @import("config.zig").Config;
|
||||
const Application = @import("application.zig").Application;
|
||||
const CloseConfirmationDialog = @import("close_confirmation_dialog.zig").CloseConfirmationDialog;
|
||||
const SplitTree = @import("split_tree.zig").SplitTree;
|
||||
const Surface = @import("surface.zig").Surface;
|
||||
|
||||
@@ -353,7 +352,7 @@ pub const Tab = extern struct {
|
||||
// a close-page signal that the parent can intercept.
|
||||
switch (mode) {
|
||||
.this => tab_view.closePage(page),
|
||||
.other => log.warn("close-tab:other is not implemented", .{}),
|
||||
.other => tab_view.closeOtherPages(page),
|
||||
}
|
||||
}
|
||||
|
@@ -306,6 +306,13 @@ pub const Window = extern struct {
|
||||
const config = config_obj.get();
|
||||
if (config.maximize) self.as(gtk.Window).maximize();
|
||||
if (config.fullscreen) self.as(gtk.Window).fullscreen();
|
||||
|
||||
// If we have an explicit title set, we set that immediately
|
||||
// so that any applications inspecting the window states see
|
||||
// an immediate title set when the window appears, rather than
|
||||
// waiting possibly a few event loop ticks for it to sync from
|
||||
// the surface.
|
||||
if (config.title) |v| self.as(gtk.Window).setTitle(v);
|
||||
}
|
||||
|
||||
// We always sync our appearance at the end because loading our
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user