mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-13 23:36:09 +03:00
Merge branch 'main' into cleanup-action-binding-docs
This commit is contained in:
25
.github/workflows/nix.yml
vendored
25
.github/workflows/nix.yml
vendored
@ -1,6 +1,31 @@
|
||||
on: [push, pull_request]
|
||||
name: Nix
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Nix"
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs:
|
||||
- check-zig-cache-hash
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
run: |
|
||||
results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
|
||||
if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
|
||||
result="failed"
|
||||
else
|
||||
result="success"
|
||||
fi
|
||||
{
|
||||
echo "result=${result}"
|
||||
echo "results=${results}"
|
||||
} | tee -a "$GITHUB_OUTPUT"
|
||||
- if: always() && steps.status.outputs.result != 'success'
|
||||
name: Check for failed status
|
||||
run: |
|
||||
echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
|
||||
exit 1
|
||||
|
||||
check-zig-cache-hash:
|
||||
if: github.repository == 'ghostty-org/ghostty'
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
|
49
.github/workflows/test.yml
vendored
49
.github/workflows/test.yml
vendored
@ -6,6 +6,45 @@ on:
|
||||
name: Test
|
||||
|
||||
jobs:
|
||||
required:
|
||||
name: "Required Checks: Test"
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs:
|
||||
- build
|
||||
- build-bench
|
||||
- build-linux-libghostty
|
||||
- build-nix
|
||||
- build-macos
|
||||
- build-macos-matrix
|
||||
- build-windows
|
||||
- test
|
||||
- test-gtk
|
||||
- test-sentry-linux
|
||||
- test-macos
|
||||
- prettier
|
||||
- alejandra
|
||||
- typos
|
||||
- test-pkg-linux
|
||||
steps:
|
||||
- id: status
|
||||
name: Determine status
|
||||
run: |
|
||||
results=$(tr -d '\n' <<< '${{ toJSON(needs.*.result) }}')
|
||||
if ! grep -q -v -E '(failure|cancelled)' <<< "$results"; then
|
||||
result="failed"
|
||||
else
|
||||
result="success"
|
||||
fi
|
||||
{
|
||||
echo "result=${result}"
|
||||
echo "results=${results}"
|
||||
} | tee -a "$GITHUB_OUTPUT"
|
||||
- if: always() && steps.status.outputs.result != 'success'
|
||||
name: Check for failed status
|
||||
run: |
|
||||
echo "One or more required build workflows failed: ${{ steps.status.outputs.results }}"
|
||||
exit 1
|
||||
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -247,10 +286,10 @@ jobs:
|
||||
run: |
|
||||
# Get the zig version from build.zig so that it only needs to be updated
|
||||
$fileContent = Get-Content -Path "build.zig" -Raw
|
||||
$pattern = 'const required_zig = "(.*?)";'
|
||||
$pattern = 'buildpkg\.requireZig\("(.*?)"\);'
|
||||
$zigVersion = [regex]::Match($fileContent, $pattern).Groups[1].Value
|
||||
Write-Output $version
|
||||
$version = "zig-windows-x86_64-$zigVersion"
|
||||
Write-Output $version
|
||||
$uri = "https://ziglang.org/download/$zigVersion/$version.zip"
|
||||
Invoke-WebRequest -Uri "$uri" -OutFile ".\zig-windows.zip"
|
||||
Expand-Archive -Path ".\zig-windows.zip" -DestinationPath ".\" -Force
|
||||
@ -342,7 +381,8 @@ jobs:
|
||||
matrix:
|
||||
adwaita: ["true", "false"]
|
||||
x11: ["true", "false"]
|
||||
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }}
|
||||
wayland: ["true", "false"]
|
||||
name: GTK adwaita=${{ matrix.adwaita }} x11=${{ matrix.x11 }} wayland=${{ matrix.wayland }}
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
@ -374,7 +414,8 @@ jobs:
|
||||
zig build \
|
||||
-Dapp-runtime=gtk \
|
||||
-Dgtk-adwaita=${{ matrix.adwaita }} \
|
||||
-Dgtk-x11=${{ matrix.x11 }}
|
||||
-Dgtk-x11=${{ matrix.x11 }} \
|
||||
-Dgtk-wayland=${{ matrix.wayland }}
|
||||
|
||||
test-sentry-linux:
|
||||
strategy:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -17,3 +17,4 @@ test/cases/**/*.actual.png
|
||||
glad.zip
|
||||
/Box_test.ppm
|
||||
/Box_test_diff.ppm
|
||||
/ghostty.qcow2
|
||||
|
@ -77,3 +77,100 @@ 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.
|
||||
|
||||
## 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. The 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.
|
||||
|
@ -5,14 +5,22 @@
|
||||
.dependencies = .{
|
||||
// Zig libs
|
||||
.libxev = .{
|
||||
.url = "https://github.com/mitchellh/libxev/archive/db6a52bafadf00360e675fefa7926e8e6c0e9931.tar.gz",
|
||||
.hash = "12206029de146b685739f69b10a6f08baee86b3d0a5f9a659fa2b2b66c9602078bbf",
|
||||
.url = "https://github.com/mitchellh/libxev/archive/31eed4e337fed7b0149319e5cdbb62b848c24fbd.tar.gz",
|
||||
.hash = "1220ebf88622c4d502dc59e71347e4d28c47e033f11b59aff774ae5787565c40999c",
|
||||
},
|
||||
.mach_glfw = .{
|
||||
.url = "https://github.com/mitchellh/mach-glfw/archive/37c2995f31abcf7e8378fba68ddcf4a3faa02de0.tar.gz",
|
||||
.hash = "12206ed982e709e565d536ce930701a8c07edfd2cfdce428683f3f2a601d37696a62",
|
||||
.lazy = true,
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
|
||||
},
|
||||
.z2d = .{
|
||||
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
||||
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
||||
},
|
||||
.zig_objc = .{
|
||||
.url = "https://github.com/mitchellh/zig-objc/archive/9b8ba849b0f58fe207ecd6ab7c147af55b17556e.tar.gz",
|
||||
.hash = "1220e17e64ef0ef561b3e4b9f3a96a2494285f2ec31c097721bf8c8677ec4415c634",
|
||||
@ -25,6 +33,14 @@
|
||||
.url = "https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.tar.gz",
|
||||
.hash = "12207831bce7d4abce57b5a98e8f3635811cfefd160bca022eb91fe905d36a02cf25",
|
||||
},
|
||||
.zig_wayland = .{
|
||||
.url = "https://codeberg.org/ifreund/zig-wayland/archive/fbfe3b4ac0b472a27b1f1a67405436c58cbee12d.tar.gz",
|
||||
.hash = "12209ca054cb1919fa276e328967f10b253f7537c4136eb48f3332b0f7cf661cad38",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
|
||||
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
|
||||
},
|
||||
|
||||
// C libs
|
||||
.cimgui = .{ .path = "./pkg/cimgui" },
|
||||
@ -46,23 +62,25 @@
|
||||
.glslang = .{ .path = "./pkg/glslang" },
|
||||
.spirv_cross = .{ .path = "./pkg/spirv-cross" },
|
||||
|
||||
// Wayland
|
||||
.wayland = .{
|
||||
.url = "https://deps.files.ghostty.org/wayland-9cb3d7aa9dc995ffafdbdef7ab86a949d0fb0e7d.tar.gz",
|
||||
.hash = "12202cdac858abc52413a6c6711d5026d2d3c8e13f95ca2c327eade0736298bb021f",
|
||||
},
|
||||
.wayland_protocols = .{
|
||||
.url = "https://deps.files.ghostty.org/wayland-protocols-258d8f88f2c8c25a830c6316f87d23ce1a0f12d9.tar.gz",
|
||||
.hash = "12201a57c6ce0001aa034fa80fba3e1cd2253c560a45748f4f4dd21ff23b491cddef",
|
||||
},
|
||||
.plasma_wayland_protocols = .{
|
||||
.url = "git+https://github.com/KDE/plasma-wayland-protocols?ref=main#db525e8f9da548cffa2ac77618dd0fbe7f511b86",
|
||||
.hash = "12207e0851c12acdeee0991e893e0132fc87bb763969a585dc16ecca33e88334c566",
|
||||
},
|
||||
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e030599a6a6e19fcd1ea047c7714021170129d56.tar.gz",
|
||||
.hash = "1220cc25b537556a42b0948437c791214c229efb78b551c80b1e9b18d70bf0498620",
|
||||
},
|
||||
.vaxis = .{
|
||||
.url = "git+https://github.com/rockorager/libvaxis/?ref=main#6d729a2dc3b934818dffe06d2ba3ce02841ed74b",
|
||||
.hash = "12200df4ebeaed45de26cb2c9f3b6f3746d8013b604e035dae658f86f586c8c91d2f",
|
||||
},
|
||||
.zf = .{
|
||||
.url = "git+https://github.com/natecraddock/zf/?ref=main#ed99ca18b02dda052e20ba467e90b623c04690dd",
|
||||
.hash = "1220edc3b8d8bedbb50555947987e5e8e2f93871ca3c8e8d4cc8f1377c15b5dd35e8",
|
||||
},
|
||||
.z2d = .{
|
||||
.url = "git+https://github.com/vancluever/z2d?ref=v0.4.0#4638bb02a9dc41cc2fb811f092811f6a951c752a",
|
||||
.hash = "12201f0d542e7541cf492a001d4d0d0155c92f58212fbcb0d224e95edeba06b5416a",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/0e23daf59234fc892cba949562d7bf69204594bb.tar.gz",
|
||||
.hash = "12204fc99743d8232e691ac22e058519bfc6ea92de4a11c6dba59b117531c847cd6a",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
//! Reverse Index (RI) - ESC M
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\nC", .{});
|
||||
try stdout.print("\x1BM", .{});
|
||||
try stdout.print("D\n\n", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
//! Reverse Index (RI) - ESC M
|
||||
//! Case: test that if the cursor is at the top, it scrolls down.
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\n\n", .{});
|
||||
|
||||
try stdout.print("\x1B[H", .{}); // Top-left
|
||||
try stdout.print("\x1BM", .{}); // Reverse-Index
|
||||
try stdout.print("D", .{});
|
||||
|
||||
try stdout.print("\x0D", .{}); // CR
|
||||
try stdout.print("\x0A", .{}); // LF
|
||||
try stdout.print("\x1B[H", .{}); // Top-left
|
||||
try stdout.print("\x1BM", .{}); // Reverse-Index
|
||||
try stdout.print("E", .{});
|
||||
|
||||
try stdout.print("\n", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@ -1,99 +0,0 @@
|
||||
//! Outputs various box glyphs for testing.
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
|
||||
// Box Drawing
|
||||
{
|
||||
try stdout.print("\x1b[4mBox Drawing\x1b[0m\n", .{});
|
||||
var i: usize = 0x2500;
|
||||
const step: usize = 32;
|
||||
while (i <= 0x257F) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
// Block Elements
|
||||
{
|
||||
try stdout.print("\x1b[4mBlock Elements\x1b[0m\n", .{});
|
||||
var i: usize = 0x2580;
|
||||
const step: usize = 32;
|
||||
while (i <= 0x259f) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
// Braille Elements
|
||||
{
|
||||
try stdout.print("\x1b[4mBraille\x1b[0m\n", .{});
|
||||
var i: usize = 0x2800;
|
||||
const step: usize = 32;
|
||||
while (i <= 0x28FF) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
try stdout.print("{u} ", .{@as(u21, @intCast(i + j))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
try stdout.print("\x1b[4mSextants\x1b[0m\n", .{});
|
||||
var i: usize = 0x1FB00;
|
||||
const step: usize = 32;
|
||||
const end = 0x1FB3B;
|
||||
while (i <= end) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
const v = i + j;
|
||||
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
try stdout.print("\x1b[4mWedge Triangles\x1b[0m\n", .{});
|
||||
var i: usize = 0x1FB3C;
|
||||
const step: usize = 32;
|
||||
const end = 0x1FB6B;
|
||||
while (i <= end) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
const v = i + j;
|
||||
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
try stdout.print("\x1b[4mOther\x1b[0m\n", .{});
|
||||
var i: usize = 0x1FB70;
|
||||
const step: usize = 32;
|
||||
const end = 0x1FB8B;
|
||||
while (i <= end) : (i += step) {
|
||||
var j: usize = 0;
|
||||
while (j < step) : (j += 1) {
|
||||
const v = i + j;
|
||||
if (v <= end) try stdout.print("{u} ", .{@as(u21, @intCast(v))});
|
||||
}
|
||||
|
||||
try stdout.print("\n\n", .{});
|
||||
}
|
||||
}
|
||||
}
|
@ -1,15 +0,0 @@
|
||||
//! Set Top and Bottom Margins (DECSTBM) - ESC [ r
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\nC\nD", .{});
|
||||
try stdout.print("\x1B[1;3r", .{}); // cursor up
|
||||
try stdout.print("\x1B[1;1H", .{}); // top-left
|
||||
try stdout.print("\x1B[M", .{}); // delete line
|
||||
try stdout.print("E\n", .{});
|
||||
try stdout.print("\x1B[7;1H", .{}); // cursor up
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
//! Delete Line (DL) - Esc [ M
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("A\nB\nC\nD", .{});
|
||||
try stdout.print("\x1B[2A", .{}); // cursor up
|
||||
try stdout.print("\x1B[M", .{});
|
||||
try stdout.print("E\n", .{});
|
||||
try stdout.print("\x1B[B", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
//! Insert Line (IL) - Esc [ L
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("\x1B[2J", .{}); // clear screen
|
||||
try stdout.print("\x1B[1;1H", .{}); // set cursor position
|
||||
try stdout.print("A\nB\nC\nD\nE", .{});
|
||||
try stdout.print("\x1B[1;2r", .{}); // set scroll region
|
||||
try stdout.print("\x1B[1;1H", .{}); // set cursor position
|
||||
try stdout.print("\x1B[1L", .{}); // insert lines
|
||||
try stdout.print("X", .{});
|
||||
try stdout.print("\x1B[7;1H", .{}); // set cursor position
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
//! DECALN - ESC # 8
|
||||
const std = @import("std");
|
||||
|
||||
pub fn main() !void {
|
||||
const stdout = std.io.getStdOut().writer();
|
||||
try stdout.print("\x1B#8", .{});
|
||||
|
||||
// const stdin = std.io.getStdIn().reader();
|
||||
// _ = try stdin.readByte();
|
||||
}
|
1
dist/linux/app.desktop
vendored
1
dist/linux/app.desktop
vendored
@ -7,6 +7,7 @@ Icon=com.mitchellh.ghostty
|
||||
Categories=System;TerminalEmulator;
|
||||
Keywords=terminal;tty;pty;
|
||||
StartupNotify=true
|
||||
StartupWMClass=com.mitchellh.ghostty
|
||||
Terminal=false
|
||||
Actions=new-window;
|
||||
X-GNOME-UsesNotifications=true
|
||||
|
0
dist/linux/ghostty_dolphin.desktop
vendored
Normal file → Executable file
0
dist/linux/ghostty_dolphin.desktop
vendored
Normal file → Executable file
97
dist/linux/ghostty_nautilus.py
vendored
Normal file
97
dist/linux/ghostty_nautilus.py
vendored
Normal file
@ -0,0 +1,97 @@
|
||||
# Adapted from wezterm: https://github.com/wez/wezterm/blob/main/assets/wezterm-nautilus.py
|
||||
# original copyright notice:
|
||||
#
|
||||
# Copyright (C) 2022 Sebastian Wiesner <sebastian@swsnr.de>
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 2 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along
|
||||
# with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
from os.path import isdir
|
||||
from gi import require_version
|
||||
from gi.repository import Nautilus, GObject, Gio, GLib
|
||||
|
||||
|
||||
class OpenInGhosttyAction(GObject.GObject, Nautilus.MenuProvider):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
session = Gio.bus_get_sync(Gio.BusType.SESSION, None)
|
||||
self._systemd = None
|
||||
# Check if the this system runs under systemd, per sd_booted(3)
|
||||
if isdir('/run/systemd/system/'):
|
||||
self._systemd = Gio.DBusProxy.new_sync(session,
|
||||
Gio.DBusProxyFlags.NONE,
|
||||
None,
|
||||
"org.freedesktop.systemd1",
|
||||
"/org/freedesktop/systemd1",
|
||||
"org.freedesktop.systemd1.Manager", None)
|
||||
|
||||
def _open_terminal(self, path):
|
||||
cmd = ['ghostty', f'--working-directory={path}', '--gtk-single-instance=false']
|
||||
child = Gio.Subprocess.new(cmd, Gio.SubprocessFlags.NONE)
|
||||
if self._systemd:
|
||||
# Move new terminal into a dedicated systemd scope to make systemd
|
||||
# track the terminal separately; in particular this makes systemd
|
||||
# keep a separate CPU and memory account for the terminal which in turn
|
||||
# ensures that oomd doesn't take nautilus down if a process in
|
||||
# ghostty consumes a lot of memory.
|
||||
pid = int(child.get_identifier())
|
||||
props = [("PIDs", GLib.Variant('au', [pid])),
|
||||
('CollectMode', GLib.Variant('s', 'inactive-or-failed'))]
|
||||
name = 'app-nautilus-com.mitchellh.ghostty-{}.scope'.format(pid)
|
||||
args = GLib.Variant('(ssa(sv)a(sa(sv)))', (name, 'fail', props, []))
|
||||
self._systemd.call_sync('StartTransientUnit', args,
|
||||
Gio.DBusCallFlags.NO_AUTO_START, 500, None)
|
||||
|
||||
def _menu_item_activated(self, _menu, paths):
|
||||
for path in paths:
|
||||
self._open_terminal(path)
|
||||
|
||||
def _make_item(self, name, paths):
|
||||
item = Nautilus.MenuItem(name=name, label='Open in Ghostty',
|
||||
icon='com.mitchellh.ghostty')
|
||||
item.connect('activate', self._menu_item_activated, paths)
|
||||
return item
|
||||
|
||||
def _paths_to_open(self, files):
|
||||
paths = []
|
||||
for file in files:
|
||||
location = file.get_location() if file.is_directory() else file.get_parent_location()
|
||||
path = location.get_path()
|
||||
if path and path not in paths:
|
||||
paths.append(path)
|
||||
if 10 < len(paths):
|
||||
# Let's not open anything if the user selected a lot of directories,
|
||||
# to avoid accidentally spamming their desktop with dozends of
|
||||
# new windows or tabs. Ten is a totally arbitrary limit :)
|
||||
return []
|
||||
else:
|
||||
return paths
|
||||
|
||||
def get_file_items(self, *args):
|
||||
# Nautilus 3.0 API passes args (window, files), 4.0 API just passes files
|
||||
files = args[0] if len(args) == 1 else args[1]
|
||||
paths = self._paths_to_open(files)
|
||||
if paths:
|
||||
return [self._make_item(name='GhosttyNautilus::open_in_ghostty', paths=paths)]
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_background_items(self, *args):
|
||||
# Nautilus 3.0 API passes args (window, file), 4.0 API just passes file
|
||||
file = args[0] if len(args) == 1 else args[1]
|
||||
paths = self._paths_to_open([file])
|
||||
if paths:
|
||||
return [self._make_item(name='GhosttyNautilus::open_folder_in_ghostty', paths=paths)]
|
||||
else:
|
||||
return []
|
93
flake.nix
93
flake.nix
@ -31,38 +31,81 @@
|
||||
zig,
|
||||
...
|
||||
}:
|
||||
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (builtins.map (system: let
|
||||
pkgs-stable = nixpkgs-stable.legacyPackages.${system};
|
||||
pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.13.0";
|
||||
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
||||
};
|
||||
builtins.foldl' nixpkgs-stable.lib.recursiveUpdate {} (
|
||||
builtins.map (
|
||||
system: let
|
||||
pkgs-stable = nixpkgs-stable.legacyPackages.${system};
|
||||
pkgs-unstable = nixpkgs-unstable.legacyPackages.${system};
|
||||
in {
|
||||
devShell.${system} = pkgs-stable.callPackage ./nix/devShell.nix {
|
||||
zig = zig.packages.${system}."0.13.0";
|
||||
wraptest = pkgs-stable.callPackage ./nix/wraptest.nix {};
|
||||
};
|
||||
|
||||
packages.${system} = let
|
||||
mkArgs = optimize: {
|
||||
inherit optimize;
|
||||
packages.${system} = let
|
||||
mkArgs = optimize: {
|
||||
inherit optimize;
|
||||
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
};
|
||||
in rec {
|
||||
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
|
||||
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
|
||||
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
|
||||
revision = self.shortRev or self.dirtyShortRev or "dirty";
|
||||
};
|
||||
in rec {
|
||||
ghostty-debug = pkgs-stable.callPackage ./nix/package.nix (mkArgs "Debug");
|
||||
ghostty-releasesafe = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseSafe");
|
||||
ghostty-releasefast = pkgs-stable.callPackage ./nix/package.nix (mkArgs "ReleaseFast");
|
||||
|
||||
ghostty = ghostty-releasefast;
|
||||
default = ghostty;
|
||||
};
|
||||
ghostty = ghostty-releasefast;
|
||||
default = ghostty;
|
||||
};
|
||||
|
||||
formatter.${system} = pkgs-stable.alejandra;
|
||||
formatter.${system} = pkgs-stable.alejandra;
|
||||
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
}) (builtins.attrNames zig.packages))
|
||||
apps.${system} = let
|
||||
runVM = (
|
||||
module: let
|
||||
vm = import ./nix/vm/create.nix {
|
||||
inherit system module;
|
||||
nixpkgs = nixpkgs-stable;
|
||||
overlay = self.overlays.debug;
|
||||
};
|
||||
program = pkgs-stable.writeShellScript "run-ghostty-vm" ''
|
||||
SHARED_DIR=$(pwd)
|
||||
export SHARED_DIR
|
||||
|
||||
${pkgs-stable.lib.getExe vm.config.system.build.vm} "$@"
|
||||
'';
|
||||
in {
|
||||
type = "app";
|
||||
program = "${program}";
|
||||
}
|
||||
);
|
||||
in {
|
||||
wayland-cinnamon = runVM ./nix/vm/wayland-cinnamon.nix;
|
||||
wayland-gnome = runVM ./nix/vm/wayland-gnome.nix;
|
||||
wayland-plasma6 = runVM ./nix/vm/wayland-plasma6.nix;
|
||||
x11-cinnamon = runVM ./nix/vm/x11-cinnamon.nix;
|
||||
x11-gnome = runVM ./nix/vm/x11-gnome.nix;
|
||||
x11-plasma6 = runVM ./nix/vm/x11-plasma6.nix;
|
||||
x11-xfce = runVM ./nix/vm/x11-xfce.nix;
|
||||
};
|
||||
}
|
||||
# Our supported systems are the same supported systems as the Zig binaries.
|
||||
) (builtins.attrNames zig.packages)
|
||||
)
|
||||
// {
|
||||
overlays.default = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.default;
|
||||
overlays = {
|
||||
default = self.overlays.releasefast;
|
||||
releasefast = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-releasefast;
|
||||
};
|
||||
debug = final: prev: {
|
||||
ghostty = self.packages.${prev.system}.ghostty-debug;
|
||||
};
|
||||
};
|
||||
create-vm = import ./nix/vm/create.nix;
|
||||
create-cinnamon-vm = import ./nix/vm/create-cinnamon.nix;
|
||||
create-gnome-vm = import ./nix/vm/create-gnome.nix;
|
||||
create-plasma6-vm = import ./nix/vm/create-plasma6.nix;
|
||||
create-xfce-vm = import ./nix/vm/create-xfce.nix;
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
|
@ -159,7 +159,7 @@ typedef enum {
|
||||
GHOSTTY_KEY_EQUAL,
|
||||
GHOSTTY_KEY_LEFT_BRACKET, // [
|
||||
GHOSTTY_KEY_RIGHT_BRACKET, // ]
|
||||
GHOSTTY_KEY_BACKSLASH, // /
|
||||
GHOSTTY_KEY_BACKSLASH, // \
|
||||
|
||||
// control
|
||||
GHOSTTY_KEY_UP,
|
||||
@ -559,10 +559,13 @@ typedef struct {
|
||||
|
||||
// apprt.Action.Key
|
||||
typedef enum {
|
||||
GHOSTTY_ACTION_QUIT,
|
||||
GHOSTTY_ACTION_NEW_WINDOW,
|
||||
GHOSTTY_ACTION_NEW_TAB,
|
||||
GHOSTTY_ACTION_CLOSE_TAB,
|
||||
GHOSTTY_ACTION_NEW_SPLIT,
|
||||
GHOSTTY_ACTION_CLOSE_ALL_WINDOWS,
|
||||
GHOSTTY_ACTION_TOGGLE_MAXIMIZE,
|
||||
GHOSTTY_ACTION_TOGGLE_FULLSCREEN,
|
||||
GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW,
|
||||
GHOSTTY_ACTION_TOGGLE_WINDOW_DECORATIONS,
|
||||
@ -681,10 +684,11 @@ void ghostty_config_open();
|
||||
ghostty_app_t ghostty_app_new(const ghostty_runtime_config_s*,
|
||||
ghostty_config_t);
|
||||
void ghostty_app_free(ghostty_app_t);
|
||||
bool ghostty_app_tick(ghostty_app_t);
|
||||
void ghostty_app_tick(ghostty_app_t);
|
||||
void* ghostty_app_userdata(ghostty_app_t);
|
||||
void ghostty_app_set_focus(ghostty_app_t, bool);
|
||||
bool ghostty_app_key(ghostty_app_t, ghostty_input_key_s);
|
||||
bool ghostty_app_key_is_binding(ghostty_app_t, ghostty_input_key_s);
|
||||
void ghostty_app_keyboard_changed(ghostty_app_t);
|
||||
void ghostty_app_open_config(ghostty_app_t);
|
||||
void ghostty_app_update_config(ghostty_app_t, ghostty_config_t);
|
||||
@ -712,7 +716,8 @@ void ghostty_surface_set_color_scheme(ghostty_surface_t,
|
||||
ghostty_color_scheme_e);
|
||||
ghostty_input_mods_e ghostty_surface_key_translation_mods(ghostty_surface_t,
|
||||
ghostty_input_mods_e);
|
||||
void ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key(ghostty_surface_t, ghostty_input_key_s);
|
||||
bool ghostty_surface_key_is_binding(ghostty_surface_t, ghostty_input_key_s);
|
||||
void ghostty_surface_text(ghostty_surface_t, const char*, uintptr_t);
|
||||
bool ghostty_surface_mouse_captured(ghostty_surface_t);
|
||||
bool ghostty_surface_mouse_button(ghostty_surface_t,
|
||||
|
@ -10,8 +10,8 @@
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */ = {isa = PBXBuildFile; fileRef = 29C15B1C2CDC3B2000520DD4 /* bat */; };
|
||||
55154BE02B33911F001622DC /* ghostty in Resources */ = {isa = PBXBuildFile; fileRef = 55154BDF2B33911F001622DC /* ghostty */; };
|
||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
@ -69,8 +69,12 @@
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */; };
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A59FB5D02AE0DEA7009128F3 /* MetalView.swift */; };
|
||||
A5A1F8852A489D6800D1E8BC /* terminfo in Resources */ = {isa = PBXBuildFile; fileRef = A5A1F8842A489D6800D1E8BC /* terminfo */; };
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3C92D4445E20033CF96 /* Dock.swift */; };
|
||||
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */; };
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5A6F7292CC41B8700B232A5 /* Xcode.swift */; };
|
||||
A5B30539299BEAAB0047F10C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A5B30538299BEAAB0047F10C /* Assets.xcassets */; };
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */; };
|
||||
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CA378D2D31D6C100931030 /* Weak.swift */; };
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */; };
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CBD0572C9F30860017A1AE /* Cursor.swift */; };
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
@ -87,6 +91,8 @@
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDB29B8009000646FDA /* SplitView.swift */; };
|
||||
A5CEAFDE29B8058B00646FDA /* SplitView.Divider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */; };
|
||||
A5CEAFFF29C2410700646FDA /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CEAFFE29C2410700646FDA /* Backport.swift */; };
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */; };
|
||||
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 */; };
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||
@ -99,6 +105,7 @@
|
||||
C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EA62B738B9900404083 /* NSView+Extension.swift */; };
|
||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = C1F26EE82B76CBFC00404083 /* VibrantLayer.m */; };
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */; };
|
||||
FC5218FA2D10FFCE004C93E0 /* zsh in Resources */ = {isa = PBXBuildFile; fileRef = FC5218F92D10FFC7004C93E0 /* zsh */; };
|
||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */ = {isa = PBXBuildFile; fileRef = FC9ABA9B2D0F538D0020D4C8 /* bash-completion */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -108,8 +115,8 @@
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GhosttyReleaseLocal.entitlements; sourceTree = "<group>"; };
|
||||
55154BDF2B33911F001622DC /* ghostty */ = {isa = PBXFileReference; lastKnownFileType = folder; name = ghostty; path = "../zig-out/share/ghostty"; sourceTree = "<group>"; };
|
||||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51B78462AF4B58B00F3EDB9 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
|
||||
@ -158,10 +165,14 @@
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = "<group>"; };
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetalView.swift; sourceTree = "<group>"; };
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */ = {isa = PBXFileReference; lastKnownFileType = folder; name = terminfo; path = "../zig-out/share/terminfo"; sourceTree = "<group>"; };
|
||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dock.swift; sourceTree = "<group>"; };
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5A6F7292CC41B8700B232A5 /* Xcode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Xcode.swift; sourceTree = "<group>"; };
|
||||
A5B30531299BEAAA0047F10C /* Ghostty.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ghostty.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Ghostty.entitlements; sourceTree = "<group>"; };
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardLayout.swift; sourceTree = "<group>"; };
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraggableWindowView.swift; sourceTree = "<group>"; };
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cursor.swift; sourceTree = "<group>"; };
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = QuickTerminal.xib; sourceTree = "<group>"; };
|
||||
@ -177,6 +188,8 @@
|
||||
A5CEAFDB29B8009000646FDA /* SplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.swift; sourceTree = "<group>"; };
|
||||
A5CEAFDD29B8058B00646FDA /* SplitView.Divider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplitView.Divider.swift; sourceTree = "<group>"; };
|
||||
A5CEAFFE29C2410700646FDA /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSEvent+Extension.swift"; sourceTree = "<group>"; };
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Event.swift; sourceTree = "<group>"; };
|
||||
A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalRestorable.swift; sourceTree = "<group>"; };
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableBridge.swift; sourceTree = "<group>"; };
|
||||
A5D4499D2B53AE7B000F5B83 /* Ghostty-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ghostty-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -192,6 +205,7 @@
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VibrantLayer.h; sourceTree = "<group>"; };
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VibrantLayer.m; sourceTree = "<group>"; };
|
||||
C1F26EEA2B76CC2400404083 /* ghostty-bridging-header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ghostty-bridging-header.h"; sourceTree = "<group>"; };
|
||||
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalSpaceBehavior.swift; sourceTree = "<group>"; };
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */ = {isa = PBXFileReference; lastKnownFileType = folder; name = zsh; path = "../zig-out/share/zsh"; sourceTree = "<group>"; };
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "bash-completion"; path = "../zig-out/share/bash-completion"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@ -261,18 +275,22 @@
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
A5A2A3C92D4445E20033CF96 /* Dock.swift */,
|
||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
|
||||
A54B0CEA2D0CFB4A00CBEFF8 /* NSImage+Extension.swift */,
|
||||
A52FFF5C2CAB4D05000C6A5B /* NSScreen+Extension.swift */,
|
||||
C1F26EA62B738B9900404083 /* NSView+Extension.swift */,
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */,
|
||||
A5985CD62C320C4500C57AD3 /* String+Extension.swift */,
|
||||
A5CC36142C9CDA03004D6760 /* View+Extension.swift */,
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */,
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
|
||||
A5CEAFDA29B8005900646FDA /* SplitView */,
|
||||
@ -351,12 +369,14 @@
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A59630A32AF059BB00D64628 /* Ghostty.SplitNode.swift */,
|
||||
A59630A12AF0415000D64628 /* Ghostty.TerminalSplit.swift */,
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */,
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */,
|
||||
);
|
||||
path = Ghostty;
|
||||
sourceTree = "<group>";
|
||||
@ -399,13 +419,13 @@
|
||||
children = (
|
||||
FC9ABA9B2D0F538D0020D4C8 /* bash-completion */,
|
||||
29C15B1C2CDC3B2000520DD4 /* bat */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
A586167B2B7703CC009BDB1D /* fish */,
|
||||
55154BDF2B33911F001622DC /* ghostty */,
|
||||
A5985CE52C33060F00C57AD3 /* man */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */,
|
||||
9351BE8E2D22937F003B3499 /* nvim */,
|
||||
A5A1F8842A489D6800D1E8BC /* terminfo */,
|
||||
552964E52B34A9B400030505 /* vim */,
|
||||
FC5218F92D10FFC7004C93E0 /* zsh */,
|
||||
);
|
||||
name = Resources;
|
||||
sourceTree = "<group>";
|
||||
@ -439,6 +459,7 @@
|
||||
children = (
|
||||
A5CBD05B2CA0C5C70017A1AE /* QuickTerminal.xib */,
|
||||
A5CBD05D2CA0C5E70017A1AE /* QuickTerminalController.swift */,
|
||||
CFBB5FE92D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift */,
|
||||
A5CBD0632CA122E70017A1AE /* QuickTerminalPosition.swift */,
|
||||
A52FFF562CA90481000C6A5B /* QuickTerminalScreen.swift */,
|
||||
A5CBD05F2CA0C9080017A1AE /* QuickTerminalWindow.swift */,
|
||||
@ -607,16 +628,20 @@
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */,
|
||||
A59FB5CF2AE0DB50009128F3 /* InspectorView.swift in Sources */,
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */,
|
||||
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
|
||||
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
||||
A5CC36152C9CDA06004D6760 /* View+Extension.swift in Sources */,
|
||||
A56D58892ACDE6CA00508D2C /* ServiceProvider.swift in Sources */,
|
||||
A5CBD0602CA0C90A0017A1AE /* QuickTerminalWindow.swift in Sources */,
|
||||
A5CBD05E2CA0C5EC0017A1AE /* QuickTerminalController.swift in Sources */,
|
||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */,
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
|
||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
@ -632,12 +657,14 @@
|
||||
A5A6F72A2CC41B8900B232A5 /* Xcode.swift in Sources */,
|
||||
A52FFF5B2CAA54B1000C6A5B /* FullscreenMode+Extension.swift in Sources */,
|
||||
A5333E222B5A2128008AEFF7 /* SurfaceView_AppKit.swift in Sources */,
|
||||
A5CA378E2D31D6C300931030 /* Weak.swift in Sources */,
|
||||
A5CDF1952AAFA19600513312 /* ConfigurationErrorsView.swift in Sources */,
|
||||
A55B7BBC29B6FC330055DE60 /* SurfaceView.swift in Sources */,
|
||||
A5333E1C2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||
A59444F729A2ED5200725BBA /* SettingsView.swift in Sources */,
|
||||
A56D58862ACDDB4100508D2C /* Ghostty.Shell.swift in Sources */,
|
||||
A5985CD72C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||
A5A2A3CC2D444ABB0033CF96 /* NSApplication+Extension.swift in Sources */,
|
||||
A59630A22AF0415000D64628 /* Ghostty.TerminalSplit.swift in Sources */,
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
||||
@ -647,6 +674,7 @@
|
||||
A5CDF1932AAF9E0800513312 /* ConfigurationErrorsController.swift in Sources */,
|
||||
A53A6C032CCC1B7F00943E98 /* Ghostty.Action.swift in Sources */,
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */,
|
||||
A5CA378C2D2A4DEB00931030 /* KeyboardLayout.swift in Sources */,
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */,
|
||||
A59FB5D12AE0DEA7009128F3 /* MetalView.swift in Sources */,
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */,
|
||||
@ -765,21 +793,22 @@
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts.";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program running within Ghostty would like to access your Calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program running within Ghostty would like to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program running within Ghostty would like to access your Contacts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information.";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program running within Ghostty would like to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program running within Ghostty would like to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
@ -934,21 +963,22 @@
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts.";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program running within Ghostty would like to access your Calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program running within Ghostty would like to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program running within Ghostty would like to access your Contacts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information.";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program running within Ghostty would like to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program running within Ghostty would like to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
@ -987,21 +1017,22 @@
|
||||
INFOPLIST_FILE = "Ghostty-Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ghostty;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program in Ghostty wants to use AppleScript.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program in Ghostty wants to use your calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program in Ghostty wants to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program in Ghostty wants to use your contacts.";
|
||||
INFOPLIST_KEY_NSAppleEventsUsageDescription = "A program running within Ghostty would like to use AppleScript.";
|
||||
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "A program running within Ghostty would like to use Bluetooth.";
|
||||
INFOPLIST_KEY_NSCalendarsUsageDescription = "A program running within Ghostty would like to access your Calendar.";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "A program running within Ghostty would like to use the camera.";
|
||||
INFOPLIST_KEY_NSContactsUsageDescription = "A program running within Ghostty would like to access your Contacts.";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program in Ghostty wants to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program in Ghostty wants to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program in Ghostty wants to use your location information.";
|
||||
INFOPLIST_KEY_NSLocalNetworkUsageDescription = "A program running within Ghostty would like to access the local network.";
|
||||
INFOPLIST_KEY_NSLocationTemporaryUsageDescriptionDictionary = "A program running within Ghostty would like to use your location temporarily.";
|
||||
INFOPLIST_KEY_NSLocationUsageDescription = "A program running within Ghostty would like to access your location information.";
|
||||
INFOPLIST_KEY_NSMainNibFile = MainMenu;
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program in Ghostty wants to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program in Ghostty wants to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program in Ghostty wants to use your photo library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program in Ghostty wants to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program in Ghostty wants to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program in Ghostty requires elevated privileges.";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "A program running within Ghostty would like to use your microphone.";
|
||||
INFOPLIST_KEY_NSMotionUsageDescription = "A program running within Ghostty would like to access motion data.";
|
||||
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "A program running within Ghostty would like to access your Photo Library.";
|
||||
INFOPLIST_KEY_NSRemindersUsageDescription = "A program running within Ghostty would like to access your reminders.";
|
||||
INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "A program running within Ghostty would like to use speech recognition.";
|
||||
INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "A program running within Ghostty requires elevated privileges.";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
|
@ -30,11 +30,13 @@ class AppDelegate: NSObject,
|
||||
@IBOutlet private var menuSplitRight: NSMenuItem?
|
||||
@IBOutlet private var menuSplitDown: NSMenuItem?
|
||||
@IBOutlet private var menuClose: NSMenuItem?
|
||||
@IBOutlet private var menuCloseTab: NSMenuItem?
|
||||
@IBOutlet private var menuCloseWindow: NSMenuItem?
|
||||
@IBOutlet private var menuCloseAllWindows: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuCopy: NSMenuItem?
|
||||
@IBOutlet private var menuPaste: NSMenuItem?
|
||||
@IBOutlet private var menuPasteSelection: NSMenuItem?
|
||||
@IBOutlet private var menuSelectAll: NSMenuItem?
|
||||
|
||||
@IBOutlet private var menuToggleVisibility: NSMenuItem?
|
||||
@ -90,10 +92,8 @@ class AppDelegate: NSObject,
|
||||
return ProcessInfo.processInfo.systemUptime - applicationLaunchTime
|
||||
}
|
||||
|
||||
/// Tracks whether the application is currently visible. This can be gamed, i.e. if a user manually
|
||||
/// brings each window one by one to the front. But at worst its off by one set of toggles and this
|
||||
/// makes our logic very easy.
|
||||
private var isVisible: Bool = true
|
||||
/// Tracks the windows that we hid for toggleVisibility.
|
||||
private var hiddenWindows: [Weak<NSWindow>] = []
|
||||
|
||||
/// The observer for the app appearance.
|
||||
private var appearanceObserver: NSKeyValueObservation? = nil
|
||||
@ -217,15 +217,20 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
guard !applicationHasBecomeActive else { return }
|
||||
applicationHasBecomeActive = true
|
||||
// If we're back then clear the hidden windows
|
||||
self.hiddenWindows = []
|
||||
|
||||
// Let's launch our first window. We only do this if we have no other windows. It
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
// First launch stuff
|
||||
if (!applicationHasBecomeActive) {
|
||||
applicationHasBecomeActive = true
|
||||
|
||||
// Let's launch our first window. We only do this if we have no other windows. It
|
||||
// is possible to have other windows in a few scenarios:
|
||||
// - if we're opening a URL since `application(_:openFile:)` is called before this.
|
||||
// - if we're restoring from persisted state
|
||||
if terminalManager.windows.count == 0 && derivedConfig.initialWindow {
|
||||
terminalManager.newWindow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -346,6 +351,7 @@ class AppDelegate: NSObject,
|
||||
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(config, action: "close_tab", menuItem: self.menuCloseTab)
|
||||
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
syncMenuShortcut(config, action: "new_split:right", menuItem: self.menuSplitRight)
|
||||
@ -353,6 +359,7 @@ class AppDelegate: NSObject,
|
||||
|
||||
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
|
||||
syncMenuShortcut(config, action: "select_all", menuItem: self.menuSelectAll)
|
||||
|
||||
syncMenuShortcut(config, action: "toggle_split_zoom", menuItem: self.menuZoomSplit)
|
||||
@ -424,32 +431,42 @@ class AppDelegate: NSObject,
|
||||
// If we have a main window then we don't process any of the keys
|
||||
// because we let it capture and propagate.
|
||||
guard NSApp.mainWindow == nil else { return event }
|
||||
|
||||
|
||||
// If this event as-is would result in a key binding then we send it.
|
||||
if let app = ghostty.app,
|
||||
ghostty_app_key_is_binding(
|
||||
app,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) {
|
||||
// If the key was handled by Ghostty we stop the event chain. If
|
||||
// the key wasn't handled then we let it fall through and continue
|
||||
// processing. This is important because some bindings may have no
|
||||
// affect at this scope.
|
||||
if (ghostty_app_key(
|
||||
app,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// If this event would be handled by our menu then we do nothing.
|
||||
if let mainMenu = NSApp.mainMenu,
|
||||
mainMenu.performKeyEquivalent(with: event) {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// If we reach this point then we try to process the key event
|
||||
// through the Ghostty key mechanism.
|
||||
|
||||
|
||||
// Ghostty must be loaded
|
||||
guard let ghostty = self.ghostty.app else { return event }
|
||||
|
||||
|
||||
// Build our event input and call ghostty
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = GHOSTTY_ACTION_PRESS
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
if (ghostty_app_key(ghostty, key_ev)) {
|
||||
if (ghostty_app_key(ghostty, event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS))) {
|
||||
// The key was used so we want to stop it from going to our Mac app
|
||||
Ghostty.logger.debug("local key event handled event=\(event)")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
@ -692,21 +709,23 @@ class AppDelegate: NSObject,
|
||||
|
||||
/// Toggles visibility of all Ghosty Terminal windows. When hidden, activates Ghostty as the frontmost application
|
||||
@IBAction func toggleVisibility(_ sender: Any) {
|
||||
// We only care about terminal windows.
|
||||
for window in NSApp.windows.filter({ $0.windowController is BaseTerminalController }) {
|
||||
if isVisible {
|
||||
window.orderOut(nil)
|
||||
} else {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
// If we have focus, then we hide all windows.
|
||||
if NSApp.isActive {
|
||||
// We need to keep track of the windows that were visible because we only
|
||||
// want to bring back these windows if we remove the toggle.
|
||||
self.hiddenWindows = NSApp.windows.filter { $0.isVisible }.map { Weak($0) }
|
||||
NSApp.hide(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// After bringing them all to front we make sure our app is active too.
|
||||
if !isVisible {
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
}
|
||||
// If we're not active, we want to become active
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
|
||||
isVisible.toggle()
|
||||
// Bring all windows to the front. Note: we don't use NSApp.unhide because
|
||||
// that will unhide ALL hidden windows. We want to only bring forward the
|
||||
// ones that we hid.
|
||||
self.hiddenWindows.forEach { $0.value?.orderFrontRegardless() }
|
||||
self.hiddenWindows = []
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
|
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23504" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23504"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
@ -17,6 +17,7 @@
|
||||
<outlet property="menuCheckForUpdates" destination="GEA-5y-yzH" id="0nV-Tf-nJQ"/>
|
||||
<outlet property="menuClose" destination="DVo-aG-piG" id="R3t-0C-aSU"/>
|
||||
<outlet property="menuCloseAllWindows" destination="yKr-Vi-Yqw" id="Zet-Ir-zbm"/>
|
||||
<outlet property="menuCloseTab" destination="Obb-Mk-j8J" id="Gda-L0-gdz"/>
|
||||
<outlet property="menuCloseWindow" destination="W5w-UZ-crk" id="6ff-BT-ENV"/>
|
||||
<outlet property="menuCopy" destination="Jqf-pv-Zcu" id="bKd-1C-oy9"/>
|
||||
<outlet property="menuDecreaseFontSize" destination="kzb-SZ-dOA" id="Y1B-Vh-6Z2"/>
|
||||
@ -31,6 +32,7 @@
|
||||
<outlet property="menuNextSplit" destination="bD7-ei-wKU" id="LeT-xw-eh4"/>
|
||||
<outlet property="menuOpenConfig" destination="BOF-NM-1cW" id="Nze-Go-glw"/>
|
||||
<outlet property="menuPaste" destination="i27-pK-umN" id="ICc-X2-gV3"/>
|
||||
<outlet property="menuPasteSelection" destination="akq-ov-Jjh" id="GS8-aQ-hVw"/>
|
||||
<outlet property="menuPreviousSplit" destination="Lic-px-1wg" id="Rto-CG-yRe"/>
|
||||
<outlet property="menuQuickTerminal" destination="1pv-LF-NBJ" id="glN-5B-IGi"/>
|
||||
<outlet property="menuQuit" destination="4sb-4s-VLi" id="qYN-S1-6UW"/>
|
||||
@ -154,6 +156,12 @@
|
||||
<action selector="close:" target="-1" id="tTZ-2b-Mbm"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Close Tab" id="Obb-Mk-j8J">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="closeTab:" target="-1" id="UBb-Bd-nkj"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Close Window" id="W5w-UZ-crk">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
@ -185,6 +193,12 @@
|
||||
<action selector="paste:" target="-1" id="ZKe-2B-mel"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Paste Selection" id="akq-ov-Jjh">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="pasteSelection:" target="-1" id="vo3-Rf-Udb"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Select All" id="q2h-lq-e4r">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
|
@ -3,6 +3,12 @@ import Cocoa
|
||||
import SwiftUI
|
||||
import GhosttyKit
|
||||
|
||||
// This is a Apple's private function that we need to call to get the active space.
|
||||
@_silgen_name("CGSGetActiveSpace")
|
||||
func CGSGetActiveSpace(_ cid: Int) -> size_t
|
||||
@_silgen_name("CGSMainConnectionID")
|
||||
func CGSMainConnectionID() -> Int
|
||||
|
||||
/// Controller for the "quick" terminal.
|
||||
class QuickTerminalController: BaseTerminalController {
|
||||
override var windowNibName: NSNib.Name? { "QuickTerminal" }
|
||||
@ -18,6 +24,13 @@ class QuickTerminalController: BaseTerminalController {
|
||||
/// application to the front.
|
||||
private var previousApp: NSRunningApplication? = nil
|
||||
|
||||
// The active space when the quick terminal was last shown.
|
||||
private var previousActiveSpace: size_t = 0
|
||||
|
||||
/// This is set to true of the dock was autohid when the terminal animated in. This lets us
|
||||
/// know if we have to unhide when the terminal is animated out.
|
||||
private var hidDock: Bool = false
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
|
||||
@ -107,8 +120,28 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.previousApp = nil
|
||||
}
|
||||
|
||||
if (derivedConfig.quickTerminalAutoHide) {
|
||||
animateOut()
|
||||
if derivedConfig.quickTerminalAutoHide {
|
||||
switch derivedConfig.quickTerminalSpaceBehavior {
|
||||
case .remain:
|
||||
// If we lose focus on the active space, then we can animate out
|
||||
animateOut()
|
||||
|
||||
case .move:
|
||||
let currentActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
|
||||
if previousActiveSpace == currentActiveSpace {
|
||||
// We haven't moved spaces. We lost focus to another app on the
|
||||
// current space. Animate out.
|
||||
animateOut()
|
||||
} else {
|
||||
// We've moved to a different space. Bring the quick terminal back
|
||||
// into view.
|
||||
DispatchQueue.main.async {
|
||||
self.window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
self.previousActiveSpace = currentActiveSpace
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,6 +196,9 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
// Set previous active space
|
||||
self.previousActiveSpace = CGSGetActiveSpace(CGSMainConnectionID())
|
||||
|
||||
// Animate the window in
|
||||
animateWindowIn(window: window, from: position)
|
||||
|
||||
@ -192,14 +228,39 @@ class QuickTerminalController: BaseTerminalController {
|
||||
animateWindowOut(window: window, to: position)
|
||||
}
|
||||
|
||||
private func hideDock() {
|
||||
guard !hidDock else { return }
|
||||
NSApp.acquirePresentationOption(.autoHideDock)
|
||||
hidDock = true
|
||||
}
|
||||
|
||||
private func unhideDock() {
|
||||
guard hidDock else { return }
|
||||
NSApp.releasePresentationOption(.autoHideDock)
|
||||
hidDock = false
|
||||
}
|
||||
|
||||
private func animateWindowIn(window: NSWindow, from position: QuickTerminalPosition) {
|
||||
guard let screen = derivedConfig.quickTerminalScreen.screen else { return }
|
||||
|
||||
// Move our window off screen to the top
|
||||
position.setInitial(in: window, on: screen)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
window.level = .popUpMenu
|
||||
|
||||
// Move it to the visible position since animation requires this
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
DispatchQueue.main.async {
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
// If our dock position would conflict with our target location then
|
||||
// we autohide the dock.
|
||||
if position.conflictsWithDock(on: screen) {
|
||||
hideDock()
|
||||
}
|
||||
|
||||
// Run the animation that moves our window into the proper place and makes
|
||||
// it visible.
|
||||
@ -211,8 +272,16 @@ class QuickTerminalController: BaseTerminalController {
|
||||
// There is a very minor delay here so waiting at least an event loop tick
|
||||
// keeps us safe from the view not being on the window.
|
||||
DispatchQueue.main.async {
|
||||
// If we canceled our animation in we do nothing
|
||||
guard self.visible else { return }
|
||||
// If we canceled our animation clean up some state.
|
||||
guard self.visible else {
|
||||
self.unhideDock()
|
||||
return
|
||||
}
|
||||
|
||||
// After animating in, we reset the window level to a value that
|
||||
// is above other windows but not as high as popUpMenu. This allows
|
||||
// things like IME dropdowns to appear properly.
|
||||
window.level = .floating
|
||||
|
||||
// Now that the window is visible, sync our appearance. This function
|
||||
// requires the window is visible.
|
||||
@ -276,6 +345,17 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
private func animateWindowOut(window: NSWindow, to position: QuickTerminalPosition) {
|
||||
// If we hid the dock then we unhide it.
|
||||
unhideDock()
|
||||
|
||||
// If the window isn't on our active space then we don't animate, we just
|
||||
// hide it.
|
||||
if !window.isOnActiveSpace {
|
||||
self.previousApp = nil
|
||||
window.orderOut(self)
|
||||
return
|
||||
}
|
||||
|
||||
// We always animate out to whatever screen the window is actually on.
|
||||
guard let screen = window.screen ?? NSScreen.main else { return }
|
||||
|
||||
@ -297,6 +377,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
window.level = .popUpMenu
|
||||
|
||||
NSAnimationContext.runAnimationGroup({ context in
|
||||
context.duration = derivedConfig.quickTerminalAnimationDuration
|
||||
context.timingFunction = .init(name: .easeIn)
|
||||
@ -311,23 +396,13 @@ class QuickTerminalController: BaseTerminalController {
|
||||
private func syncAppearance() {
|
||||
guard let window else { return }
|
||||
|
||||
// Change the collection behavior of the window depending on the configuration.
|
||||
window.collectionBehavior = derivedConfig.quickTerminalSpaceBehavior.collectionBehavior
|
||||
|
||||
// If our window is not visible, then no need to sync the appearance yet.
|
||||
// Some APIs such as window blur have no effect unless the window is visible.
|
||||
guard window.isVisible else { return }
|
||||
|
||||
// Terminals typically operate in sRGB color space and macOS defaults
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (self.derivedConfig.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
fallthrough
|
||||
default:
|
||||
window.colorSpace = .sRGB
|
||||
}
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
if (self.derivedConfig.backgroundOpacity < 1) {
|
||||
window.isOpaque = false
|
||||
@ -396,14 +471,14 @@ class QuickTerminalController: BaseTerminalController {
|
||||
let quickTerminalScreen: QuickTerminalScreen
|
||||
let quickTerminalAnimationDuration: Double
|
||||
let quickTerminalAutoHide: Bool
|
||||
let windowColorspace: String
|
||||
let quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior
|
||||
let backgroundOpacity: Double
|
||||
|
||||
init() {
|
||||
self.quickTerminalScreen = .main
|
||||
self.quickTerminalAnimationDuration = 0.2
|
||||
self.quickTerminalAutoHide = true
|
||||
self.windowColorspace = ""
|
||||
self.quickTerminalSpaceBehavior = .move
|
||||
self.backgroundOpacity = 1.0
|
||||
}
|
||||
|
||||
@ -411,7 +486,7 @@ class QuickTerminalController: BaseTerminalController {
|
||||
self.quickTerminalScreen = config.quickTerminalScreen
|
||||
self.quickTerminalAnimationDuration = config.quickTerminalAnimationDuration
|
||||
self.quickTerminalAutoHide = config.quickTerminalAutoHide
|
||||
self.windowColorspace = config.windowColorspace
|
||||
self.quickTerminalSpaceBehavior = config.quickTerminalSpaceBehavior
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
}
|
||||
}
|
||||
|
@ -69,7 +69,7 @@ enum QuickTerminalPosition : String {
|
||||
finalSize.width = screen.frame.width
|
||||
|
||||
case .left, .right:
|
||||
finalSize.height = screen.frame.height
|
||||
finalSize.height = screen.visibleFrame.height
|
||||
|
||||
case .center:
|
||||
finalSize.width = screen.frame.width / 2
|
||||
@ -89,13 +89,13 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.frame.minX, y: -window.frame.height)
|
||||
|
||||
case .left:
|
||||
return .init(x: -window.frame.width, y: 0)
|
||||
return .init(x: screen.frame.minX-window.frame.width, y: 0)
|
||||
|
||||
case .right:
|
||||
return .init(x: screen.frame.maxX, y: 0)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: screen.visibleFrame.maxY - window.frame.width)
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.height - window.frame.width)
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +115,25 @@ enum QuickTerminalPosition : String {
|
||||
return .init(x: screen.visibleFrame.maxX - window.frame.width, y: window.frame.origin.y)
|
||||
|
||||
case .center:
|
||||
return .init(x: (screen.visibleFrame.maxX - window.frame.width) / 2, y: (screen.visibleFrame.maxY - window.frame.height) / 2)
|
||||
return .init(x: screen.visibleFrame.origin.x + (screen.visibleFrame.width - window.frame.width) / 2, y: screen.visibleFrame.origin.y + (screen.visibleFrame.height - window.frame.height) / 2)
|
||||
}
|
||||
}
|
||||
|
||||
func conflictsWithDock(on screen: NSScreen) -> Bool {
|
||||
// Screen must have a dock for it to conflict
|
||||
guard screen.hasDock else { return false }
|
||||
|
||||
// Get the dock orientation for this screen
|
||||
guard let orientation = Dock.orientation else { return false }
|
||||
|
||||
// Depending on the orientation of the dock, we conflict if our quick terminal
|
||||
// would potentially "hit" the dock. In the future we should probably consider
|
||||
// the frame of the quick terminal.
|
||||
return switch (orientation) {
|
||||
case .top: self == .top || self == .left || self == .right
|
||||
case .bottom: self == .bottom || self == .left || self == .right
|
||||
case .left: self == .top || self == .bottom
|
||||
case .right: self == .top || self == .bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import Cocoa
|
||||
|
||||
enum QuickTerminalSpaceBehavior {
|
||||
case remain
|
||||
case move
|
||||
|
||||
init?(fromGhosttyConfig string: String) {
|
||||
switch (string) {
|
||||
case "move":
|
||||
self = .move
|
||||
|
||||
case "remain":
|
||||
self = .remain
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var collectionBehavior: NSWindow.CollectionBehavior {
|
||||
let commonBehavior: [NSWindow.CollectionBehavior] = [
|
||||
.ignoresCycle,
|
||||
.fullScreenAuxiliary
|
||||
]
|
||||
|
||||
switch (self) {
|
||||
case .move:
|
||||
// We want this to move the window to the active space.
|
||||
return NSWindow.CollectionBehavior([.canJoinAllSpaces] + commonBehavior)
|
||||
case .remain:
|
||||
// We want this to remain the window in the current space.
|
||||
return NSWindow.CollectionBehavior([.moveToActiveSpace] + commonBehavior)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import Cocoa
|
||||
|
||||
class QuickTerminalWindow: NSWindow {
|
||||
class QuickTerminalWindow: NSPanel {
|
||||
// Both of these must be true for windows without decorations to be able to
|
||||
// still become key/main and receive events.
|
||||
override var canBecomeKey: Bool { return true }
|
||||
@ -26,22 +26,7 @@ class QuickTerminalWindow: NSWindow {
|
||||
// window remains resizable.
|
||||
self.styleMask.remove(.titled)
|
||||
|
||||
// We need to set our window level to a high value. In testing, only
|
||||
// popUpMenu and above do what we want. This gets it above the menu bar
|
||||
// and lets us render off screen.
|
||||
self.level = .popUpMenu
|
||||
|
||||
// This plus the level above was what was needed for the animation to work,
|
||||
// because it gets the window off screen properly. Plus we add some fields
|
||||
// we just want the behavior of.
|
||||
self.collectionBehavior = [
|
||||
// We want this to be part of every space because it is a singleton.
|
||||
.canJoinAllSpaces,
|
||||
|
||||
// We don't want to be part of command-tilde
|
||||
.ignoresCycle,
|
||||
|
||||
// We never support fullscreen
|
||||
.fullScreenNone]
|
||||
// We don't want to activate the owning app when quick terminal is triggered.
|
||||
self.styleMask.insert(.nonactivatingPanel)
|
||||
}
|
||||
}
|
||||
|
@ -389,9 +389,9 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
|
||||
switch (request) {
|
||||
case .osc_52_write:
|
||||
case let .osc_52_write(pasteboard):
|
||||
guard case .confirm = action else { break }
|
||||
let pb = NSPasteboard.general
|
||||
let pb = pasteboard ?? NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
pb.setString(cc.contents, forType: .string)
|
||||
case .osc_52_read, .paste:
|
||||
@ -452,6 +452,7 @@ class BaseTerminalController: NSWindowController,
|
||||
self.alert = nil
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
alert.window.orderOut(nil)
|
||||
window.close()
|
||||
|
||||
default:
|
||||
|
@ -22,7 +22,7 @@ class TerminalController: BaseTerminalController {
|
||||
private var restorable: Bool = true
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig
|
||||
private(set) var derivedConfig: DerivedConfig
|
||||
|
||||
/// The notification cancellable for focused surface property changes.
|
||||
private var surfaceAppearanceCancellables: Set<AnyCancellable> = []
|
||||
@ -60,6 +60,11 @@ class TerminalController: BaseTerminalController {
|
||||
selector: #selector(onGotoTab),
|
||||
name: Ghostty.Notification.ghosttyGotoTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(onCloseTab),
|
||||
name: .ghosttyCloseTab,
|
||||
object: nil)
|
||||
center.addObserver(
|
||||
self,
|
||||
selector: #selector(ghosttyConfigDidChange(_:)),
|
||||
@ -310,28 +315,28 @@ class TerminalController: BaseTerminalController {
|
||||
window.styleMask = [
|
||||
// We need `titled` in the mask to get the normal window frame
|
||||
.titled,
|
||||
|
||||
|
||||
// Full size content view so we can extend
|
||||
// content in to the hidden titlebar's area
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.fullSizeContentView,
|
||||
|
||||
.resizable,
|
||||
.closable,
|
||||
.miniaturizable,
|
||||
]
|
||||
|
||||
|
||||
// Hide the title
|
||||
window.titleVisibility = .hidden
|
||||
window.titlebarAppearsTransparent = true
|
||||
|
||||
|
||||
// Hide the traffic lights (window control buttons)
|
||||
window.standardWindowButton(.closeButton)?.isHidden = true
|
||||
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
window.standardWindowButton(.zoomButton)?.isHidden = true
|
||||
|
||||
|
||||
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
|
||||
window.tabbingMode = .disallowed
|
||||
|
||||
|
||||
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
|
||||
// some operations that appear to bring back the titlebar visibility so this ensures
|
||||
// it is gone forever.
|
||||
@ -340,7 +345,7 @@ class TerminalController: BaseTerminalController {
|
||||
titleBarContainer.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func windowDidLoad() {
|
||||
super.windowDidLoad()
|
||||
guard let window = window as? TerminalWindow else { return }
|
||||
@ -361,33 +366,31 @@ class TerminalController: BaseTerminalController {
|
||||
// If window decorations are disabled, remove our title
|
||||
if (!config.windowDecorations) { window.styleMask.remove(.titled) }
|
||||
|
||||
// Terminals typically operate in sRGB color space and macOS defaults
|
||||
// to "native" which is typically P3. There is a lot more resources
|
||||
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
|
||||
// Ghostty defaults to sRGB but this can be overridden.
|
||||
switch (config.windowColorspace) {
|
||||
case "display-p3":
|
||||
window.colorSpace = .displayP3
|
||||
case "srgb":
|
||||
fallthrough
|
||||
default:
|
||||
window.colorSpace = .sRGB
|
||||
}
|
||||
|
||||
// If we have only a single surface (no splits) and that surface requested
|
||||
// an initial size then we set it here now.
|
||||
if case let .leaf(leaf) = surfaceTree {
|
||||
if let initialSize = leaf.surface.initialSize,
|
||||
let screen = window.screen ?? NSScreen.main {
|
||||
// Setup our frame. We need to first subtract the views frame so that we can
|
||||
// just get the chrome frame so that we only affect the surface view size.
|
||||
// Get the current frame of the window
|
||||
var frame = window.frame
|
||||
frame.size.width -= leaf.surface.frame.size.width
|
||||
frame.size.height -= leaf.surface.frame.size.height
|
||||
frame.size.width += min(initialSize.width, screen.frame.width)
|
||||
frame.size.height += min(initialSize.height, screen.frame.height)
|
||||
|
||||
// We have no tabs and we are not a split, so set the initial size of the window.
|
||||
// Calculate the chrome size (window size minus view size)
|
||||
let chromeWidth = frame.size.width - leaf.surface.frame.size.width
|
||||
let chromeHeight = frame.size.height - leaf.surface.frame.size.height
|
||||
|
||||
// Calculate the new width and height, clamping to the screen's size
|
||||
let newWidth = min(initialSize.width + chromeWidth, screen.visibleFrame.width)
|
||||
let newHeight = min(initialSize.height + chromeHeight, screen.visibleFrame.height)
|
||||
|
||||
// Update the frame size while keeping the window's position intact
|
||||
frame.size.width = newWidth
|
||||
frame.size.height = newHeight
|
||||
|
||||
// Ensure the window doesn't go outside the screen boundaries
|
||||
frame.origin.x = max(screen.frame.origin.x, min(frame.origin.x, screen.frame.maxX - newWidth))
|
||||
frame.origin.y = max(screen.frame.origin.y, min(frame.origin.y, screen.frame.maxY - newHeight))
|
||||
|
||||
// Set the updated frame to the window
|
||||
window.setFrame(frame, display: true)
|
||||
}
|
||||
}
|
||||
@ -508,7 +511,50 @@ class TerminalController: BaseTerminalController {
|
||||
ghostty.newTab(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any) {
|
||||
private func confirmClose(
|
||||
window: NSWindow,
|
||||
messageText: String,
|
||||
informativeText: String,
|
||||
completion: @escaping () -> Void
|
||||
) {
|
||||
// If we need confirmation by any, show one confirmation for all windows
|
||||
// in the tab group.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = messageText
|
||||
alert.informativeText = informativeText
|
||||
alert.addButton(withTitle: "Close")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window) { response in
|
||||
if response == .alertFirstButtonReturn {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func closeTab(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard window.tabGroup != nil else {
|
||||
// No tabs, no tab group, just perform a normal close.
|
||||
window.performClose(sender)
|
||||
return
|
||||
}
|
||||
|
||||
if surfaceTree?.needsConfirmQuit() ?? false {
|
||||
confirmClose(
|
||||
window: window,
|
||||
messageText: "Close Tab?",
|
||||
informativeText: "The terminal still has a running process. If you close the tab the process will be killed."
|
||||
) {
|
||||
window.close()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
window.close()
|
||||
}
|
||||
|
||||
@IBAction override func closeWindow(_ sender: Any?) {
|
||||
guard let window = window else { return }
|
||||
guard let tabGroup = window.tabGroup else {
|
||||
// No tabs, no tab group, just perform a normal close.
|
||||
@ -523,47 +569,34 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
// Check if any windows require close confirmation.
|
||||
var needsConfirm: Bool = false
|
||||
for tabWindow in tabGroup.windows {
|
||||
guard let c = tabWindow.windowController as? TerminalController else { continue }
|
||||
if (c.surfaceTree?.needsConfirmQuit() ?? false) {
|
||||
needsConfirm = true
|
||||
break
|
||||
let needsConfirm = tabGroup.windows.contains { tabWindow in
|
||||
guard let controller = tabWindow.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
return controller.surfaceTree?.needsConfirmQuit() ?? false
|
||||
}
|
||||
|
||||
// If none need confirmation then we can just close all the windows.
|
||||
if (!needsConfirm) {
|
||||
for tabWindow in tabGroup.windows {
|
||||
tabWindow.close()
|
||||
}
|
||||
|
||||
if !needsConfirm {
|
||||
tabGroup.windows.forEach { $0.close() }
|
||||
return
|
||||
}
|
||||
|
||||
// If we need confirmation by any, show one confirmation for all windows
|
||||
// in the tab group.
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Close Window?"
|
||||
alert.informativeText = "All terminal sessions in this window will be terminated."
|
||||
alert.addButton(withTitle: "Close Window")
|
||||
alert.addButton(withTitle: "Cancel")
|
||||
alert.alertStyle = .warning
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
if (response == .alertFirstButtonReturn) {
|
||||
for tabWindow in tabGroup.windows {
|
||||
tabWindow.close()
|
||||
}
|
||||
}
|
||||
})
|
||||
confirmClose(
|
||||
window: window,
|
||||
messageText: "Close Window?",
|
||||
informativeText: "All terminal sessions in this window will be terminated."
|
||||
) {
|
||||
tabGroup.windows.forEach { $0.close() }
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any) {
|
||||
@IBAction func toggleGhosttyFullScreen(_ sender: Any?) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleFullscreen(surface: surface)
|
||||
}
|
||||
|
||||
@IBAction func toggleTerminalInspector(_ sender: Any) {
|
||||
@IBAction func toggleTerminalInspector(_ sender: Any?) {
|
||||
guard let surface = focusedSurface?.surface else { return }
|
||||
ghostty.toggleTerminalInspector(surface: surface)
|
||||
}
|
||||
@ -720,6 +753,12 @@ class TerminalController: BaseTerminalController {
|
||||
targetWindow.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
@objc private func onCloseTab(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard surfaceTree?.contains(view: target) ?? false else { return }
|
||||
closeTab(self)
|
||||
}
|
||||
|
||||
@objc private func onToggleFullscreen(notification: SwiftUI.Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard target == self.focusedSurface else { return }
|
||||
@ -737,7 +776,7 @@ class TerminalController: BaseTerminalController {
|
||||
toggleFullscreen(mode: fullscreenMode)
|
||||
}
|
||||
|
||||
private struct DerivedConfig {
|
||||
struct DerivedConfig {
|
||||
let backgroundColor: Color
|
||||
let macosTitlebarStyle: String
|
||||
|
||||
|
@ -10,7 +10,7 @@ protocol TerminalViewDelegate: AnyObject {
|
||||
|
||||
/// The title of the terminal should change.
|
||||
func titleDidChange(to: String)
|
||||
|
||||
|
||||
/// The URL of the pwd should change.
|
||||
func pwdDidChange(to: URL?)
|
||||
|
||||
@ -56,15 +56,10 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
|
||||
// The title for our window
|
||||
private var title: String {
|
||||
var title = "👻"
|
||||
|
||||
if let surfaceTitle = surfaceTitle {
|
||||
if (surfaceTitle.count > 0) {
|
||||
title = surfaceTitle
|
||||
}
|
||||
if let surfaceTitle, !surfaceTitle.isEmpty {
|
||||
return surfaceTitle
|
||||
}
|
||||
|
||||
return title
|
||||
return "👻"
|
||||
}
|
||||
|
||||
// The pwd of the focused surface as a URL
|
||||
@ -72,7 +67,7 @@ struct TerminalView<ViewModel: TerminalViewModel>: View {
|
||||
guard let surfacePwd, surfacePwd != "" else { return nil }
|
||||
return URL(fileURLWithPath: surfacePwd)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
switch ghostty.readiness {
|
||||
case .loading:
|
||||
|
@ -115,6 +115,21 @@ class TerminalWindow: NSWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// We override this so that with the hidden titlebar style the titlebar
|
||||
// area is not draggable.
|
||||
override var contentLayoutRect: CGRect {
|
||||
var rect = super.contentLayoutRect
|
||||
|
||||
// If we are using a hidden titlebar style, the content layout is the
|
||||
// full frame making it so that it is not draggable.
|
||||
if let controller = windowController as? TerminalController,
|
||||
controller.derivedConfig.macosTitlebarStyle == "hidden" {
|
||||
rect.origin.y = 0
|
||||
rect.size.height = self.frame.height
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
// The window theme configuration from Ghostty. This is used to control some
|
||||
// behaviors that don't look quite right in certain situations.
|
||||
var windowTheme: TerminalWindowTheme?
|
||||
@ -667,12 +682,16 @@ fileprivate class WindowDragView: NSView {
|
||||
|
||||
// A view that matches the color of selected and unselected tabs in the adjacent tab bar.
|
||||
fileprivate class WindowButtonsBackdropView: NSView {
|
||||
private let terminalWindow: TerminalWindow
|
||||
// This must be weak because the window has this view. Otherwise
|
||||
// a retain cycle occurs.
|
||||
private weak var terminalWindow: TerminalWindow?
|
||||
private let isLightTheme: Bool
|
||||
private let overlayLayer = VibrantLayer()
|
||||
|
||||
var isHighlighted: Bool = true {
|
||||
didSet {
|
||||
guard let terminalWindow else { return }
|
||||
|
||||
if isLightTheme {
|
||||
overlayLayer.isHidden = isHighlighted
|
||||
layer?.backgroundColor = .clear
|
||||
|
@ -62,7 +62,7 @@ extension Ghostty {
|
||||
// uses to interface with the application runtime environment.
|
||||
var runtime_cfg = ghostty_runtime_config_s(
|
||||
userdata: Unmanaged.passUnretained(self).toOpaque(),
|
||||
supports_selection_clipboard: false,
|
||||
supports_selection_clipboard: true,
|
||||
wakeup_cb: { userdata in App.wakeup(userdata) },
|
||||
action_cb: { app, target, action in App.action(app!, target: target, action: action) },
|
||||
read_clipboard_cb: { userdata, loc, state in App.readClipboard(userdata, location: loc, state: state) },
|
||||
@ -117,23 +117,7 @@ extension Ghostty {
|
||||
|
||||
func appTick() {
|
||||
guard let app = self.app else { return }
|
||||
|
||||
// Tick our app, which lets us know if we want to quit
|
||||
let exit = ghostty_app_tick(app)
|
||||
if (!exit) { return }
|
||||
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
ghostty_app_tick(app)
|
||||
}
|
||||
|
||||
func openConfig() {
|
||||
@ -336,13 +320,13 @@ extension Ghostty {
|
||||
let surfaceView = self.surfaceUserdata(from: userdata)
|
||||
guard let surface = surfaceView.surface else { return }
|
||||
|
||||
// We only support the standard clipboard
|
||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) {
|
||||
// Get our pasteboard
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else {
|
||||
return completeClipboardRequest(surface, data: "", state: state)
|
||||
}
|
||||
|
||||
// Get our string
|
||||
let str = NSPasteboard.general.getOpinionatedStringContents() ?? ""
|
||||
let str = pasteboard.getOpinionatedStringContents() ?? ""
|
||||
completeClipboardRequest(surface, data: str, state: state)
|
||||
}
|
||||
|
||||
@ -380,14 +364,12 @@ extension Ghostty {
|
||||
static func writeClipboard(_ userdata: UnsafeMutableRawPointer?, string: UnsafePointer<CChar>?, location: ghostty_clipboard_e, confirm: Bool) {
|
||||
let surface = self.surfaceUserdata(from: userdata)
|
||||
|
||||
// We only support the standard clipboard
|
||||
if (location != GHOSTTY_CLIPBOARD_STANDARD) { return }
|
||||
|
||||
guard let pasteboard = NSPasteboard.ghostty(location) else { return }
|
||||
guard let valueStr = String(cString: string!, encoding: .utf8) else { return }
|
||||
if !confirm {
|
||||
let pb = NSPasteboard.general
|
||||
pb.declareTypes([.string], owner: nil)
|
||||
pb.setString(valueStr, forType: .string)
|
||||
pasteboard.declareTypes([.string], owner: nil)
|
||||
pasteboard.setString(valueStr, forType: .string)
|
||||
return
|
||||
}
|
||||
|
||||
@ -396,7 +378,7 @@ extension Ghostty {
|
||||
object: surface,
|
||||
userInfo: [
|
||||
Notification.ConfirmClipboardStrKey: valueStr,
|
||||
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write,
|
||||
Notification.ConfirmClipboardRequestKey: Ghostty.ClipboardRequest.osc_52_write(pasteboard),
|
||||
]
|
||||
)
|
||||
}
|
||||
@ -454,6 +436,9 @@ extension Ghostty {
|
||||
|
||||
// Action dispatch
|
||||
switch (action.tag) {
|
||||
case GHOSTTY_ACTION_QUIT:
|
||||
quit(app)
|
||||
|
||||
case GHOSTTY_ACTION_NEW_WINDOW:
|
||||
newWindow(app, target: target)
|
||||
|
||||
@ -463,6 +448,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_NEW_SPLIT:
|
||||
newSplit(app, target: target, direction: action.action.new_split)
|
||||
|
||||
case GHOSTTY_ACTION_CLOSE_TAB:
|
||||
closeTab(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_TOGGLE_FULLSCREEN:
|
||||
toggleFullscreen(app, target: target, mode: action.action.toggle_fullscreen)
|
||||
|
||||
@ -559,6 +547,21 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func quit(_ app: ghostty_app_t) {
|
||||
// On iOS, applications do not terminate programmatically like they do
|
||||
// on macOS. On iOS, applications are only terminated when a user physically
|
||||
// closes the application (i.e. going to the home screen). If we request
|
||||
// exit on iOS we ignore it.
|
||||
#if os(iOS)
|
||||
logger.info("quit request received, ignoring on iOS")
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
// We want to quit, start that process
|
||||
NSApplication.shared.terminate(nil)
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
@ -651,6 +654,27 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
private static func closeTab(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||
switch (target.tag) {
|
||||
case GHOSTTY_TARGET_APP:
|
||||
Ghostty.logger.warning("close tab 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 }
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: .ghosttyCloseTab,
|
||||
object: surfaceView
|
||||
)
|
||||
|
||||
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
}
|
||||
|
||||
private static func toggleFullscreen(
|
||||
_ app: ghostty_app_t,
|
||||
target: ghostty_target_s,
|
||||
|
@ -132,15 +132,6 @@ extension Ghostty {
|
||||
return v
|
||||
}
|
||||
|
||||
var windowColorspace: String {
|
||||
guard let config = self.config else { return "" }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "window-colorspace"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
|
||||
guard let ptr = v else { return "" }
|
||||
return String(cString: ptr)
|
||||
}
|
||||
|
||||
var windowSaveState: String {
|
||||
guard let config = self.config else { return "" }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
@ -174,11 +165,14 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
var windowDecorations: Bool {
|
||||
guard let config = self.config else { return true }
|
||||
var v = false;
|
||||
let defaultValue = true
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "window-decoration"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v;
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return defaultValue }
|
||||
guard let ptr = v else { return defaultValue }
|
||||
let str = String(cString: ptr)
|
||||
return WindowDecoration(rawValue: str)?.enabled() ?? defaultValue
|
||||
}
|
||||
|
||||
var windowTheme: String? {
|
||||
@ -345,7 +339,7 @@ extension Ghostty {
|
||||
var backgroundBlurRadius: Int {
|
||||
guard let config = self.config else { return 1 }
|
||||
var v: Int = 0
|
||||
let key = "background-blur-radius"
|
||||
let key = "background-blur"
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v;
|
||||
}
|
||||
@ -375,13 +369,24 @@ extension Ghostty {
|
||||
)
|
||||
}
|
||||
|
||||
// This isn't actually a configurable value currently but it could be done day.
|
||||
// We put it here because it is a color that changes depending on the configuration.
|
||||
var splitDividerColor: Color {
|
||||
let backgroundColor = OSColor(backgroundColor)
|
||||
let isLightBackground = backgroundColor.isLightColor
|
||||
let newColor = isLightBackground ? backgroundColor.darken(by: 0.08) : backgroundColor.darken(by: 0.4)
|
||||
return Color(newColor)
|
||||
|
||||
guard let config = self.config else { return Color(newColor) }
|
||||
|
||||
var color: ghostty_config_color_s = .init();
|
||||
let key = "split-divider-color"
|
||||
if (!ghostty_config_get(config, &color, key, UInt(key.count))) {
|
||||
return Color(newColor)
|
||||
}
|
||||
|
||||
return .init(
|
||||
red: Double(color.r) / 255,
|
||||
green: Double(color.g) / 255,
|
||||
blue: Double(color.b) / 255
|
||||
)
|
||||
}
|
||||
|
||||
#if canImport(AppKit)
|
||||
@ -420,6 +425,16 @@ extension Ghostty {
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
|
||||
var quickTerminalSpaceBehavior: QuickTerminalSpaceBehavior {
|
||||
guard let config = self.config else { return .move }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "quick-terminal-space-behavior"
|
||||
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .move }
|
||||
guard let ptr = v else { return .move }
|
||||
let str = String(cString: ptr)
|
||||
return QuickTerminalSpaceBehavior(fromGhosttyConfig: str) ?? .move
|
||||
}
|
||||
#endif
|
||||
|
||||
var resizeOverlay: ResizeOverlay {
|
||||
@ -542,4 +557,18 @@ extension Ghostty.Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum WindowDecoration: String {
|
||||
case none
|
||||
case client
|
||||
case server
|
||||
case auto
|
||||
|
||||
func enabled() -> Bool {
|
||||
switch self {
|
||||
case .client, .server, .auto: return true
|
||||
case .none: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
15
macos/Sources/Ghostty/Ghostty.Event.swift
Normal file
15
macos/Sources/Ghostty/Ghostty.Event.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// A comparable event.
|
||||
struct ComparableKeyEvent: Equatable {
|
||||
let keyCode: UInt16
|
||||
let flags: NSEvent.ModifierFlags
|
||||
|
||||
init(event: NSEvent) {
|
||||
self.keyCode = event.keyCode
|
||||
self.flags = event.modifierFlags
|
||||
}
|
||||
}
|
||||
}
|
@ -205,6 +205,7 @@ extension Ghostty {
|
||||
alert.beginSheetModal(for: window, completionHandler: { response in
|
||||
switch (response) {
|
||||
case .alertFirstButtonReturn:
|
||||
alert.window.orderOut(nil)
|
||||
node = nil
|
||||
|
||||
default:
|
||||
|
15
macos/Sources/Ghostty/NSEvent+Extension.swift
Normal file
15
macos/Sources/Ghostty/NSEvent+Extension.swift
Normal file
@ -0,0 +1,15 @@
|
||||
import Cocoa
|
||||
import GhosttyKit
|
||||
|
||||
extension NSEvent {
|
||||
/// Create a Ghostty key event for a given keyboard action.
|
||||
func ghosttyKeyEvent(_ action: ghostty_input_action_e) -> ghostty_input_key_s {
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(modifierFlags)
|
||||
key_ev.keycode = UInt32(keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
return key_ev
|
||||
}
|
||||
}
|
@ -159,7 +159,7 @@ extension Ghostty {
|
||||
case osc_52_read
|
||||
|
||||
/// An application is attempting to write to the clipboard using OSC 52
|
||||
case osc_52_write
|
||||
case osc_52_write(OSPasteboard?)
|
||||
|
||||
/// The text to show in the clipboard confirmation prompt for a given request type
|
||||
func text() -> String {
|
||||
@ -188,7 +188,7 @@ extension Ghostty {
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_READ:
|
||||
return .osc_52_read
|
||||
case GHOSTTY_CLIPBOARD_REQUEST_OSC_52_WRITE:
|
||||
return .osc_52_write
|
||||
return .osc_52_write(nil)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@ -236,6 +236,9 @@ extension Notification.Name {
|
||||
/// Goto tab. Has tab index in the userinfo.
|
||||
static let ghosttyMoveTab = Notification.Name("com.mitchellh.ghostty.moveTab")
|
||||
static let GhosttyMoveTabKey = ghosttyMoveTab.rawValue
|
||||
|
||||
/// Close tab
|
||||
static let ghosttyCloseTab = Notification.Name("com.mitchellh.ghostty.closeTab")
|
||||
}
|
||||
|
||||
// NOTE: I am moving all of these to Notification.Name extensions over time. This
|
||||
|
@ -92,22 +92,6 @@ extension Ghostty {
|
||||
windowFocus = false
|
||||
}
|
||||
}
|
||||
.onDrop(of: [.fileURL], isTargeted: nil) { providers in
|
||||
providers.forEach { provider in
|
||||
_ = provider.loadObject(ofClass: URL.self) { url, _ in
|
||||
guard let url = url else { return }
|
||||
let path = Shell.escape(url.path)
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.insertText(
|
||||
path,
|
||||
replacementRange: NSMakeRange(0, 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
// If our geo size changed then we show the resize overlay as configured.
|
||||
|
@ -1,3 +1,4 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
import CoreText
|
||||
import UserNotifications
|
||||
@ -12,7 +13,14 @@ extension Ghostty {
|
||||
// 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
|
||||
// to the app level and it is set from there.
|
||||
@Published private(set) var title: String = "👻"
|
||||
@Published private(set) var title: String = "" {
|
||||
didSet {
|
||||
if !title.isEmpty {
|
||||
titleFallbackTimer?.invalidate()
|
||||
titleFallbackTimer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The current pwd of the surface as defined by the pty. This can be
|
||||
// changed with escape codes.
|
||||
@ -113,6 +121,12 @@ extension Ghostty {
|
||||
// A small delay that is introduced before a title change to avoid flickers
|
||||
private var titleChangeTimer: Timer?
|
||||
|
||||
// A timer to fallback to ghost emoji if no title is set within the grace period
|
||||
private var titleFallbackTimer: Timer?
|
||||
|
||||
/// Event monitor (see individual events for why)
|
||||
private var eventMonitor: Any? = nil
|
||||
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
|
||||
@ -136,6 +150,13 @@ extension Ghostty {
|
||||
// can do SOMETHING.
|
||||
super.init(frame: NSMakeRect(0, 0, 800, 600))
|
||||
|
||||
// Set a timer to show the ghost emoji after 500ms if no title is set
|
||||
titleFallbackTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { [weak self] _ in
|
||||
if let self = self, self.title.isEmpty {
|
||||
self.title = "👻"
|
||||
}
|
||||
}
|
||||
|
||||
// Before we initialize the surface we want to register our notifications
|
||||
// so there is no window where we can't receive them.
|
||||
let center = NotificationCenter.default
|
||||
@ -170,6 +191,15 @@ extension Ghostty {
|
||||
name: NSWindow.didChangeScreenNotification,
|
||||
object: nil)
|
||||
|
||||
// Listen for local events that we need to know of outside of
|
||||
// single surface handlers.
|
||||
self.eventMonitor = NSEvent.addLocalMonitorForEvents(
|
||||
matching: [
|
||||
// We need keyUp because command+key events don't trigger keyUp.
|
||||
.keyUp
|
||||
]
|
||||
) { [weak self] event in self?.localEventHandler(event) }
|
||||
|
||||
// Setup our surface. This will also initialize all the terminal IO.
|
||||
let surface_cfg = baseConfig ?? SurfaceConfiguration()
|
||||
var surface_cfg_c = surface_cfg.ghosttyConfig(view: self)
|
||||
@ -201,6 +231,9 @@ extension Ghostty {
|
||||
|
||||
ghostty_surface_set_color_scheme(surface, scheme)
|
||||
}
|
||||
|
||||
// The UTTypes that can be dragged onto this view.
|
||||
registerForDraggedTypes(Array(Self.dropTypes))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -212,6 +245,11 @@ extension Ghostty {
|
||||
let center = NotificationCenter.default
|
||||
center.removeObserver(self)
|
||||
|
||||
// Remove our event monitor
|
||||
if let eventMonitor {
|
||||
NSEvent.removeMonitor(eventMonitor)
|
||||
}
|
||||
|
||||
// Whenever the surface is removed, we need to note that our restorable
|
||||
// state is invalid to prevent the surface from being restored.
|
||||
invalidateRestorableState()
|
||||
@ -356,6 +394,30 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Local Events
|
||||
|
||||
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
|
||||
return switch event.type {
|
||||
case .keyUp:
|
||||
localEventKeyUp(event)
|
||||
|
||||
default:
|
||||
event
|
||||
}
|
||||
}
|
||||
|
||||
private func localEventKeyUp(_ event: NSEvent) -> NSEvent? {
|
||||
// We only care about events with "command" because all others will
|
||||
// trigger the normal responder chain.
|
||||
if (!event.modifierFlags.contains(.command)) { return event }
|
||||
|
||||
// Command keyUp events are never sent to the normal responder chain
|
||||
// so we send them here.
|
||||
guard focused else { return event }
|
||||
self.keyUp(with: event)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc private func onUpdateRendererHealth(notification: SwiftUI.Notification) {
|
||||
@ -764,16 +826,51 @@ extension Ghostty {
|
||||
// know if these events cleared it.
|
||||
let markedTextBefore = markedText.length > 0
|
||||
|
||||
// We need to know the keyboard layout before below because some keyboard
|
||||
// input events will change our keyboard layout and we don't want those
|
||||
// going to the terminal.
|
||||
let keyboardIdBefore: String? = if (!markedTextBefore) {
|
||||
KeyboardLayout.id
|
||||
} else {
|
||||
nil
|
||||
}
|
||||
|
||||
self.interpretKeyEvents([translationEvent])
|
||||
|
||||
// If our keyboard changed from this we just assume an input method
|
||||
// grabbed it and do nothing.
|
||||
if (!markedTextBefore && keyboardIdBefore != KeyboardLayout.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// If we have text, then we've composed a character, send that down. We do this
|
||||
// first because if we completed a preedit, the text will be available here
|
||||
// AND we'll have a preedit.
|
||||
var handled: Bool = false
|
||||
if let list = keyTextAccumulator, list.count > 0 {
|
||||
handled = true
|
||||
for text in list {
|
||||
keyAction(action, event: event, text: text)
|
||||
|
||||
// This is a hack. libghostty on macOS treats ctrl input as not having
|
||||
// text because some keyboard layouts generate bogus characters for
|
||||
// ctrl+key. libghostty can't tell this is from an IM keyboard giving
|
||||
// us direct values. So, we just remove control.
|
||||
var modifierFlags = event.modifierFlags
|
||||
modifierFlags.remove(.control)
|
||||
if let keyTextEvent = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: event.locationInWindow,
|
||||
modifierFlags: modifierFlags,
|
||||
timestamp: event.timestamp,
|
||||
windowNumber: event.windowNumber,
|
||||
context: nil,
|
||||
characters: event.characters ?? "",
|
||||
charactersIgnoringModifiers: event.charactersIgnoringModifiers ?? "",
|
||||
isARepeat: event.isARepeat,
|
||||
keyCode: event.keyCode
|
||||
) {
|
||||
for text in list {
|
||||
_ = keyAction(action, event: keyTextEvent, text: text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -783,38 +880,49 @@ extension Ghostty {
|
||||
// the preedit.
|
||||
if (markedText.length > 0 || markedTextBefore) {
|
||||
handled = true
|
||||
keyAction(action, event: event, preedit: markedText.string)
|
||||
_ = keyAction(action, event: event, preedit: markedText.string)
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
// No text or anything, we want to handle this manually.
|
||||
keyAction(action, event: event)
|
||||
_ = keyAction(action, event: event)
|
||||
}
|
||||
}
|
||||
|
||||
override func keyUp(with event: NSEvent) {
|
||||
keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
_ = keyAction(GHOSTTY_ACTION_RELEASE, event: event)
|
||||
}
|
||||
|
||||
/// Special case handling for some control keys
|
||||
override func performKeyEquivalent(with event: NSEvent) -> Bool {
|
||||
// Only process key down events
|
||||
if (event.type != .keyDown) {
|
||||
switch (event.type) {
|
||||
case .keyDown:
|
||||
// Continue, we care about key down events
|
||||
break
|
||||
|
||||
default:
|
||||
// Any other key event we don't care about. I don't think its even
|
||||
// possible to receive any other event type.
|
||||
return false
|
||||
}
|
||||
|
||||
// Only process events if we're focused. Some key events like C-/ macOS
|
||||
// appears to send to the first view in the hierarchy rather than the
|
||||
// the first responder (I don't know why). This prevents us from handling it.
|
||||
// Besides C-/, its important we don't process key equivalents if unfocused
|
||||
// because there are other event listeners for that (i.e. AppDelegate's
|
||||
// local event handler).
|
||||
if (!focused) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Only process keys when Control is active. All known issues we're
|
||||
// resolving happen only in this scenario. This probably isn't fully robust
|
||||
// but we can broaden the scope as we find more cases.
|
||||
if (!event.modifierFlags.contains(.control)) {
|
||||
return false
|
||||
// If this event as-is would result in a key binding then we send it.
|
||||
if let surface,
|
||||
ghostty_surface_key_is_binding(
|
||||
surface,
|
||||
event.ghosttyKeyEvent(GHOSTTY_ACTION_PRESS)) {
|
||||
self.keyDown(with: event)
|
||||
return true
|
||||
}
|
||||
|
||||
let equivalent: String
|
||||
@ -832,14 +940,25 @@ extension Ghostty {
|
||||
case "\r":
|
||||
// Pass C-<return> through verbatim
|
||||
// (prevent the default context menu equivalent)
|
||||
if (!event.modifierFlags.contains(.control)) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "\r"
|
||||
|
||||
case ".":
|
||||
if (!event.modifierFlags.contains(.command)) {
|
||||
return false
|
||||
}
|
||||
|
||||
equivalent = "."
|
||||
|
||||
default:
|
||||
// Ignore other events
|
||||
return false
|
||||
}
|
||||
|
||||
let newEvent = NSEvent.keyEvent(
|
||||
let finalEvent = NSEvent.keyEvent(
|
||||
with: .keyDown,
|
||||
location: event.locationInWindow,
|
||||
modifierFlags: event.modifierFlags,
|
||||
@ -852,7 +971,7 @@ extension Ghostty {
|
||||
keyCode: event.keyCode
|
||||
)
|
||||
|
||||
self.keyDown(with: newEvent!)
|
||||
self.keyDown(with: finalEvent!)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -867,6 +986,9 @@ extension Ghostty {
|
||||
default: return
|
||||
}
|
||||
|
||||
// If we're in the middle of a preedit, don't do anything with mods.
|
||||
if hasMarkedText() { return }
|
||||
|
||||
// The keyAction function will do this AGAIN below which sucks to repeat
|
||||
// but this is super cheap and flagsChanged isn't that common.
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
@ -897,45 +1019,38 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
keyAction(action, event: event)
|
||||
_ = keyAction(action, event: event)
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
key_ev.text = nil
|
||||
key_ev.composing = false
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
return ghostty_surface_key(surface, event.ghosttyKeyEvent(action))
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, preedit: String) {
|
||||
guard let surface = self.surface else { return }
|
||||
private func keyAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
event: NSEvent, preedit: String
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
preedit.withCString { ptr in
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
return preedit.withCString { ptr in
|
||||
var key_ev = event.ghosttyKeyEvent(action)
|
||||
key_ev.text = ptr
|
||||
key_ev.composing = true
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent, text: String) {
|
||||
guard let surface = self.surface else { return }
|
||||
private func keyAction(
|
||||
_ action: ghostty_input_action_e,
|
||||
event: NSEvent, text: String
|
||||
) -> Bool {
|
||||
guard let surface = self.surface else { return false }
|
||||
|
||||
text.withCString { ptr in
|
||||
var key_ev = ghostty_input_key_s()
|
||||
key_ev.action = action
|
||||
key_ev.mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
key_ev.keycode = UInt32(event.keyCode)
|
||||
return text.withCString { ptr in
|
||||
var key_ev = event.ghosttyKeyEvent(action)
|
||||
key_ev.text = ptr
|
||||
ghostty_surface_key(surface, key_ev)
|
||||
return ghostty_surface_key(surface, key_ev)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1053,6 +1168,14 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func pasteSelection(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "paste_from_selection"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
AppDelegate.logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction override func selectAll(_ sender: Any?) {
|
||||
guard let surface = self.surface else { return }
|
||||
let action = "select_all"
|
||||
@ -1374,3 +1497,78 @@ extension Ghostty.SurfaceView: NSServicesMenuRequestor {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSMenuItemValidation
|
||||
|
||||
extension Ghostty.SurfaceView: NSMenuItemValidation {
|
||||
func validateMenuItem(_ item: NSMenuItem) -> Bool {
|
||||
switch item.action {
|
||||
case #selector(pasteSelection):
|
||||
let pb = NSPasteboard.ghosttySelection
|
||||
guard let str = pb.getOpinionatedStringContents() else { return false }
|
||||
return !str.isEmpty
|
||||
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NSDraggingDestination
|
||||
|
||||
extension Ghostty.SurfaceView {
|
||||
static let dropTypes: Set<NSPasteboard.PasteboardType> = [
|
||||
.string,
|
||||
.fileURL,
|
||||
.URL
|
||||
]
|
||||
|
||||
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
guard let types = sender.draggingPasteboard.types else { return [] }
|
||||
|
||||
// If the dragging object contains none of our types then we return none.
|
||||
// This shouldn't happen because AppKit should guarantee that we only
|
||||
// receive types we registered for but its good to check.
|
||||
if Set(types).isDisjoint(with: Self.dropTypes) {
|
||||
return []
|
||||
}
|
||||
|
||||
// We use copy to get the proper icon
|
||||
return .copy
|
||||
}
|
||||
|
||||
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
|
||||
let pb = sender.draggingPasteboard
|
||||
|
||||
let content: String?
|
||||
if let url = pb.string(forType: .URL) {
|
||||
// URLs first, they get escaped as-is.
|
||||
content = Ghostty.Shell.escape(url)
|
||||
} else if let urls = pb.readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
urls.count > 0 {
|
||||
// File URLs next. They get escaped individually and then joined by a
|
||||
// space if there are multiple.
|
||||
content = urls
|
||||
.map { Ghostty.Shell.escape($0.path) }
|
||||
.joined(separator: " ")
|
||||
} else if let str = pb.string(forType: .string) {
|
||||
// Strings are not escaped because they may be copy/pasting a
|
||||
// command they want to execute.
|
||||
content = str
|
||||
} else {
|
||||
content = nil
|
||||
}
|
||||
|
||||
if let content {
|
||||
DispatchQueue.main.async {
|
||||
self.insertText(
|
||||
content,
|
||||
replacementRange: NSMakeRange(0, 0)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import AppKit
|
||||
typealias OSView = NSView
|
||||
typealias OSColor = NSColor
|
||||
typealias OSSize = NSSize
|
||||
typealias OSPasteboard = NSPasteboard
|
||||
|
||||
protocol OSViewRepresentable: NSViewRepresentable where NSViewType == OSViewType {
|
||||
associatedtype OSViewType: NSView
|
||||
@ -34,6 +35,7 @@ import UIKit
|
||||
typealias OSView = UIView
|
||||
typealias OSColor = UIColor
|
||||
typealias OSSize = CGSize
|
||||
typealias OSPasteboard = UIPasteboard
|
||||
|
||||
protocol OSViewRepresentable: UIViewRepresentable {
|
||||
associatedtype OSViewType: UIView
|
||||
|
33
macos/Sources/Helpers/Dock.swift
Normal file
33
macos/Sources/Helpers/Dock.swift
Normal file
@ -0,0 +1,33 @@
|
||||
import Cocoa
|
||||
|
||||
// Private API to get Dock location
|
||||
@_silgen_name("CoreDockGetOrientationAndPinning")
|
||||
func CoreDockGetOrientationAndPinning(
|
||||
_ outOrientation: UnsafeMutablePointer<Int32>,
|
||||
_ outPinning: UnsafeMutablePointer<Int32>)
|
||||
|
||||
// Private API to get the current Dock auto-hide state
|
||||
@_silgen_name("CoreDockGetAutoHideEnabled")
|
||||
func CoreDockGetAutoHideEnabled() -> Bool
|
||||
|
||||
enum DockOrientation: Int {
|
||||
case top = 1
|
||||
case bottom = 2
|
||||
case left = 3
|
||||
case right = 4
|
||||
}
|
||||
|
||||
class Dock {
|
||||
/// Returns the orientation of the dock or nil if it can't be determined.
|
||||
static var orientation: DockOrientation? {
|
||||
var orientation: Int32 = 0
|
||||
var pinning: Int32 = 0
|
||||
CoreDockGetOrientationAndPinning(&orientation, &pinning)
|
||||
return .init(rawValue: Int(orientation)) ?? nil
|
||||
}
|
||||
|
||||
/// Returns true if the dock has auto-hide enabled.
|
||||
static var autoHideEnabled: Bool {
|
||||
return CoreDockGetAutoHideEnabled()
|
||||
}
|
||||
}
|
@ -307,21 +307,21 @@ class NonNativeFullscreen: FullscreenBase, FullscreenStyle {
|
||||
// MARK: Dock
|
||||
|
||||
private func hideDock() {
|
||||
NSApp.presentationOptions.insert(.autoHideDock)
|
||||
NSApp.acquirePresentationOption(.autoHideDock)
|
||||
}
|
||||
|
||||
private func unhideDock() {
|
||||
NSApp.presentationOptions.remove(.autoHideDock)
|
||||
NSApp.releasePresentationOption(.autoHideDock)
|
||||
}
|
||||
|
||||
// MARK: Menu
|
||||
|
||||
func hideMenu() {
|
||||
NSApp.presentationOptions.insert(.autoHideMenuBar)
|
||||
NSApp.acquirePresentationOption(.autoHideMenuBar)
|
||||
}
|
||||
|
||||
func unhideMenu() {
|
||||
NSApp.presentationOptions.remove(.autoHideMenuBar)
|
||||
NSApp.releasePresentationOption(.autoHideMenuBar)
|
||||
}
|
||||
|
||||
/// The state that must be saved for non-native fullscreen to exit fullscreen.
|
||||
|
14
macos/Sources/Helpers/KeyboardLayout.swift
Normal file
14
macos/Sources/Helpers/KeyboardLayout.swift
Normal file
@ -0,0 +1,14 @@
|
||||
import Carbon
|
||||
|
||||
class KeyboardLayout {
|
||||
/// Return a string ID of the current keyboard input source.
|
||||
static var id: String? {
|
||||
if let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
|
||||
let sourceIdPointer = TISGetInputSourceProperty(source, kTISPropertyInputSourceID) {
|
||||
let sourceId = unsafeBitCast(sourceIdPointer, to: CFString.self)
|
||||
return sourceId as String
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
31
macos/Sources/Helpers/NSApplication+Extension.swift
Normal file
31
macos/Sources/Helpers/NSApplication+Extension.swift
Normal file
@ -0,0 +1,31 @@
|
||||
import Cocoa
|
||||
|
||||
extension NSApplication {
|
||||
private static var presentationOptionCounts: [NSApplication.PresentationOptions.Element: UInt] = [:]
|
||||
|
||||
/// Add a presentation option to the application and main a reference count so that and equal
|
||||
/// number of pops is required to disable it. This is useful so that multiple classes can affect global
|
||||
/// app state without overriding others.
|
||||
func acquirePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
|
||||
Self.presentationOptionCounts[option, default: 0] += 1
|
||||
presentationOptions.insert(option)
|
||||
}
|
||||
|
||||
/// See acquirePresentationOption
|
||||
func releasePresentationOption(_ option: NSApplication.PresentationOptions.Element) {
|
||||
guard let value = Self.presentationOptionCounts[option] else { return }
|
||||
guard value > 0 else { return }
|
||||
if (value == 1) {
|
||||
presentationOptions.remove(option)
|
||||
Self.presentationOptionCounts.removeValue(forKey: option)
|
||||
} else {
|
||||
Self.presentationOptionCounts[option] = value - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSApplication.PresentationOptions.Element: @retroactive Hashable {
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(rawValue)
|
||||
}
|
||||
}
|
@ -1,17 +1,39 @@
|
||||
import AppKit
|
||||
import GhosttyKit
|
||||
|
||||
extension NSPasteboard {
|
||||
/// The pasteboard to used for Ghostty selection.
|
||||
static var ghosttySelection: NSPasteboard = {
|
||||
NSPasteboard(name: .init("com.mitchellh.ghostty.selection"))
|
||||
}()
|
||||
|
||||
/// Gets the contents of the pasteboard as a string following a specific set of semantics.
|
||||
/// Does these things in order:
|
||||
/// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one.
|
||||
/// - Tries to get the absolute filesystem path of the file in the pasteboard if there is one and ensures the file path is properly escaped.
|
||||
/// - Tries to get any string from the pasteboard.
|
||||
/// If all of the above fail, returns None.
|
||||
func getOpinionatedStringContents() -> String? {
|
||||
if let file = self.string(forType: .fileURL) {
|
||||
if let path = NSURL(string: file)?.path {
|
||||
return path
|
||||
}
|
||||
if let urls = readObjects(forClasses: [NSURL.self]) as? [URL],
|
||||
urls.count > 0 {
|
||||
return urls
|
||||
.map { $0.isFileURL ? Ghostty.Shell.escape($0.path) : $0.absoluteString }
|
||||
.joined(separator: " ")
|
||||
}
|
||||
|
||||
return self.string(forType: .string)
|
||||
}
|
||||
|
||||
/// The pasteboard for the Ghostty enum type.
|
||||
static func ghostty(_ clipboard: ghostty_clipboard_e) -> NSPasteboard? {
|
||||
switch (clipboard) {
|
||||
case GHOSTTY_CLIPBOARD_STANDARD:
|
||||
return Self.general
|
||||
|
||||
case GHOSTTY_CLIPBOARD_SELECTION:
|
||||
return Self.ghosttySelection
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
9
macos/Sources/Helpers/Weak.swift
Normal file
9
macos/Sources/Helpers/Weak.swift
Normal file
@ -0,0 +1,9 @@
|
||||
/// A wrapper that holds a weak reference to an object. This lets us create native containers
|
||||
/// of weak references.
|
||||
class Weak<T: AnyObject> {
|
||||
weak var value: T?
|
||||
|
||||
init(_ value: T) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
@ -51,6 +51,9 @@
|
||||
pandoc,
|
||||
hyperfine,
|
||||
typos,
|
||||
wayland,
|
||||
wayland-scanner,
|
||||
wayland-protocols,
|
||||
}: let
|
||||
# See package.nix. Keep in sync.
|
||||
rpathLibs =
|
||||
@ -80,6 +83,7 @@
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
wayland
|
||||
];
|
||||
in
|
||||
mkShell {
|
||||
@ -153,6 +157,9 @@ in
|
||||
libadwaita
|
||||
gtk4
|
||||
glib
|
||||
wayland
|
||||
wayland-scanner
|
||||
wayland-protocols
|
||||
];
|
||||
|
||||
# This should be set onto the rpath of the ghostty binary if you want
|
||||
|
@ -10,10 +10,6 @@
|
||||
oniguruma,
|
||||
zlib,
|
||||
libGL,
|
||||
libX11,
|
||||
libXcursor,
|
||||
libXi,
|
||||
libXrandr,
|
||||
glib,
|
||||
gtk4,
|
||||
libadwaita,
|
||||
@ -26,7 +22,15 @@
|
||||
pandoc,
|
||||
revision ? "dirty",
|
||||
optimize ? "Debug",
|
||||
x11 ? true,
|
||||
enableX11 ? true,
|
||||
libX11,
|
||||
libXcursor,
|
||||
libXi,
|
||||
libXrandr,
|
||||
enableWayland ? true,
|
||||
wayland,
|
||||
wayland-protocols,
|
||||
wayland-scanner,
|
||||
}: let
|
||||
# The Zig hook has no way to select the release type without actual
|
||||
# overriding of the default flags.
|
||||
@ -49,7 +53,6 @@
|
||||
fileset = lib.fileset.intersection (lib.fileset.fromSource (lib.sources.cleanSource ../.)) (
|
||||
lib.fileset.unions [
|
||||
../dist/linux
|
||||
../conformance
|
||||
../images
|
||||
../include
|
||||
../pkg
|
||||
@ -114,14 +117,19 @@ in
|
||||
version = "1.0.2";
|
||||
inherit src;
|
||||
|
||||
nativeBuildInputs = [
|
||||
git
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
];
|
||||
nativeBuildInputs =
|
||||
[
|
||||
git
|
||||
ncurses
|
||||
pandoc
|
||||
pkg-config
|
||||
zig_hook
|
||||
wrapGAppsHook4
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland-scanner
|
||||
wayland-protocols
|
||||
];
|
||||
|
||||
buildInputs =
|
||||
[
|
||||
@ -142,16 +150,19 @@ in
|
||||
glib
|
||||
gsettings-desktop-schemas
|
||||
]
|
||||
++ lib.optionals x11 [
|
||||
++ lib.optionals enableX11 [
|
||||
libX11
|
||||
libXcursor
|
||||
libXi
|
||||
libXrandr
|
||||
]
|
||||
++ lib.optionals enableWayland [
|
||||
wayland
|
||||
];
|
||||
|
||||
dontConfigure = true;
|
||||
|
||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString x11}";
|
||||
zigBuildFlags = "-Dversion-string=${finalAttrs.version}-${revision}-nix -Dgtk-x11=${lib.boolToString enableX11} -Dgtk-wayland=${lib.boolToString enableWayland}";
|
||||
|
||||
preBuild = ''
|
||||
rm -rf $ZIG_GLOBAL_CACHE_DIR
|
||||
|
18
nix/vm/common-cinnamon.nix
Normal file
18
nix/vm/common-cinnamon.nix
Normal file
@ -0,0 +1,18 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
lightdm = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
cinnamon = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
136
nix/vm/common-gnome.nix
Normal file
136
nix/vm/common-gnome.nix
Normal file
@ -0,0 +1,136 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
gdm = {
|
||||
enable = true;
|
||||
autoSuspend = false;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
gnome = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.gnomeExtensions.no-overview
|
||||
];
|
||||
|
||||
environment.gnome.excludePackages = with pkgs; [
|
||||
atomix
|
||||
baobab
|
||||
cheese
|
||||
epiphany
|
||||
evince
|
||||
file-roller
|
||||
geary
|
||||
gnome-backgrounds
|
||||
gnome-calculator
|
||||
gnome-calendar
|
||||
gnome-clocks
|
||||
gnome-connections
|
||||
gnome-contacts
|
||||
gnome-disk-utility
|
||||
gnome-extension-manager
|
||||
gnome-logs
|
||||
gnome-maps
|
||||
gnome-music
|
||||
gnome-photos
|
||||
gnome-software
|
||||
gnome-system-monitor
|
||||
gnome-text-editor
|
||||
gnome-themes-extra
|
||||
gnome-tour
|
||||
gnome-user-docs
|
||||
gnome-weather
|
||||
hitori
|
||||
iagno
|
||||
loupe
|
||||
nautilus
|
||||
orca
|
||||
seahorse
|
||||
simple-scan
|
||||
snapshot
|
||||
sushi
|
||||
tali
|
||||
totem
|
||||
yelp
|
||||
];
|
||||
|
||||
programs.dconf = {
|
||||
enable = true;
|
||||
profiles.user.databases = [
|
||||
{
|
||||
settings = with lib.gvariant; {
|
||||
"org/gnome/desktop/background" = {
|
||||
picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-uri-dark = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-options = "centered";
|
||||
primary-color = "#000000000000";
|
||||
secondary-color = "#000000000000";
|
||||
};
|
||||
"org/gnome/desktop/interface" = {
|
||||
color-scheme = "prefer-dark";
|
||||
};
|
||||
"org/gnome/desktop/notifications" = {
|
||||
show-in-lock-screen = false;
|
||||
};
|
||||
"org/gnome/desktop/screensaver" = {
|
||||
lock-enabled = false;
|
||||
picture-uri = "file://${pkgs.ghostty}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png";
|
||||
picture-options = "centered";
|
||||
primary-color = "#000000000000";
|
||||
secondary-color = "#000000000000";
|
||||
};
|
||||
"org/gnome/desktop/session" = {
|
||||
idle-delay = mkUint32 0;
|
||||
};
|
||||
"org/gnome/shell" = {
|
||||
disable-user-extensions = false;
|
||||
enabled-extensions = builtins.map (x: x.extensionUuid) (
|
||||
lib.filter (p: p ? extensionUuid) config.environment.systemPackages
|
||||
);
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
programs.geary.enable = false;
|
||||
|
||||
services.gnome = {
|
||||
gnome-browser-connector.enable = false;
|
||||
gnome-initial-setup.enable = false;
|
||||
gnome-online-accounts.enable = false;
|
||||
gnome-remote-desktop.enable = false;
|
||||
rygel.enable = false;
|
||||
};
|
||||
|
||||
system.activationScripts = {
|
||||
face = {
|
||||
text = ''
|
||||
mkdir -p /var/lib/AccountsService/{icons,users}
|
||||
|
||||
cp ${pkgs.ghostty}/share/icons/hicolor/1024x1024/apps/com.mitchellh.ghostty.png /var/lib/AccountsService/icons/ghostty
|
||||
|
||||
echo -e "[User]\nIcon=/var/lib/AccountsService/icons/ghostty\n" > /var/lib/AccountsService/users/ghostty
|
||||
|
||||
chown root:root /var/lib/AccountsService/users/ghostty
|
||||
chmod 0600 /var/lib/AccountsService/users/ghostty
|
||||
|
||||
chown root:root /var/lib/AccountsService/icons/ghostty
|
||||
chmod 0444 /var/lib/AccountsService/icons/ghostty
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
21
nix/vm/common-plasma6.nix
Normal file
21
nix/vm/common-plasma6.nix
Normal file
@ -0,0 +1,21 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services = {
|
||||
displayManager = {
|
||||
sddm = {
|
||||
enable = true;
|
||||
wayland = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
plasma6 = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
18
nix/vm/common-xfce.nix
Normal file
18
nix/vm/common-xfce.nix
Normal file
@ -0,0 +1,18 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common.nix
|
||||
];
|
||||
|
||||
services.xserver = {
|
||||
displayManager = {
|
||||
lightdm = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
desktopManager = {
|
||||
xfce = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
83
nix/vm/common.nix
Normal file
83
nix/vm/common.nix
Normal file
@ -0,0 +1,83 @@
|
||||
{pkgs, ...}: {
|
||||
boot.loader.systemd-boot.enable = true;
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
documentation.nixos.enable = false;
|
||||
|
||||
networking.hostName = "ghostty";
|
||||
networking.domain = "mitchellh.com";
|
||||
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.memorySize = 2048;
|
||||
};
|
||||
|
||||
nix = {
|
||||
settings = {
|
||||
trusted-users = [
|
||||
"root"
|
||||
"ghostty"
|
||||
];
|
||||
};
|
||||
extraOptions = ''
|
||||
experimental-features = nix-command flakes
|
||||
'';
|
||||
};
|
||||
|
||||
users.mutableUsers = false;
|
||||
|
||||
users.groups.ghostty = {};
|
||||
|
||||
users.users.ghostty = {
|
||||
description = "Ghostty";
|
||||
group = "ghostty";
|
||||
extraGroups = ["wheel"];
|
||||
isNormalUser = true;
|
||||
initialPassword = "ghostty";
|
||||
};
|
||||
|
||||
environment.etc = {
|
||||
"xdg/autostart/com.mitchellh.ghostty.desktop" = {
|
||||
source = "${pkgs.ghostty}/share/applications/com.mitchellh.ghostty.desktop";
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.kitty
|
||||
pkgs.fish
|
||||
pkgs.ghostty
|
||||
pkgs.helix
|
||||
pkgs.neovim
|
||||
pkgs.xterm
|
||||
pkgs.zsh
|
||||
];
|
||||
|
||||
security.polkit = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.dbus = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.displayManager = {
|
||||
autoLogin = {
|
||||
user = "ghostty";
|
||||
};
|
||||
};
|
||||
|
||||
services.libinput = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.qemuGuest = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.spice-vdagentd = {
|
||||
enable = true;
|
||||
};
|
||||
|
||||
services.xserver = {
|
||||
enable = true;
|
||||
};
|
||||
}
|
12
nix/vm/create-cinnamon.nix
Normal file
12
nix/vm/create-cinnamon.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-cinnamon.nix;
|
||||
}
|
12
nix/vm/create-gnome.nix
Normal file
12
nix/vm/create-gnome.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-gnome.nix;
|
||||
}
|
12
nix/vm/create-plasma6.nix
Normal file
12
nix/vm/create-plasma6.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-plasma6.nix;
|
||||
}
|
12
nix/vm/create-xfce.nix
Normal file
12
nix/vm/create-xfce.nix
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}:
|
||||
import ./create.nix {
|
||||
inherit system nixpkgs overlay module uid gid;
|
||||
common = ./common-xfce.nix;
|
||||
}
|
42
nix/vm/create.nix
Normal file
42
nix/vm/create.nix
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
system,
|
||||
nixpkgs,
|
||||
overlay,
|
||||
module,
|
||||
common ? ./common.nix,
|
||||
uid ? 1000,
|
||||
gid ? 1000,
|
||||
}: let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
overlays = [
|
||||
overlay
|
||||
];
|
||||
};
|
||||
in
|
||||
nixpkgs.lib.nixosSystem {
|
||||
system = builtins.replaceStrings ["darwin"] ["linux"] system;
|
||||
modules = [
|
||||
{
|
||||
virtualisation.vmVariant = {
|
||||
virtualisation.host.pkgs = pkgs;
|
||||
};
|
||||
|
||||
nixpkgs.overlays = [
|
||||
overlay
|
||||
];
|
||||
|
||||
users.groups.ghostty = {
|
||||
gid = gid;
|
||||
};
|
||||
|
||||
users.users.ghostty = {
|
||||
uid = uid;
|
||||
};
|
||||
|
||||
system.stateVersion = nixpkgs.lib.trivial.release;
|
||||
}
|
||||
common
|
||||
module
|
||||
];
|
||||
}
|
7
nix/vm/wayland-cinnamon.nix
Normal file
7
nix/vm/wayland-cinnamon.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-cinnamon.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "cinnamon-wayland";
|
||||
}
|
9
nix/vm/wayland-gnome.nix
Normal file
9
nix/vm/wayland-gnome.nix
Normal file
@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome";
|
||||
};
|
||||
}
|
6
nix/vm/wayland-plasma6.nix
Normal file
6
nix/vm/wayland-plasma6.nix
Normal file
@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-plasma6.nix
|
||||
];
|
||||
services.displayManager.defaultSession = "plasma";
|
||||
}
|
7
nix/vm/x11-cinnamon.nix
Normal file
7
nix/vm/x11-cinnamon.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-cinnamon.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "cinnamon";
|
||||
}
|
9
nix/vm/x11-gnome.nix
Normal file
9
nix/vm/x11-gnome.nix
Normal file
@ -0,0 +1,9 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-gnome.nix
|
||||
];
|
||||
|
||||
services.displayManager = {
|
||||
defaultSession = "gnome-xorg";
|
||||
};
|
||||
}
|
6
nix/vm/x11-plasma6.nix
Normal file
6
nix/vm/x11-plasma6.nix
Normal file
@ -0,0 +1,6 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-plasma6.nix
|
||||
];
|
||||
services.displayManager.defaultSession = "plasmax11";
|
||||
}
|
7
nix/vm/x11-xfce.nix
Normal file
7
nix/vm/x11-xfce.nix
Normal file
@ -0,0 +1,7 @@
|
||||
{...}: {
|
||||
imports = [
|
||||
./common-xfce.nix
|
||||
];
|
||||
|
||||
services.displayManager.defaultSession = "xfce";
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
# This file is auto-generated! check build-support/check-zig-cache-hash.sh for
|
||||
# more details.
|
||||
"sha256-njCce+r1DPTKLNrmrD2ObEoBS9nR7q03hqegQWe1UuY="
|
||||
"sha256-Bjy31evaKgpRX1mGwAFkai44eiiorTV1gW3VdP9Ins8="
|
||||
|
@ -56,7 +56,7 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
@ -186,7 +186,7 @@ pub fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*st
|
||||
_ = b.systemIntegrationOption("freetype", .{}); // So it shows up in help
|
||||
if (freetype_enabled) {
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
lib.linkSystemLibrary2("freetype", dynamic_link_opts);
|
||||
lib.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
const freetype_dep = b.dependency(
|
||||
"freetype",
|
||||
|
@ -5,7 +5,61 @@ pub fn build(b: *std.Build) !void {
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const libpng_enabled = b.option(bool, "enable-libpng", "Build libpng") orelse false;
|
||||
|
||||
const module = b.addModule("freetype", .{ .root_source_file = b.path("main.zig") });
|
||||
const module = b.addModule("freetype", .{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var test_exe: ?*std.Build.Step.Compile = null;
|
||||
if (target.query.isNative()) {
|
||||
test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const tests_run = b.addRunArtifact(test_exe.?);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
|
||||
module.addIncludePath(b.path(""));
|
||||
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
module.linkSystemLibrary("freetype2", dynamic_link_opts);
|
||||
if (test_exe) |exe| {
|
||||
exe.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
}
|
||||
} else {
|
||||
const lib = try buildLib(b, module, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
|
||||
.libpng_enabled = libpng_enabled,
|
||||
|
||||
.dynamic_link_opts = dynamic_link_opts,
|
||||
});
|
||||
|
||||
if (test_exe) |exe| {
|
||||
exe.linkLibrary(lib);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
const libpng_enabled = options.libpng_enabled;
|
||||
|
||||
const upstream = b.dependency("freetype", .{});
|
||||
const lib = b.addStaticLibrary(.{
|
||||
@ -21,16 +75,6 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
|
||||
module.addIncludePath(upstream.path("include"));
|
||||
module.addIncludePath(b.path(""));
|
||||
|
||||
// For dynamic linking, we prefer dynamic linking and to search by
|
||||
// mode first. Mode first will search all paths for a dynamic library
|
||||
// before falling back to static.
|
||||
const dynamic_link_opts: std.Build.Module.LinkSystemLibraryOptions = .{
|
||||
.preferred_link_mode = .dynamic,
|
||||
.search_strategy = .mode_first,
|
||||
};
|
||||
|
||||
var flags = std.ArrayList([]const u8).init(b.allocator);
|
||||
defer flags.deinit();
|
||||
try flags.appendSlice(&.{
|
||||
@ -44,6 +88,8 @@ pub fn build(b: *std.Build) !void {
|
||||
"-fno-sanitize=undefined",
|
||||
});
|
||||
|
||||
const dynamic_link_opts = options.dynamic_link_opts;
|
||||
|
||||
// Zlib
|
||||
if (b.systemIntegrationOption("zlib", .{})) {
|
||||
lib.linkSystemLibrary2("zlib", dynamic_link_opts);
|
||||
@ -113,18 +159,7 @@ pub fn build(b: *std.Build) !void {
|
||||
|
||||
b.installArtifact(lib);
|
||||
|
||||
if (target.query.isNative()) {
|
||||
const test_exe = b.addTest(.{
|
||||
.name = "test",
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
test_exe.linkLibrary(lib);
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
const srcs: []const []const u8 = &.{
|
||||
|
@ -43,7 +43,11 @@ pub fn build(b: *std.Build) !void {
|
||||
{
|
||||
var it = module.import_table.iterator();
|
||||
while (it.next()) |entry| test_exe.root_module.addImport(entry.key_ptr.*, entry.value_ptr.*);
|
||||
test_exe.linkLibrary(freetype.artifact("freetype"));
|
||||
if (b.systemIntegrationOption("freetype", .{})) {
|
||||
test_exe.linkSystemLibrary2("freetype2", dynamic_link_opts);
|
||||
} else {
|
||||
test_exe.linkLibrary(freetype.artifact("freetype"));
|
||||
}
|
||||
const tests_run = b.addRunArtifact(test_exe);
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&tests_run.step);
|
||||
@ -67,7 +71,7 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
|
@ -18,9 +18,72 @@ pub const ColorSpace = opaque {
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn createNamed(name: Name) Allocator.Error!*ColorSpace {
|
||||
return @as(
|
||||
?*ColorSpace,
|
||||
@ptrFromInt(@intFromPtr(c.CGColorSpaceCreateWithName(name.cfstring()))),
|
||||
) orelse Allocator.Error.OutOfMemory;
|
||||
}
|
||||
|
||||
pub fn release(self: *ColorSpace) void {
|
||||
c.CGColorSpaceRelease(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub const Name = enum {
|
||||
/// This color space uses the DCI P3 primaries, a D65 white point, and
|
||||
/// the sRGB transfer function.
|
||||
displayP3,
|
||||
/// The Display P3 color space with a linear transfer function and
|
||||
/// extended-range values.
|
||||
extendedLinearDisplayP3,
|
||||
/// The sRGB colorimetry and non-linear transfer function are specified
|
||||
/// in IEC 61966-2-1.
|
||||
sRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`, but uses a
|
||||
/// linear transfer function.
|
||||
linearSRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`, but you can
|
||||
/// encode component values below `0.0` and above `1.0`. Negative values
|
||||
/// are encoded as the signed reflection of the original encoding
|
||||
/// function, as shown in the formula below:
|
||||
/// ```
|
||||
/// extendedTransferFunction(x) = sign(x) * sRGBTransferFunction(abs(x))
|
||||
/// ```
|
||||
extendedSRGB,
|
||||
/// This color space has the same colorimetry as `sRGB`; in addition,
|
||||
/// you may encode component values below `0.0` and above `1.0`.
|
||||
extendedLinearSRGB,
|
||||
/// ...
|
||||
genericGrayGamma2_2,
|
||||
/// ...
|
||||
linearGray,
|
||||
/// This color space has the same colorimetry as `genericGrayGamma2_2`,
|
||||
/// but you can encode component values below `0.0` and above `1.0`.
|
||||
/// Negative values are encoded as the signed reflection of the
|
||||
/// original encoding function, as shown in the formula below:
|
||||
/// ```
|
||||
/// extendedGrayTransferFunction(x) = sign(x) * gamma22Function(abs(x))
|
||||
/// ```
|
||||
extendedGray,
|
||||
/// This color space has the same colorimetry as `linearGray`; in
|
||||
/// addition, you may encode component values below `0.0` and above `1.0`.
|
||||
extendedLinearGray,
|
||||
|
||||
fn cfstring(self: Name) c.CFStringRef {
|
||||
return switch (self) {
|
||||
.displayP3 => c.kCGColorSpaceDisplayP3,
|
||||
.extendedLinearDisplayP3 => c.kCGColorSpaceExtendedLinearDisplayP3,
|
||||
.sRGB => c.kCGColorSpaceSRGB,
|
||||
.extendedSRGB => c.kCGColorSpaceExtendedSRGB,
|
||||
.linearSRGB => c.kCGColorSpaceLinearSRGB,
|
||||
.extendedLinearSRGB => c.kCGColorSpaceExtendedLinearSRGB,
|
||||
.genericGrayGamma2_2 => c.kCGColorSpaceGenericGrayGamma2_2,
|
||||
.extendedGray => c.kCGColorSpaceExtendedGray,
|
||||
.linearGray => c.kCGColorSpaceLinearGray,
|
||||
.extendedLinearGray => c.kCGColorSpaceExtendedLinearGray,
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
test {
|
||||
|
@ -53,7 +53,7 @@ pub fn build(b: *std.Build) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Build.Step.Compile {
|
||||
const target = options.target;
|
||||
const optimize = options.optimize;
|
||||
|
||||
|
@ -162,4 +162,26 @@ pub const Binding = struct {
|
||||
data,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn copySubImage2D(
|
||||
b: Binding,
|
||||
level: c.GLint,
|
||||
xoffset: c.GLint,
|
||||
yoffset: c.GLint,
|
||||
x: c.GLint,
|
||||
y: c.GLint,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.CopyTexSubImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
xoffset,
|
||||
yoffset,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame_config(
|
||||
decoder,
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
const status = c.wuffs_jpeg__decoder__decode_frame(
|
||||
decoder,
|
||||
|
@ -55,7 +55,7 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
|
||||
c.wuffs_base__pixel_config__set(
|
||||
&image_config.pixcfg,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_PREMUL,
|
||||
c.WUFFS_BASE__PIXEL_FORMAT__RGBA_NONPREMUL,
|
||||
c.WUFFS_BASE__PIXEL_SUBSAMPLING__NONE,
|
||||
width,
|
||||
height,
|
||||
@ -95,16 +95,6 @@ pub fn decode(alloc: Allocator, data: []const u8) Error!ImageData {
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
var frame_config: c.wuffs_base__frame_config = undefined;
|
||||
{
|
||||
const status = c.wuffs_png__decoder__decode_frame_config(
|
||||
decoder,
|
||||
&frame_config,
|
||||
&source_buffer,
|
||||
);
|
||||
try check(log, &status);
|
||||
}
|
||||
|
||||
{
|
||||
const status = c.wuffs_png__decoder__decode_frame(
|
||||
decoder,
|
||||
|
44
src/App.zig
44
src/App.zig
@ -54,9 +54,6 @@ focused_surface: ?*Surface = null,
|
||||
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||
mailbox: Mailbox.Queue,
|
||||
|
||||
/// Set to true once we're quitting. This never goes false again.
|
||||
quit: bool,
|
||||
|
||||
/// The set of font GroupCache instances shared by surfaces with the
|
||||
/// same font configuration.
|
||||
font_grid_set: font.SharedGridSet,
|
||||
@ -98,7 +95,6 @@ pub fn create(
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.quit = false,
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
};
|
||||
@ -125,9 +121,7 @@ pub fn destroy(self: *App) void {
|
||||
/// Tick ticks the app loop. This will drain our mailbox and process those
|
||||
/// events. This should be called by the application runtime on every loop
|
||||
/// tick.
|
||||
///
|
||||
/// This returns whether the app should quit or not.
|
||||
pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
pub fn tick(self: *App, rt_app: *apprt.App) !void {
|
||||
// If any surfaces are closing, destroy them
|
||||
var i: usize = 0;
|
||||
while (i < self.surfaces.items.len) {
|
||||
@ -142,13 +136,6 @@ pub fn tick(self: *App, rt_app: *apprt.App) !bool {
|
||||
|
||||
// Drain our mailbox
|
||||
try self.drainMailbox(rt_app);
|
||||
|
||||
// No matter what, we reset the quit flag after a tick. If the apprt
|
||||
// doesn't want to quit, then we can't force it to.
|
||||
defer self.quit = false;
|
||||
|
||||
// We quit if our quit flag is on
|
||||
return self.quit;
|
||||
}
|
||||
|
||||
/// Update the configuration associated with the app. This can only be
|
||||
@ -272,7 +259,7 @@ fn drainMailbox(self: *App, rt_app: *apprt.App) !void {
|
||||
// can try to quit as quickly as possible.
|
||||
.quit => {
|
||||
log.info("quit message received, short circuiting mailbox drain", .{});
|
||||
self.setQuit();
|
||||
try self.performAction(rt_app, .quit);
|
||||
return;
|
||||
},
|
||||
}
|
||||
@ -314,12 +301,6 @@ pub fn newWindow(self: *App, rt_app: *apprt.App, msg: Message.NewWindow) !void {
|
||||
);
|
||||
}
|
||||
|
||||
/// Start quitting
|
||||
pub fn setQuit(self: *App) void {
|
||||
if (self.quit) return;
|
||||
self.quit = true;
|
||||
}
|
||||
|
||||
/// Handle an app-level focus event. This should be called whenever
|
||||
/// the focus state of the entire app containing Ghostty changes.
|
||||
/// This is separate from surface focus events. See the `focused`
|
||||
@ -332,6 +313,25 @@ pub fn focusEvent(self: *App, focused: bool) void {
|
||||
self.focused = focused;
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *App,
|
||||
rt_app: *apprt.App,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
_ = self;
|
||||
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return rt_app.config.keybind.set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Handle a key event at the app-scope. If this key event is used,
|
||||
/// this will return true and the caller shouldn't continue processing
|
||||
/// the event. If the event is not used, this will return false.
|
||||
@ -437,7 +437,7 @@ pub fn performAction(
|
||||
switch (action) {
|
||||
.unbind => unreachable,
|
||||
.ignore => {},
|
||||
.quit => self.setQuit(),
|
||||
.quit => try rt_app.performAction(.app, .quit, {}),
|
||||
.new_window => try self.newWindow(rt_app, .{ .parent = null }),
|
||||
.open_config => try rt_app.performAction(.app, .open_config, {}),
|
||||
.reload_config => try rt_app.performAction(.app, .reload_config, .{}),
|
||||
|
120
src/Surface.zig
120
src/Surface.zig
@ -569,12 +569,16 @@ pub fn init(
|
||||
|
||||
// Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app
|
||||
// but is otherwise somewhat arbitrary.
|
||||
|
||||
const min_window_width_cells: u32 = 10;
|
||||
const min_window_height_cells: u32 = 4;
|
||||
|
||||
try rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.size_limit,
|
||||
.{
|
||||
.min_width = size.cell.width * 10,
|
||||
.min_height = size.cell.height * 4,
|
||||
.min_width = size.cell.width * min_window_width_cells,
|
||||
.min_height = size.cell.height * min_window_height_cells,
|
||||
// No max:
|
||||
.max_width = 0,
|
||||
.max_height = 0,
|
||||
@ -617,8 +621,8 @@ pub fn init(
|
||||
// start messing with the window.
|
||||
if (config.@"window-height" > 0 and config.@"window-width" > 0) init: {
|
||||
const scale = rt_surface.getContentScale() catch break :init;
|
||||
const height = @max(config.@"window-height" * cell_size.height, 480);
|
||||
const width = @max(config.@"window-width" * cell_size.width, 640);
|
||||
const height = @max(config.@"window-height", min_window_height_cells) * cell_size.height;
|
||||
const width = @max(config.@"window-width", min_window_width_cells) * cell_size.width;
|
||||
const width_f32: f32 = @floatFromInt(width);
|
||||
const height_f32: f32 = @floatFromInt(height);
|
||||
|
||||
@ -1037,6 +1041,9 @@ fn mouseRefreshLinks(
|
||||
pos_vp: terminal.point.Coordinate,
|
||||
over_link: bool,
|
||||
) !void {
|
||||
// If the position is outside our viewport, do nothing
|
||||
if (pos.x < 0 or pos.y < 0) return;
|
||||
|
||||
self.mouse.link_point = pos_vp;
|
||||
|
||||
if (try self.linkAtPos(pos)) |link| {
|
||||
@ -1312,8 +1319,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 };
|
||||
|
||||
const x: f64 = x: {
|
||||
// Simple x * cell width gives the top-left corner
|
||||
var x: f64 = @floatFromInt(cursor.x * self.size.cell.width);
|
||||
// Simple x * cell width gives the top-left corner, then add padding offset
|
||||
var x: f64 = @floatFromInt(cursor.x * self.size.cell.width + self.size.padding.left);
|
||||
|
||||
// We want the midpoint
|
||||
x += @as(f64, @floatFromInt(self.size.cell.width)) / 2;
|
||||
@ -1325,8 +1332,8 @@ pub fn imePoint(self: *const Surface) apprt.IMEPos {
|
||||
};
|
||||
|
||||
const y: f64 = y: {
|
||||
// Simple x * cell width gives the top-left corner
|
||||
var y: f64 = @floatFromInt(cursor.y * self.size.cell.height);
|
||||
// Simple y * cell height gives the top-left corner, then add padding offset
|
||||
var y: f64 = @floatFromInt(cursor.y * self.size.cell.height + self.size.padding.top);
|
||||
|
||||
// We want the bottom
|
||||
y += @floatFromInt(self.size.cell.height);
|
||||
@ -1587,6 +1594,15 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
|
||||
// We clear our selection when ANY OF:
|
||||
// 1. We have an existing preedit
|
||||
// 2. We have preedit text
|
||||
if (self.renderer_state.preedit != null or
|
||||
preedit_ != null)
|
||||
{
|
||||
self.setSelection(null) catch {};
|
||||
}
|
||||
|
||||
// We always clear our prior preedit
|
||||
if (self.renderer_state.preedit) |p| {
|
||||
self.alloc.free(p.codepoints);
|
||||
@ -1637,6 +1653,31 @@ pub fn preeditCallback(self: *Surface, preedit_: ?[]const u8) !void {
|
||||
try self.queueRender();
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a keybinding
|
||||
/// if it were to be processed. This is useful for determining if
|
||||
/// a key event should be sent to the terminal or not.
|
||||
///
|
||||
/// Note that this function does not check if the binding itself
|
||||
/// is performable, only if the key event would trigger a binding.
|
||||
/// If a performable binding is found and the event is not performable,
|
||||
/// then Ghosty will act as though the binding does not exist.
|
||||
pub fn keyEventIsBinding(
|
||||
self: *Surface,
|
||||
event: input.KeyEvent,
|
||||
) bool {
|
||||
switch (event.action) {
|
||||
.release => return false,
|
||||
.press, .repeat => {},
|
||||
}
|
||||
|
||||
// Our keybinding set is either our current nested set (for
|
||||
// sequences) or the root set.
|
||||
const set = self.keyboard.bindings orelse &self.config.keybind.set;
|
||||
|
||||
// If we have a keybinding for this event then we return true.
|
||||
return set.getEvent(event) != null;
|
||||
}
|
||||
|
||||
/// Called for any key events. This handles keybindings, encoding and
|
||||
/// sending to the terminal, etc.
|
||||
pub fn keyCallback(
|
||||
@ -3525,22 +3566,21 @@ fn dragLeftClickTriple(
|
||||
const screen = &self.io.terminal.screen;
|
||||
const click_pin = self.mouse.left_click_pin.?.*;
|
||||
|
||||
// Get the word under our current point. If there isn't a word, do nothing.
|
||||
const word = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
// Get the line selection under our current drag point. If there isn't a
|
||||
// line, do nothing.
|
||||
const line = screen.selectLine(.{ .pin = drag_pin }) orelse return;
|
||||
|
||||
// Get our selection to grow it. If we don't have a selection, start it now.
|
||||
// We may not have a selection if we started our dbl-click in an area
|
||||
// that had no data, then we dragged our mouse into an area with data.
|
||||
var sel = screen.selectLine(.{ .pin = click_pin }) orelse {
|
||||
try self.setSelection(word);
|
||||
return;
|
||||
};
|
||||
// Get the selection under our click point. We first try to trim
|
||||
// whitespace if we've selected a word. But if no word exists then
|
||||
// we select the blank line.
|
||||
const sel_ = screen.selectLine(.{ .pin = click_pin }) orelse
|
||||
screen.selectLine(.{ .pin = click_pin, .whitespace = null });
|
||||
|
||||
// Grow our selection
|
||||
var sel = sel_ orelse return;
|
||||
if (drag_pin.before(click_pin)) {
|
||||
sel.startPtr().* = word.start();
|
||||
sel.startPtr().* = line.start();
|
||||
} else {
|
||||
sel.endPtr().* = word.end();
|
||||
sel.endPtr().* = line.end();
|
||||
}
|
||||
try self.setSelection(sel);
|
||||
}
|
||||
@ -3907,6 +3947,33 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
return false;
|
||||
},
|
||||
|
||||
.copy_url_to_clipboard => {
|
||||
// If the mouse isn't over a link, nothing we can do.
|
||||
if (!self.mouse.over_link) return false;
|
||||
|
||||
const pos = try self.rt_surface.getCursorPos();
|
||||
if (try self.linkAtPos(pos)) |link_info| {
|
||||
// Get the URL text from selection
|
||||
const url_text = (self.io.terminal.screen.selectionString(self.alloc, .{
|
||||
.sel = link_info[1],
|
||||
.trim = self.config.clipboard_trim_trailing_spaces,
|
||||
})) catch |err| {
|
||||
log.err("error reading url string err={}", .{err});
|
||||
return false;
|
||||
};
|
||||
defer self.alloc.free(url_text);
|
||||
|
||||
self.rt_surface.setClipboardString(url_text, .standard, false) catch |err| {
|
||||
log.err("error copying url to clipboard err={}", .{err});
|
||||
return true;
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
.paste_from_clipboard => try self.startClipboardRequest(
|
||||
.standard,
|
||||
.{ .paste = {} },
|
||||
@ -4032,6 +4099,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.close_tab => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.close_tab,
|
||||
{},
|
||||
),
|
||||
|
||||
inline .previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
@ -4106,6 +4179,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_maximize => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_maximize,
|
||||
{},
|
||||
),
|
||||
|
||||
.toggle_fullscreen => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.toggle_fullscreen,
|
||||
@ -4231,6 +4310,7 @@ fn closingAction(action: input.Binding.Action) bool {
|
||||
return switch (action) {
|
||||
.close_surface,
|
||||
.close_window,
|
||||
.close_tab,
|
||||
=> true,
|
||||
|
||||
else => false,
|
||||
|
@ -70,6 +70,9 @@ pub const Action = union(Key) {
|
||||
// entry. If the value type is void then only the key needs to be
|
||||
// added. Ensure the order matches exactly with the Zig code.
|
||||
|
||||
/// Quit the application.
|
||||
quit,
|
||||
|
||||
/// Open a new window. The target determines whether properties such
|
||||
/// as font size should be inherited.
|
||||
new_window,
|
||||
@ -79,6 +82,9 @@ pub const Action = union(Key) {
|
||||
/// the tab should be opened in a new window.
|
||||
new_tab,
|
||||
|
||||
/// Closes the tab belonging to the currently focused split.
|
||||
close_tab,
|
||||
|
||||
/// Create a new split. The value determines the location of the split
|
||||
/// relative to the target.
|
||||
new_split: SplitDirection,
|
||||
@ -86,6 +92,9 @@ pub const Action = union(Key) {
|
||||
/// Close all open windows.
|
||||
close_all_windows,
|
||||
|
||||
/// Toggle maximized window state.
|
||||
toggle_maximize,
|
||||
|
||||
/// Toggle fullscreen mode.
|
||||
toggle_fullscreen: Fullscreen,
|
||||
|
||||
@ -219,10 +228,13 @@ pub const Action = union(Key) {
|
||||
|
||||
/// Sync with: ghostty_action_tag_e
|
||||
pub const Key = enum(c_int) {
|
||||
quit,
|
||||
new_window,
|
||||
new_tab,
|
||||
close_tab,
|
||||
new_split,
|
||||
close_all_windows,
|
||||
toggle_maximize,
|
||||
toggle_fullscreen,
|
||||
toggle_tab_overview,
|
||||
toggle_window_decorations,
|
||||
|
@ -147,12 +147,12 @@ pub const App = struct {
|
||||
self.core_app.focusEvent(focused);
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
/// Convert a C key event into a Zig key event.
|
||||
fn coreKeyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
) !?input.KeyEvent {
|
||||
const action = event.action;
|
||||
const keycode = event.keycode;
|
||||
const mods = event.mods;
|
||||
@ -199,6 +199,11 @@ pub const App = struct {
|
||||
// This logic only applies to macOS.
|
||||
if (comptime builtin.os.tag != .macos) break :event_text event.text;
|
||||
|
||||
// If we're in a preedit state then we allow it through. This
|
||||
// allows ctrl sequences that affect IME to work. For example,
|
||||
// Ctrl+H deletes a character with Japanese input.
|
||||
if (event.composing) break :event_text event.text;
|
||||
|
||||
// If the modifiers are ONLY "control" then we never process
|
||||
// the event text because we want to do our own translation so
|
||||
// we can handle ctrl+c, ctrl+z, etc.
|
||||
@ -243,7 +248,7 @@ pub const App = struct {
|
||||
result.text,
|
||||
) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@ -251,7 +256,7 @@ pub const App = struct {
|
||||
.app => {},
|
||||
.surface => |surface| surface.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
return false;
|
||||
return null;
|
||||
},
|
||||
}
|
||||
|
||||
@ -335,7 +340,7 @@ pub const App = struct {
|
||||
} else .invalid;
|
||||
|
||||
// Build our final key event
|
||||
const input_event: input.KeyEvent = .{
|
||||
return .{
|
||||
.action = action,
|
||||
.key = key,
|
||||
.physical_key = physical_key,
|
||||
@ -345,24 +350,39 @@ pub const App = struct {
|
||||
.utf8 = result.text,
|
||||
.unshifted_codepoint = unshifted_codepoint,
|
||||
};
|
||||
}
|
||||
|
||||
/// See CoreApp.keyEvent.
|
||||
pub fn keyEvent(
|
||||
self: *App,
|
||||
target: KeyTarget,
|
||||
event: KeyEvent,
|
||||
) !bool {
|
||||
// Convert our C key event into a Zig one.
|
||||
const input_event: input.KeyEvent = (try self.coreKeyEvent(
|
||||
target,
|
||||
event,
|
||||
)) orelse return false;
|
||||
|
||||
// Invoke the core Ghostty logic to handle this input.
|
||||
const effect: CoreSurface.InputEffect = switch (target) {
|
||||
.app => if (self.core_app.keyEvent(
|
||||
self,
|
||||
input_event,
|
||||
))
|
||||
.consumed
|
||||
else
|
||||
.ignored,
|
||||
)) .consumed else .ignored,
|
||||
|
||||
.surface => |surface| try surface.core_surface.keyCallback(input_event),
|
||||
.surface => |surface| try surface.core_surface.keyCallback(
|
||||
input_event,
|
||||
),
|
||||
};
|
||||
|
||||
return switch (effect) {
|
||||
.closed => true,
|
||||
.ignored => false,
|
||||
.consumed => consumed: {
|
||||
const is_down = input_event.action == .press or
|
||||
input_event.action == .repeat;
|
||||
|
||||
if (is_down) {
|
||||
// If we consume the key then we want to reset the dead
|
||||
// key state.
|
||||
@ -618,7 +638,7 @@ pub const Surface = struct {
|
||||
.y = @floatCast(opts.scale_factor),
|
||||
},
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.cursor_pos = .{ .x = -1, .y = -1 },
|
||||
.keymap_state = .{},
|
||||
};
|
||||
|
||||
@ -1332,10 +1352,9 @@ pub const CAPI = struct {
|
||||
|
||||
/// Tick the event loop. This should be called whenever the "wakeup"
|
||||
/// callback is invoked for the runtime.
|
||||
export fn ghostty_app_tick(v: *App) bool {
|
||||
return v.core_app.tick(v) catch |err| err: {
|
||||
export fn ghostty_app_tick(v: *App) void {
|
||||
v.core_app.tick(v) catch |err| {
|
||||
log.err("error app tick err={}", .{err});
|
||||
break :err false;
|
||||
};
|
||||
}
|
||||
|
||||
@ -1372,6 +1391,28 @@ pub const CAPI = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_app_key_is_binding(
|
||||
app: *App,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = app.coreKeyEvent(
|
||||
.app,
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return app.core_app.keyEventIsBinding(app, core_event);
|
||||
}
|
||||
|
||||
/// Notify the app that the keyboard was changed. This causes the
|
||||
/// keyboard layout to be reloaded from the OS.
|
||||
export fn ghostty_app_keyboard_changed(v: *App) void {
|
||||
@ -1592,16 +1633,38 @@ pub const CAPI = struct {
|
||||
export fn ghostty_surface_key(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) void {
|
||||
_ = surface.app.keyEvent(
|
||||
) bool {
|
||||
return surface.app.keyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns true if the given key event would trigger a binding
|
||||
/// if it were sent to the surface right now. The "right now"
|
||||
/// is important because things like trigger sequences are only
|
||||
/// valid until the next key event.
|
||||
export fn ghostty_surface_key_is_binding(
|
||||
surface: *Surface,
|
||||
event: KeyEvent,
|
||||
) bool {
|
||||
const core_event = surface.app.coreKeyEvent(
|
||||
.{ .surface = surface },
|
||||
event.keyEvent(),
|
||||
) catch |err| {
|
||||
log.warn("error processing key event err={}", .{err});
|
||||
return false;
|
||||
} orelse {
|
||||
log.warn("error processing key event", .{});
|
||||
return false;
|
||||
};
|
||||
|
||||
return surface.core_surface.keyEventIsBinding(core_event);
|
||||
}
|
||||
|
||||
/// Send raw text to the terminal. This is treated like a paste
|
||||
/// so this isn't useful for sending escape sequences. For that,
|
||||
/// individual key input should be used.
|
||||
@ -1895,7 +1958,7 @@ pub const CAPI = struct {
|
||||
_ = CGSSetWindowBackgroundBlurRadius(
|
||||
CGSDefaultConnectionForThread(),
|
||||
nswindow.msgSend(usize, objc.sel("windowNumber"), .{}),
|
||||
@intCast(config.@"background-blur-radius"),
|
||||
@intCast(config.@"background-blur".cval()),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,10 @@ pub const App = struct {
|
||||
app: *CoreApp,
|
||||
config: Config,
|
||||
|
||||
/// Flips to true to quit on the next event loop tick. This
|
||||
/// never goes false and forces the event loop to exit.
|
||||
quit: bool = false,
|
||||
|
||||
/// Mac-specific state.
|
||||
darwin: if (Darwin.enabled) Darwin else void,
|
||||
|
||||
@ -124,8 +128,10 @@ pub const App = struct {
|
||||
glfw.waitEvents();
|
||||
|
||||
// Tick the terminal app
|
||||
const should_quit = try self.app.tick(self);
|
||||
if (should_quit or self.app.surfaces.items.len == 0) {
|
||||
try self.app.tick(self);
|
||||
|
||||
// If the tick caused us to quit, then we're done.
|
||||
if (self.quit or self.app.surfaces.items.len == 0) {
|
||||
for (self.app.surfaces.items) |surface| {
|
||||
surface.close(false);
|
||||
}
|
||||
@ -149,6 +155,8 @@ pub const App = struct {
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.quit => self.quit = true,
|
||||
|
||||
.new_window => _ = try self.newSurface(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
@ -210,6 +218,7 @@ pub const App = struct {
|
||||
.toggle_split_zoom,
|
||||
.present_terminal,
|
||||
.close_all_windows,
|
||||
.close_tab,
|
||||
.toggle_tab_overview,
|
||||
.toggle_window_decorations,
|
||||
.toggle_quick_terminal,
|
||||
@ -228,6 +237,7 @@ pub const App = struct {
|
||||
.color_change,
|
||||
.pwd,
|
||||
.config_change,
|
||||
.toggle_maximize,
|
||||
=> log.info("unimplemented action={}", .{action}),
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ const c = @import("c.zig").c;
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const x11 = @import("x11.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -49,6 +49,9 @@ config: Config,
|
||||
app: *c.GtkApplication,
|
||||
ctx: *c.GMainContext,
|
||||
|
||||
/// State and logic for the underlying windowing protocol.
|
||||
winproto: winproto.App,
|
||||
|
||||
/// True if the app was launched with single instance mode.
|
||||
single_instance: bool,
|
||||
|
||||
@ -70,8 +73,10 @@ clipboard_confirmation_window: ?*ClipboardConfirmationWindow = null,
|
||||
/// This is set to false when the main loop should exit.
|
||||
running: bool = true,
|
||||
|
||||
/// Xkb state (X11 only). Will be null on Wayland.
|
||||
x11_xkb: ?x11.Xkb = null,
|
||||
/// If we should retry querying D-Bus for the color scheme with the deprecated
|
||||
/// Read method, instead of the recommended ReadOne method. This is kind of
|
||||
/// nasty to have as struct state but its just a byte...
|
||||
dbus_color_scheme_retry: bool = true,
|
||||
|
||||
/// The base path of the transient cgroup used to put all surfaces
|
||||
/// into their own cgroup. This is only set if cgroups are enabled
|
||||
@ -104,42 +109,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
c.gtk_get_micro_version(),
|
||||
});
|
||||
|
||||
// Disabling Vulkan can improve startup times by hundreds of
|
||||
// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
// disable it.
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
_ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan");
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-no-fractional");
|
||||
} else if (version.atLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
// to always set this. Forwards versions are uncertain so we'll have to
|
||||
// reassess...
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
//
|
||||
// Specific details about values:
|
||||
// - "opengl" - output OpenGL debug information
|
||||
// - "gl-disable-gles" - disable GLES, Ghostty can't use GLES
|
||||
// - "vulkan-disable" - disable Vulkan, Ghostty can't use Vulkan
|
||||
// and initializing a Vulkan context was causing a longer delay
|
||||
// on some systems.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable,gl-no-fractional");
|
||||
} else {
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
// good understanding of what we may need to do.
|
||||
_ = internal_os.setenv("GDK_DEBUG", "vulkan-disable");
|
||||
}
|
||||
|
||||
if (version.atLeast(4, 14, 0)) {
|
||||
// We need to export GSK_RENDERER to opengl because GTK uses ngl by
|
||||
// default after 4.14
|
||||
_ = internal_os.setenv("GSK_RENDERER", "opengl");
|
||||
}
|
||||
|
||||
// Load our configuration
|
||||
var config = try Config.load(core_app.alloc);
|
||||
errdefer config.deinit();
|
||||
@ -161,8 +130,111 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
}
|
||||
}
|
||||
|
||||
var gdk_debug: struct {
|
||||
/// output OpenGL debug information
|
||||
opengl: bool = false,
|
||||
/// disable GLES, Ghostty can't use GLES
|
||||
@"gl-disable-gles": bool = false,
|
||||
@"gl-no-fractional": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
/// disable it.
|
||||
@"vulkan-disable": bool = false,
|
||||
} = .{
|
||||
.opengl = config.@"gtk-opengl-debug",
|
||||
};
|
||||
|
||||
var gdk_disable: struct {
|
||||
@"gles-api": bool = false,
|
||||
/// Disabling Vulkan can improve startup times by hundreds of
|
||||
/// milliseconds on some systems. We don't use Vulkan so we can just
|
||||
/// disable it.
|
||||
vulkan: bool = false,
|
||||
} = .{};
|
||||
|
||||
environment: {
|
||||
if (version.runtimeAtLeast(4, 16, 0)) {
|
||||
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
|
||||
// For the remainder of "why" see the 4.14 comment below.
|
||||
gdk_disable.@"gles-api" = true;
|
||||
gdk_disable.vulkan = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
break :environment;
|
||||
}
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
|
||||
// Older versions of GTK do not support these values so it is safe
|
||||
// to always set this. Forwards versions are uncertain so we'll have
|
||||
// to reassess...
|
||||
//
|
||||
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
|
||||
gdk_debug.@"gl-disable-gles" = true;
|
||||
gdk_debug.@"gl-no-fractional" = true;
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
break :environment;
|
||||
}
|
||||
// Versions prior to 4.14 are a bit of an unknown for Ghostty. It
|
||||
// is an environment that isn't tested well and we don't have a
|
||||
// good understanding of what we may need to do.
|
||||
gdk_debug.@"vulkan-disable" = true;
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
var fmt = std.io.fixedBufferStream(&buf);
|
||||
const writer = fmt.writer();
|
||||
var first: bool = true;
|
||||
inline for (@typeInfo(@TypeOf(gdk_debug)).Struct.fields) |field| {
|
||||
if (@field(gdk_debug, field.name)) {
|
||||
if (!first) try writer.writeAll(",");
|
||||
try writer.writeAll(field.name);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const value = fmt.getWritten();
|
||||
log.warn("setting GDK_DEBUG={s}", .{value[0 .. value.len - 1]});
|
||||
_ = internal_os.setenv("GDK_DEBUG", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
|
||||
{
|
||||
var buf: [128]u8 = undefined;
|
||||
var fmt = std.io.fixedBufferStream(&buf);
|
||||
const writer = fmt.writer();
|
||||
var first: bool = true;
|
||||
inline for (@typeInfo(@TypeOf(gdk_disable)).Struct.fields) |field| {
|
||||
if (@field(gdk_disable, field.name)) {
|
||||
if (!first) try writer.writeAll(",");
|
||||
try writer.writeAll(field.name);
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeByte(0);
|
||||
const value = fmt.getWritten();
|
||||
log.warn("setting GDK_DISABLE={s}", .{value[0 .. value.len - 1]});
|
||||
_ = internal_os.setenv("GDK_DISABLE", value[0 .. value.len - 1 :0]);
|
||||
}
|
||||
|
||||
if (version.runtimeAtLeast(4, 14, 0)) {
|
||||
switch (config.@"gtk-gsk-renderer") {
|
||||
.default => {},
|
||||
else => |renderer| {
|
||||
// Force the GSK renderer to a specific value. After GTK 4.14 the
|
||||
// `ngl` renderer is used by default which causes artifacts when
|
||||
// used with Ghostty so it should be avoided.
|
||||
log.warn("setting GSK_RENDERER={s}", .{@tagName(renderer)});
|
||||
_ = internal_os.setenv("GSK_RENDERER", @tagName(renderer));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
c.gtk_init();
|
||||
const display = c.gdk_display_get_default();
|
||||
const display: *c.GdkDisplay = c.gdk_display_get_default() orelse {
|
||||
// I'm unsure of any scenario where this happens. Because we don't
|
||||
// want to litter null checks everywhere, we just exit here.
|
||||
log.warn("gdk display is null, exiting", .{});
|
||||
std.posix.exit(1);
|
||||
};
|
||||
|
||||
// If we're using libadwaita, log the version
|
||||
if (adwaita.enabled(&config)) {
|
||||
@ -360,42 +432,15 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
return error.GtkApplicationRegisterFailed;
|
||||
}
|
||||
|
||||
// Perform all X11 initialization. This ultimately returns the X11
|
||||
// keyboard state but the block does more than that (i.e. setting up
|
||||
// WM_CLASS).
|
||||
const x11_xkb: ?x11.Xkb = x11_xkb: {
|
||||
if (comptime !build_options.x11) break :x11_xkb null;
|
||||
if (!x11.is_display(display)) break :x11_xkb null;
|
||||
|
||||
// Set the X11 window class property (WM_CLASS) if are are on an X11
|
||||
// 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.
|
||||
//
|
||||
const prgname = if (config.@"x11-instance-name") |pn|
|
||||
pn
|
||||
else if (builtin.mode == .Debug)
|
||||
"ghostty-debug"
|
||||
else
|
||||
"ghostty";
|
||||
c.g_set_prgname(prgname);
|
||||
c.gdk_x11_display_set_program_class(display, app_id);
|
||||
|
||||
// Set up Xkb
|
||||
break :x11_xkb try x11.Xkb.init(display);
|
||||
};
|
||||
// Setup our windowing protocol logic
|
||||
var winproto_app = try winproto.App.init(
|
||||
core_app.alloc,
|
||||
display,
|
||||
app_id,
|
||||
&config,
|
||||
);
|
||||
errdefer winproto_app.deinit(core_app.alloc);
|
||||
log.debug("windowing protocol={s}", .{@tagName(winproto_app)});
|
||||
|
||||
// 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
|
||||
@ -421,7 +466,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
.config = config,
|
||||
.ctx = ctx,
|
||||
.cursor_none = cursor_none,
|
||||
.x11_xkb = x11_xkb,
|
||||
.winproto = winproto_app,
|
||||
.single_instance = single_instance,
|
||||
// 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
|
||||
@ -449,6 +494,8 @@ pub fn terminate(self: *App) void {
|
||||
}
|
||||
self.custom_css_providers.deinit(self.core_app.alloc);
|
||||
|
||||
self.winproto.deinit(self.core_app.alloc);
|
||||
|
||||
self.config.deinit();
|
||||
}
|
||||
|
||||
@ -460,13 +507,16 @@ pub fn performAction(
|
||||
value: apprt.Action.Value(action),
|
||||
) !void {
|
||||
switch (action) {
|
||||
.quit => self.quit(),
|
||||
.new_window => _ = try self.newWindow(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
.toggle_maximize => self.toggleMaximize(target),
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
.close_tab => try self.closeTab(target),
|
||||
.goto_tab => self.gotoTab(target, value),
|
||||
.move_tab => self.moveTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
@ -482,6 +532,7 @@ pub fn performAction(
|
||||
.pwd => try self.setPwd(target, value),
|
||||
.present_terminal => self.presentTerminal(target),
|
||||
.initial_size => try self.setInitialSize(target, value),
|
||||
.size_limit => try self.setSizeLimit(target, value),
|
||||
.mouse_visibility => self.setMouseVisibility(target, value),
|
||||
.mouse_shape => try self.setMouseShape(target, value),
|
||||
.mouse_over_link => self.setMouseOverLink(target, value),
|
||||
@ -494,7 +545,6 @@ pub fn performAction(
|
||||
.close_all_windows,
|
||||
.toggle_quick_terminal,
|
||||
.toggle_visibility,
|
||||
.size_limit,
|
||||
.cell_size,
|
||||
.secure_input,
|
||||
.key_sequence,
|
||||
@ -522,6 +572,23 @@ fn newTab(_: *App, target: apprt.Target) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn closeTab(_: *App, target: apprt.Target) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const tab = v.rt_surface.container.tab() orelse {
|
||||
log.info(
|
||||
"close_tab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
tab.closeWithConfirmation();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
@ -648,6 +715,22 @@ fn controlInspector(
|
||||
surface.controlInspector(mode);
|
||||
}
|
||||
|
||||
fn toggleMaximize(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"toggleMaximize invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
window.toggleMaximize();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleFullscreen(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
@ -795,6 +878,23 @@ fn setInitialSize(
|
||||
}
|
||||
}
|
||||
|
||||
fn setSizeLimit(
|
||||
_: *App,
|
||||
target: apprt.Target,
|
||||
value: apprt.action.SizeLimit,
|
||||
) !void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| try v.rt_surface.setSizeLimits(.{
|
||||
.width = value.min_width,
|
||||
.height = value.min_height,
|
||||
}, if (value.max_width > 0) .{
|
||||
.width = value.max_width,
|
||||
.height = value.max_height,
|
||||
} else null),
|
||||
}
|
||||
}
|
||||
|
||||
fn showDesktopNotification(
|
||||
self: *App,
|
||||
target: apprt.Target,
|
||||
@ -837,9 +937,12 @@ fn configChange(
|
||||
new_config: *const Config,
|
||||
) void {
|
||||
switch (target) {
|
||||
// We don't do anything for surface config change events. There
|
||||
// is nothing to sync with regards to a surface today.
|
||||
.surface => {},
|
||||
.surface => |surface| surface: {
|
||||
const window = surface.rt_surface.container.window() orelse break :surface;
|
||||
window.updateConfig(new_config) catch |err| {
|
||||
log.warn("error updating config for window err={}", .{err});
|
||||
};
|
||||
},
|
||||
|
||||
.app => {
|
||||
// We clone (to take ownership) and update our configuration.
|
||||
@ -995,7 +1098,28 @@ fn loadRuntimeCss(
|
||||
unfocused_fill.b,
|
||||
});
|
||||
|
||||
if (version.atLeast(4, 16, 0)) {
|
||||
if (config.@"split-divider-color") |color| {
|
||||
try writer.print(
|
||||
\\.terminal-window .notebook separator {{
|
||||
\\ color: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\ background: rgb({[r]d},{[g]d},{[b]d});
|
||||
\\}}
|
||||
, .{
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
.b = color.b,
|
||||
});
|
||||
}
|
||||
|
||||
if (config.@"window-title-font-family") |font_family| {
|
||||
try writer.print(
|
||||
\\.window headerbar {{
|
||||
\\ font-family: "{[font_family]s}";
|
||||
\\}}
|
||||
, .{ .font_family = font_family });
|
||||
}
|
||||
|
||||
if (version.runtimeAtLeast(4, 16, 0)) {
|
||||
switch (window_theme) {
|
||||
.ghostty => try writer.print(
|
||||
\\:root {{
|
||||
@ -1008,6 +1132,8 @@ fn loadRuntimeCss(
|
||||
\\ --overview-bg-color: var(--ghostty-bg);
|
||||
\\ --popover-fg-color: var(--ghostty-fg);
|
||||
\\ --popover-bg-color: var(--ghostty-bg);
|
||||
\\ --window-fg-color: var(--ghostty-fg);
|
||||
\\ --window-bg-color: var(--ghostty-bg);
|
||||
\\}}
|
||||
\\windowhandle {{
|
||||
\\ background-color: var(--headerbar-bg-color);
|
||||
@ -1150,7 +1276,8 @@ pub fn run(self: *App) !void {
|
||||
self.transient_cgroup_base = path;
|
||||
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
|
||||
|
||||
// Setup our D-Bus connection for listening to settings changes.
|
||||
// Setup our D-Bus connection for listening to settings changes,
|
||||
// and asynchronously request the initial color scheme
|
||||
self.initDbus();
|
||||
|
||||
// Setup our menu items
|
||||
@ -1158,9 +1285,6 @@ pub fn run(self: *App) !void {
|
||||
self.initMenu();
|
||||
self.initContextMenu();
|
||||
|
||||
// Setup our initial color scheme
|
||||
self.colorSchemeEvent(self.getColorScheme());
|
||||
|
||||
// On startup, we want to check for configuration errors right away
|
||||
// so we can show our error window. We also need to setup other initial
|
||||
// state.
|
||||
@ -1172,14 +1296,10 @@ pub fn run(self: *App) !void {
|
||||
_ = c.g_main_context_iteration(self.ctx, 1);
|
||||
|
||||
// Tick the terminal app and see if we should quit.
|
||||
const should_quit = try self.core_app.tick(self);
|
||||
try self.core_app.tick(self);
|
||||
|
||||
// Check if we must quit based on the current state.
|
||||
const must_quit = q: {
|
||||
// If we've been told by GTK that we should quit, do so regardless
|
||||
// of any other setting.
|
||||
if (should_quit) break :q true;
|
||||
|
||||
// If we are configured to always stay running, don't quit.
|
||||
if (!self.config.@"quit-after-last-window-closed") break :q false;
|
||||
|
||||
@ -1212,6 +1332,22 @@ fn initDbus(self: *App) void {
|
||||
self,
|
||||
null,
|
||||
);
|
||||
|
||||
// Request the initial color scheme asynchronously.
|
||||
c.g_dbus_connection_call(
|
||||
dbus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"ReadOne",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
dbusColorSchemeCallback,
|
||||
self,
|
||||
);
|
||||
}
|
||||
|
||||
// This timeout function is started when no surfaces are open. It can be
|
||||
@ -1283,6 +1419,9 @@ fn newWindow(self: *App, parent_: ?*CoreSurface) !void {
|
||||
}
|
||||
|
||||
fn quit(self: *App) void {
|
||||
// If we're already not running, do nothing.
|
||||
if (!self.running) return;
|
||||
|
||||
// If we have no toplevel windows, then we're done.
|
||||
const list = c.gtk_window_list_toplevels();
|
||||
if (list == null) {
|
||||
@ -1446,93 +1585,58 @@ fn gtkWindowIsActive(
|
||||
core_app.focusEvent(false);
|
||||
}
|
||||
|
||||
/// Call a D-Bus method to determine the current color scheme. If there
|
||||
/// is any error at any point we'll log the error and return "light"
|
||||
pub fn getColorScheme(self: *App) apprt.ColorScheme {
|
||||
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
|
||||
fn dbusColorSchemeCallback(
|
||||
source_object: [*c]c.GObject,
|
||||
res: ?*c.GAsyncResult,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud.?));
|
||||
const dbus: *c.GDBusConnection = @ptrCast(source_object);
|
||||
|
||||
var err: ?*c.GError = null;
|
||||
defer if (err) |e| c.g_error_free(e);
|
||||
|
||||
const value = c.g_dbus_connection_call_sync(
|
||||
dbus_connection,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"ReadOne",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| {
|
||||
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
|
||||
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
|
||||
if (e.code == 19) {
|
||||
return self.getColorSchemeDeprecated();
|
||||
if (c.g_dbus_connection_call_finish(dbus, res, &err)) |value| {
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer c.g_variant_unref(inner);
|
||||
if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
|
||||
self.colorSchemeEvent(if (c.g_variant_get_uint32(inner) == 1)
|
||||
.dark
|
||||
else
|
||||
.light);
|
||||
return;
|
||||
}
|
||||
// Otherwise, log the error and return .light
|
||||
log.err("unable to get current color scheme: {s}", .{e.message});
|
||||
}
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
} else if (err) |e| {
|
||||
// If ReadOne is not yet implemented, fall back to deprecated "Read" method
|
||||
// Error code: GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No such method “ReadOne”
|
||||
if (self.dbus_color_scheme_retry and e.code == 19) {
|
||||
self.dbus_color_scheme_retry = false;
|
||||
c.g_dbus_connection_call(
|
||||
dbus,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"Read",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
dbusColorSchemeCallback,
|
||||
self,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer c.g_variant_unref(inner);
|
||||
if (c.g_variant_is_of_type(inner, c.G_VARIANT_TYPE("u")) == 1) {
|
||||
return if (c.g_variant_get_uint32(inner) == 1) .dark else .light;
|
||||
}
|
||||
// Otherwise, log the error and return .light
|
||||
log.warn("unable to get current color scheme: {s}", .{e.message});
|
||||
}
|
||||
|
||||
return .light;
|
||||
}
|
||||
|
||||
/// Call the deprecated D-Bus "Read" method to determine the current color scheme. If
|
||||
/// there is any error at any point we'll log the error and return "light"
|
||||
fn getColorSchemeDeprecated(self: *App) apprt.ColorScheme {
|
||||
const dbus_connection = c.g_application_get_dbus_connection(@ptrCast(self.app));
|
||||
var err: ?*c.GError = null;
|
||||
defer if (err) |e| c.g_error_free(e);
|
||||
|
||||
const value = c.g_dbus_connection_call_sync(
|
||||
dbus_connection,
|
||||
"org.freedesktop.portal.Desktop",
|
||||
"/org/freedesktop/portal/desktop",
|
||||
"org.freedesktop.portal.Settings",
|
||||
"Read",
|
||||
c.g_variant_new("(ss)", "org.freedesktop.appearance", "color-scheme"),
|
||||
c.G_VARIANT_TYPE("(v)"),
|
||||
c.G_DBUS_CALL_FLAGS_NONE,
|
||||
-1,
|
||||
null,
|
||||
&err,
|
||||
) orelse {
|
||||
if (err) |e| log.err("Read method failed: {s}", .{e.message});
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(value);
|
||||
|
||||
if (c.g_variant_is_of_type(value, c.G_VARIANT_TYPE("(v)")) == 1) {
|
||||
var inner: ?*c.GVariant = null;
|
||||
c.g_variant_get(value, "(v)", &inner);
|
||||
defer if (inner) |i| c.g_variant_unref(i);
|
||||
|
||||
if (inner) |i| {
|
||||
const child = c.g_variant_get_child_value(i, 0) orelse {
|
||||
return .light;
|
||||
};
|
||||
defer c.g_variant_unref(child);
|
||||
|
||||
const val = c.g_variant_get_uint32(child);
|
||||
return if (val == 1) .dark else .light;
|
||||
}
|
||||
}
|
||||
return .light;
|
||||
// Fall back
|
||||
self.colorSchemeEvent(.light);
|
||||
}
|
||||
|
||||
/// This will be called by D-Bus when the style changes between light & dark.
|
||||
@ -1623,7 +1727,9 @@ fn gtkActionQuit(
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *App = @ptrCast(@alignCast(ud orelse return));
|
||||
self.core_app.setQuit();
|
||||
self.core_app.performAction(self, .quit) catch |err| {
|
||||
log.err("error quitting err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
/// Action sent by the window manager asking us to present a specific surface to
|
||||
@ -1695,18 +1801,17 @@ fn initActions(self: *App) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// This sets the self.menu property to the application menu that can be
|
||||
/// shared by all application windows.
|
||||
fn initMenu(self: *App) void {
|
||||
const menu = c.g_menu_new();
|
||||
errdefer c.g_object_unref(menu);
|
||||
|
||||
/// Initializes and populates the provided GMenu with sections and actions.
|
||||
/// This function is used to set up the application's menu structure, either for
|
||||
/// the main menu button or as a context menu when window decorations are disabled.
|
||||
fn initMenuContent(menu: *c.GMenu) void {
|
||||
{
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
c.g_menu_append(section, "New Window", "win.new_window");
|
||||
c.g_menu_append(section, "New Tab", "win.new_tab");
|
||||
c.g_menu_append(section, "Close Tab", "win.close_tab");
|
||||
c.g_menu_append(section, "Split Right", "win.split_right");
|
||||
c.g_menu_append(section, "Split Down", "win.split_down");
|
||||
c.g_menu_append(section, "Close Window", "win.close");
|
||||
@ -1721,13 +1826,14 @@ fn initMenu(self: *App) void {
|
||||
c.g_menu_append(section, "Reload Configuration", "app.reload-config");
|
||||
c.g_menu_append(section, "About Ghostty", "win.about");
|
||||
}
|
||||
}
|
||||
|
||||
// {
|
||||
// const section = c.g_menu_new();
|
||||
// defer c.g_object_unref(section);
|
||||
// c.g_menu_append_submenu(menu, "File", @ptrCast(@alignCast(section)));
|
||||
// }
|
||||
|
||||
/// This sets the self.menu property to the application menu that can be
|
||||
/// shared by all application windows.
|
||||
fn initMenu(self: *App) void {
|
||||
const menu = c.g_menu_new();
|
||||
errdefer c.g_object_unref(menu);
|
||||
initMenuContent(@ptrCast(menu));
|
||||
self.menu = menu;
|
||||
}
|
||||
|
||||
@ -1735,7 +1841,13 @@ fn initContextMenu(self: *App) void {
|
||||
const menu = c.g_menu_new();
|
||||
errdefer c.g_object_unref(menu);
|
||||
|
||||
createContextMenuCopyPasteSection(menu, false);
|
||||
{
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
c.g_menu_append(section, "Copy", "win.copy");
|
||||
c.g_menu_append(section, "Paste", "win.paste");
|
||||
}
|
||||
|
||||
{
|
||||
const section = c.g_menu_new();
|
||||
@ -1753,21 +1865,21 @@ fn initContextMenu(self: *App) void {
|
||||
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
|
||||
}
|
||||
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
const submenu = c.g_menu_new();
|
||||
defer c.g_object_unref(submenu);
|
||||
|
||||
initMenuContent(@ptrCast(submenu));
|
||||
c.g_menu_append_submenu(section, "Menu", @ptrCast(@alignCast(submenu)));
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
|
||||
self.context_menu = menu;
|
||||
}
|
||||
|
||||
fn createContextMenuCopyPasteSection(menu: ?*c.GMenu, has_selection: bool) void {
|
||||
const section = c.g_menu_new();
|
||||
defer c.g_object_unref(section);
|
||||
c.g_menu_prepend_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
// FIXME: Feels really hackish, but disabling sensitivity on this doesn't seems to work(?)
|
||||
c.g_menu_append(section, "Copy", if (has_selection) "win.copy" else "noop");
|
||||
c.g_menu_append(section, "Paste", "win.paste");
|
||||
}
|
||||
|
||||
pub fn refreshContextMenu(self: *App, has_selection: bool) void {
|
||||
c.g_menu_remove(self.context_menu, 0);
|
||||
createContextMenuCopyPasteSection(self.context_menu, has_selection);
|
||||
pub fn refreshContextMenu(_: *App, window: ?*c.GtkWindow, has_selection: bool) void {
|
||||
const action: ?*c.GSimpleAction = @ptrCast(c.g_action_map_lookup_action(@ptrCast(window), "copy"));
|
||||
c.g_simple_action_set_enabled(action, if (has_selection) 1 else 0);
|
||||
}
|
||||
|
||||
fn isValidAppId(app_id: [:0]const u8) bool {
|
||||
|
@ -64,6 +64,7 @@ fn init(
|
||||
c.gtk_window_set_title(gtk_window, titleText(request));
|
||||
c.gtk_window_set_default_size(gtk_window, 550, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "clipboard-confirmation-window");
|
||||
_ = c.g_signal_connect_data(
|
||||
window,
|
||||
@ -88,6 +89,8 @@ fn init(
|
||||
const view = try PrimaryView.init(self, data);
|
||||
self.view = view;
|
||||
c.gtk_window_set_child(@ptrCast(window), view.root);
|
||||
_ = c.gtk_widget_grab_focus(view.buttons.cancel_button);
|
||||
|
||||
c.gtk_widget_show(window);
|
||||
|
||||
// Block the main window from input.
|
||||
@ -103,6 +106,7 @@ fn gtkDestroy(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const PrimaryView = struct {
|
||||
root: *c.GtkWidget,
|
||||
text: *c.GtkTextView,
|
||||
buttons: ButtonsView,
|
||||
|
||||
pub fn init(root: *ClipboardConfirmation, data: []const u8) !PrimaryView {
|
||||
// All our widgets
|
||||
@ -134,7 +138,7 @@ const PrimaryView = struct {
|
||||
c.gtk_text_view_set_right_margin(@ptrCast(text), 8);
|
||||
c.gtk_text_view_set_monospace(@ptrCast(text), 1);
|
||||
|
||||
return .{ .root = view.root, .text = @ptrCast(text) };
|
||||
return .{ .root = view.root, .text = @ptrCast(text), .buttons = buttons };
|
||||
}
|
||||
|
||||
/// Returns the GtkTextBuffer for the data that was unsafe.
|
||||
@ -157,6 +161,8 @@ const PrimaryView = struct {
|
||||
|
||||
const ButtonsView = struct {
|
||||
root: *c.GtkWidget,
|
||||
confirm_button: *c.GtkWidget,
|
||||
cancel_button: *c.GtkWidget,
|
||||
|
||||
pub fn init(root: *ClipboardConfirmation) !ButtonsView {
|
||||
const cancel_text, const confirm_text = switch (root.pending_req) {
|
||||
@ -170,8 +176,8 @@ const ButtonsView = struct {
|
||||
const confirm_button = c.gtk_button_new_with_label(confirm_text);
|
||||
errdefer c.g_object_unref(confirm_button);
|
||||
|
||||
// TODO: Focus on the paste button
|
||||
// c.gtk_widget_grab_focus(confirm_button);
|
||||
c.gtk_widget_add_css_class(confirm_button, "destructive-action");
|
||||
c.gtk_widget_add_css_class(cancel_button, "suggested-action");
|
||||
|
||||
// Create our view
|
||||
const view = try View.init(&.{
|
||||
@ -197,7 +203,7 @@ const ButtonsView = struct {
|
||||
c.G_CONNECT_DEFAULT,
|
||||
);
|
||||
|
||||
return .{ .root = view.root };
|
||||
return .{ .root = view.root, .confirm_button = confirm_button, .cancel_button = cancel_button };
|
||||
}
|
||||
|
||||
fn gtkCancelClick(_: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
|
@ -55,6 +55,7 @@ fn init(self: *ConfigErrors, app: *App) !void {
|
||||
c.gtk_window_set_default_size(gtk_window, 600, 275);
|
||||
c.gtk_window_set_resizable(gtk_window, 0);
|
||||
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "config-errors-window");
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
|
@ -25,7 +25,6 @@ const ResizeOverlay = @import("ResizeOverlay.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const gtk_key = @import("key.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const x11 = @import("x11.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_surface);
|
||||
|
||||
@ -347,6 +346,11 @@ cursor: ?*c.GdkCursor = null,
|
||||
/// pass it to GTK.
|
||||
title_text: ?[:0]const u8 = null,
|
||||
|
||||
/// Our current working directory. We use this value for setting tooltips in
|
||||
/// the headerbar subtitle if we have focus. When set, the text in this buf
|
||||
/// will be null-terminated because we need to pass it to GTK.
|
||||
pwd: ?[:0]const u8 = null,
|
||||
|
||||
/// The timer used to delay title updates in order to prevent flickering.
|
||||
update_title_timer: ?c.guint = null,
|
||||
|
||||
@ -364,10 +368,9 @@ cursor_pos: apprt.CursorPos,
|
||||
inspector: ?*inspector.Inspector = null,
|
||||
|
||||
/// Key input states. See gtkKeyPressed for detailed descriptions.
|
||||
in_keypress: bool = false,
|
||||
in_keyevent: bool = false,
|
||||
im_context: *c.GtkIMContext,
|
||||
im_composing: bool = false,
|
||||
im_commit_buffered: bool = false,
|
||||
im_buf: [128]u8 = undefined,
|
||||
im_len: u7 = 0,
|
||||
|
||||
@ -492,6 +495,17 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
c.gtk_widget_set_focusable(gl_area, 1);
|
||||
c.gtk_widget_set_focus_on_click(gl_area, 1);
|
||||
|
||||
// Set up to handle items being dropped on our surface. Files can be dropped
|
||||
// from Nautilus and strings can be dropped from many programs.
|
||||
const drop_target = c.gtk_drop_target_new(c.G_TYPE_INVALID, c.GDK_ACTION_COPY);
|
||||
errdefer c.g_object_unref(drop_target);
|
||||
var drop_target_types = [_]c.GType{
|
||||
c.gdk_file_list_get_type(),
|
||||
c.G_TYPE_STRING,
|
||||
};
|
||||
c.gtk_drop_target_set_gtypes(drop_target, @ptrCast(&drop_target_types), drop_target_types.len);
|
||||
c.gtk_widget_add_controller(@ptrCast(overlay), @ptrCast(drop_target));
|
||||
|
||||
// Inherit the parent's font size if we have a parent.
|
||||
const font_size: ?font.face.DesiredSize = font_size: {
|
||||
if (!app.config.@"window-inherit-font-size") break :font_size null;
|
||||
@ -545,7 +559,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
.font_size = font_size,
|
||||
.init_config = init_config,
|
||||
.size = .{ .width = 800, .height = 600 },
|
||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||
.cursor_pos = .{ .x = -1, .y = -1 },
|
||||
.im_context = im_context,
|
||||
.cgroup_path = cgroup_path,
|
||||
};
|
||||
@ -574,6 +588,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
||||
_ = c.g_signal_connect_data(im_context, "preedit-changed", c.G_CALLBACK(>kInputPreeditChanged), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "preedit-end", c.G_CALLBACK(>kInputPreeditEnd), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(im_context, "commit", c.G_CALLBACK(>kInputCommit), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(drop_target, "drop", c.G_CALLBACK(>kDrop), self, null, c.G_CONNECT_DEFAULT);
|
||||
}
|
||||
|
||||
fn realize(self: *Surface) !void {
|
||||
@ -618,9 +633,6 @@ fn realize(self: *Surface) !void {
|
||||
try self.core_surface.setFontSize(size);
|
||||
}
|
||||
|
||||
// Set the initial color scheme
|
||||
try self.core_surface.colorSchemeCallback(self.app.getColorScheme());
|
||||
|
||||
// Note we're realized
|
||||
self.realized = true;
|
||||
}
|
||||
@ -628,6 +640,7 @@ fn realize(self: *Surface) !void {
|
||||
pub fn deinit(self: *Surface) void {
|
||||
self.init_config.deinit(self.app.core_app.alloc);
|
||||
if (self.title_text) |title| self.app.core_app.alloc.free(title);
|
||||
if (self.pwd) |pwd| self.app.core_app.alloc.free(pwd);
|
||||
|
||||
// We don't allocate anything if we aren't realized.
|
||||
if (!self.realized) return;
|
||||
@ -840,6 +853,28 @@ pub fn setInitialWindowSize(self: *const Surface, width: u32, height: u32) !void
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setSizeLimits(self: *const Surface, min: apprt.SurfaceSize, max_: ?apprt.SurfaceSize) !void {
|
||||
|
||||
// There's no support for setting max size at the moment.
|
||||
_ = max_;
|
||||
|
||||
// If we are within a split, do not set the size.
|
||||
if (self.container.split() != null) return;
|
||||
|
||||
// This operation only makes sense if we're within a window view
|
||||
// hierarchy and we're the first tab in the window.
|
||||
const window = self.container.window() orelse return;
|
||||
if (window.notebook.nPages() > 1) return;
|
||||
|
||||
// Note: this doesn't properly take into account the window decorations.
|
||||
// I'm not currently sure how to do that.
|
||||
c.gtk_widget_set_size_request(
|
||||
@ptrCast(window.window),
|
||||
@intCast(min.width),
|
||||
@intCast(min.height),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn grabFocus(self: *Surface) void {
|
||||
if (self.container.tab()) |tab| {
|
||||
// If any other surface was focused and zoomed in, set it to non zoomed in
|
||||
@ -876,7 +911,7 @@ fn updateTitleLabels(self: *Surface) void {
|
||||
// I don't know a way around this yet. I've tried re-hiding the
|
||||
// cursor after setting the title but it doesn't work, I think
|
||||
// due to some gtk event loop things...
|
||||
c.gtk_window_set_title(window.window, title.ptr);
|
||||
window.setTitle(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -929,11 +964,27 @@ pub fn getTitle(self: *Surface) ?[:0]const u8 {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Set the current working directory of the surface.
|
||||
///
|
||||
/// In addition, update the tab's tooltip text, and if we are the focused child,
|
||||
/// update the subtitle of the containing window.
|
||||
pub fn setPwd(self: *Surface, pwd: [:0]const u8) !void {
|
||||
// If we have a tab and are the focused child, then we have to update the tab
|
||||
if (self.container.tab()) |tab| {
|
||||
tab.setTooltipText(pwd);
|
||||
|
||||
if (tab.focus_child == self) {
|
||||
if (self.container.window()) |window| {
|
||||
if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const alloc = self.app.core_app.alloc;
|
||||
|
||||
// Failing to set the surface's current working directory is not a big
|
||||
// deal since we just used our slice parameter which is the same value.
|
||||
if (self.pwd) |old| alloc.free(old);
|
||||
self.pwd = alloc.dupeZ(u8, pwd) catch null;
|
||||
}
|
||||
|
||||
pub fn setMouseShape(
|
||||
@ -1080,6 +1131,13 @@ pub fn setClipboardString(
|
||||
if (!confirm) {
|
||||
const clipboard = getClipboard(@ptrCast(self.gl_area), clipboard_type);
|
||||
c.gdk_clipboard_set_text(clipboard, val.ptr);
|
||||
// We only toast if we are copying to the standard clipboard.
|
||||
if (clipboard_type == .standard and
|
||||
self.app.config.@"app-notifications".@"clipboard-copy")
|
||||
{
|
||||
if (self.container.window()) |window|
|
||||
window.sendToast("Copied to clipboard");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1217,7 +1275,7 @@ fn showContextMenu(self: *Surface, x: f32, y: f32) void {
|
||||
};
|
||||
|
||||
c.gtk_popover_set_pointing_to(@ptrCast(@alignCast(window.context_menu)), &rect);
|
||||
self.app.refreshContextMenu(self.core_surface.hasSelection());
|
||||
self.app.refreshContextMenu(window.window, self.core_surface.hasSelection());
|
||||
c.gtk_popover_popup(@ptrCast(@alignCast(window.context_menu)));
|
||||
}
|
||||
|
||||
@ -1321,6 +1379,12 @@ fn gtkResize(area: *c.GtkGLArea, width: c.gint, height: c.gint, ud: ?*anyopaque)
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.container.window()) |window| {
|
||||
window.winproto.resizeEvent() catch |err| {
|
||||
log.warn("failed to notify window protocol of resize={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
self.resize_overlay.maybeShow();
|
||||
}
|
||||
}
|
||||
@ -1426,31 +1490,37 @@ fn gtkMouseMotion(
|
||||
.y = @floatCast(scaled.y),
|
||||
};
|
||||
|
||||
// When the GLArea is resized under the mouse, GTK issues a mouse motion
|
||||
// event. This has the unfortunate side effect of causing focus to potentially
|
||||
// change when `focus-follows-mouse` is enabled. To prevent this, we check
|
||||
// if the cursor is still in the same place as the last event and only grab
|
||||
// focus if it has moved.
|
||||
// There seem to be at least two cases where GTK issues a mouse motion
|
||||
// event without the cursor actually moving:
|
||||
// 1. GLArea is resized under the mouse. This has the unfortunate
|
||||
// side effect of causing focus to potentially change when
|
||||
// `focus-follows-mouse` is enabled.
|
||||
// 2. The window title is updated. This can cause the mouse to unhide
|
||||
// incorrectly when hide-mouse-when-typing is enabled.
|
||||
// To prevent incorrect behavior, we'll only grab focus and
|
||||
// continue with callback logic if the cursor has actually moved.
|
||||
const is_cursor_still = @abs(self.cursor_pos.x - pos.x) < 1 and
|
||||
@abs(self.cursor_pos.y - pos.y) < 1;
|
||||
|
||||
// If we don't have focus, and we want it, grab it.
|
||||
const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (!is_cursor_still and c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
|
||||
self.grabFocus();
|
||||
if (!is_cursor_still) {
|
||||
// If we don't have focus, and we want it, grab it.
|
||||
const gl_widget = @as(*c.GtkWidget, @ptrCast(self.gl_area));
|
||||
if (c.gtk_widget_has_focus(gl_widget) == 0 and self.app.config.@"focus-follows-mouse") {
|
||||
self.grabFocus();
|
||||
}
|
||||
|
||||
// Our pos changed, update
|
||||
self.cursor_pos = pos;
|
||||
|
||||
// Get our modifiers
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
|
||||
self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
|
||||
log.err("error in cursor pos callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
// Our pos changed, update
|
||||
self.cursor_pos = pos;
|
||||
|
||||
// Get our modifiers
|
||||
const gtk_mods = c.gdk_event_get_modifier_state(event);
|
||||
const mods = gtk_key.translateMods(gtk_mods);
|
||||
|
||||
self.core_surface.cursorPosCallback(self.cursor_pos, mods) catch |err| {
|
||||
log.err("error in cursor pos callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkMouseLeave(
|
||||
@ -1530,30 +1600,36 @@ fn gtkKeyReleased(
|
||||
)) 1 else 0;
|
||||
}
|
||||
|
||||
/// Key press event. This is where we do ALL of our key handling,
|
||||
/// translation to keyboard layouts, dead key handling, etc. Key handling
|
||||
/// is complicated so this comment will explain what's going on.
|
||||
/// Key press event (press or release).
|
||||
///
|
||||
/// At a high level, we want to construct an `input.KeyEvent` and
|
||||
/// pass that to `keyCallback`. At a low level, this is more complicated
|
||||
/// than it appears because we need to construct all of this information
|
||||
/// and its not given to us.
|
||||
///
|
||||
/// For press events, we run the keypress through the input method context
|
||||
/// in order to determine if we're in a dead key state, completed unicode
|
||||
/// char, etc. This all happens through various callbacks: preedit, commit,
|
||||
/// etc. These inspect "in_keypress" if they have to and set some instance
|
||||
/// state.
|
||||
/// For all events, we run the GdkEvent through the input method context.
|
||||
/// This allows the input method to capture the event and trigger
|
||||
/// callbacks such as preedit, commit, etc.
|
||||
///
|
||||
/// We then take all of the information in order to determine if we have
|
||||
/// There are a couple important aspects to the prior paragraph: we must
|
||||
/// send ALL events through the input method context. This is because
|
||||
/// input methods use both key press and key release events to determine
|
||||
/// the state of the input method. For example, fcitx uses key release
|
||||
/// events on modifiers (i.e. ctrl+shift) to switch the input method.
|
||||
///
|
||||
/// We set some state to note we're in a key event (self.in_keyevent)
|
||||
/// because some of the input method callbacks change behavior based on
|
||||
/// this state. For example, we don't want to send character events
|
||||
/// like "a" via the input "commit" event if we're actively processing
|
||||
/// a keypress because we'd lose access to the keycode information.
|
||||
/// However, a "commit" event may still happen outside of a keypress
|
||||
/// event from e.g. a tablet or on-screen keyboard.
|
||||
///
|
||||
/// Finally, we take all of the information in order to determine if we have
|
||||
/// a unicode character or if we have to map the keyval to a code to
|
||||
/// get the underlying logical key, etc.
|
||||
///
|
||||
/// Finally, we can emit the keyCallback.
|
||||
///
|
||||
/// Note we ALSO have an IMContext attached directly to the widget
|
||||
/// which can emit preedit and commit callbacks. But, if we're not
|
||||
/// in a keypress, we let those automatically work.
|
||||
/// Then we can emit the keyCallback.
|
||||
pub fn keyEvent(
|
||||
self: *Surface,
|
||||
action: input.Action,
|
||||
@ -1562,26 +1638,15 @@ pub fn keyEvent(
|
||||
keycode: c.guint,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) bool {
|
||||
// log.warn("GTKIM: keyEvent action={}", .{action});
|
||||
const event = c.gtk_event_controller_get_current_event(
|
||||
@ptrCast(ec_key),
|
||||
) orelse return false;
|
||||
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
|
||||
// Get the unshifted unicode value of the keyval. This is used
|
||||
// by the Kitty keyboard protocol.
|
||||
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||
@ptrCast(self.gl_area),
|
||||
event,
|
||||
keycode,
|
||||
);
|
||||
|
||||
// We always reset our committed text when ending a keypress so that
|
||||
// future keypresses don't think we have a commit event.
|
||||
defer self.im_len = 0;
|
||||
|
||||
// We only want to send the event through the IM context if we're a press
|
||||
if (action == .press or action == .repeat) {
|
||||
// The block below is all related to input method handling. See the function
|
||||
// comment for some high level details and then the comments within
|
||||
// the block for more specifics.
|
||||
{
|
||||
// This can trigger an input method so we need to notify the im context
|
||||
// where the cursor is so it can render the dropdowns in the correct
|
||||
// place.
|
||||
@ -1593,41 +1658,94 @@ pub fn keyEvent(
|
||||
.height = 1,
|
||||
});
|
||||
|
||||
// We mark that we're in a keypress event. We use this in our
|
||||
// IM commit callback to determine if we need to send a char callback
|
||||
// to the core surface or not.
|
||||
self.in_keypress = true;
|
||||
defer self.in_keypress = false;
|
||||
// Pass the event through the IM controller. This will return true
|
||||
// if the input method handled the event.
|
||||
//
|
||||
// Confusingly, not all events handled by the input method result
|
||||
// in this returning true so we have to maintain some local state to
|
||||
// find those and in one case we simply lose information.
|
||||
//
|
||||
// - If we change the input method via keypress while we have preedit
|
||||
// text, the input method will commit the pending text but will not
|
||||
// mark it as handled. We use the `was_composing` variable to detect
|
||||
// this case.
|
||||
//
|
||||
// - If we switch input methods (i.e. via ctrl+shift with fcitx),
|
||||
// the input method will handle the key release event but will not
|
||||
// mark it as handled. I don't know any way to detect this case so
|
||||
// it will result in a key event being sent to the key callback.
|
||||
// For Kitty text encoding, this will result in modifiers being
|
||||
// triggered despite being technically consumed. At the time of
|
||||
// writing, both Kitty and Alacritty have the same behavior. I
|
||||
// know of no way to fix this.
|
||||
const was_composing = self.im_composing;
|
||||
const im_handled = filter: {
|
||||
// We note that we're in a keypress because we want some logic to
|
||||
// depend on this. For example, we don't want to send character events
|
||||
// like "a" via the input "commit" event if we're actively processing
|
||||
// a keypress because we'd lose access to the keycode information.
|
||||
self.in_keyevent = true;
|
||||
defer self.in_keyevent = false;
|
||||
break :filter c.gtk_im_context_filter_keypress(
|
||||
self.im_context,
|
||||
event,
|
||||
) != 0;
|
||||
};
|
||||
// log.warn("GTKIM: im_handled={} im_len={} im_composing={}", .{
|
||||
// im_handled,
|
||||
// self.im_len,
|
||||
// self.im_composing,
|
||||
// });
|
||||
|
||||
// Pass the event through the IM controller to handle dead key states.
|
||||
// Filter is true if the event was handled by the IM controller.
|
||||
const im_handled = c.gtk_im_context_filter_keypress(self.im_context, event) != 0;
|
||||
// log.warn("im_handled={} im_len={} im_composing={}", .{ im_handled, self.im_len, self.im_composing });
|
||||
// If the input method handled the event, you would think we would
|
||||
// never proceed with key encoding for Ghostty but that is not the
|
||||
// case. Input methods will handle basic character encoding like
|
||||
// typing "a" and we want to associate that with the key event.
|
||||
// So we have to check additional state to determine if we exit.
|
||||
if (im_handled) {
|
||||
// If we are composing then we're in a preedit state and do
|
||||
// not want to encode any keys. For example: type a deadkey
|
||||
// such as single quote on a US international keyboard layout.
|
||||
if (self.im_composing) return true;
|
||||
|
||||
// If this is a dead key, then we're composing a character and
|
||||
// we need to set our proper preedit state.
|
||||
if (self.im_composing) preedit: {
|
||||
const text = self.im_buf[0..self.im_len];
|
||||
self.core_surface.preeditCallback(text) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
break :preedit;
|
||||
};
|
||||
// If we were composing and now we're not it means that we committed
|
||||
// the text. We also don't want to encode a key event for this.
|
||||
// Example: enable Japanese input method, press "konn" and then
|
||||
// press enter. The final enter should not be encoded and "konn"
|
||||
// (in hiragana) should be written as "こん".
|
||||
if (was_composing) return true;
|
||||
|
||||
// If we're composing then we don't want to send the key
|
||||
// event to the core surface so we always return immediately.
|
||||
if (im_handled) return true;
|
||||
} else {
|
||||
// If we aren't composing, then we set our preedit to
|
||||
// empty no matter what.
|
||||
self.core_surface.preeditCallback(null) catch {};
|
||||
|
||||
// If the IM handled this and we have no text, then we just
|
||||
// return because this probably just changed the input method
|
||||
// or something.
|
||||
if (im_handled and self.im_len == 0) return true;
|
||||
// Not composing and our input method buffer is empty. This could
|
||||
// mean that the input method reacted to this event by activating
|
||||
// an onscreen keyboard or something equivalent. We don't know.
|
||||
// But the input method handled it and didn't give us text so
|
||||
// we will just assume we should not encode this. This handles a
|
||||
// real scenario when ibus starts the emoji input method
|
||||
// (super+.).
|
||||
if (self.im_len == 0) return true;
|
||||
}
|
||||
|
||||
// At this point, for the sake of explanation of internal state:
|
||||
// it is possible that im_len > 0 and im_composing == false. This
|
||||
// means that we received a commit event from the input method that
|
||||
// we want associated with the key event. This is common: its how
|
||||
// basic character translation for simple inputs like "a" work.
|
||||
}
|
||||
|
||||
// We always reset the length of the im buffer. There's only one scenario
|
||||
// we reach this point with im_len > 0 and that's if we received a commit
|
||||
// event from the input method. We don't want to keep that state around
|
||||
// since we've handled it here.
|
||||
defer self.im_len = 0;
|
||||
|
||||
// Get the keyvals for this event.
|
||||
const keyval_unicode = c.gdk_keyval_to_unicode(keyval);
|
||||
const keyval_unicode_unshifted: u21 = gtk_key.keyvalUnicodeUnshifted(
|
||||
@ptrCast(self.gl_area),
|
||||
event,
|
||||
keycode,
|
||||
);
|
||||
|
||||
// We want to get the physical unmapped key to process physical keybinds.
|
||||
// (These are keybinds explicitly marked as requesting physical mapping).
|
||||
const physical_key = keycode: for (input.keycodes.entries) |entry| {
|
||||
@ -1636,11 +1754,10 @@ pub fn keyEvent(
|
||||
|
||||
// Get our modifier for the event
|
||||
const mods: input.Mods = gtk_key.eventMods(
|
||||
@ptrCast(self.gl_area),
|
||||
event,
|
||||
physical_key,
|
||||
gtk_mods,
|
||||
if (self.app.x11_xkb) |*xkb| xkb else null,
|
||||
&self.app.winproto,
|
||||
);
|
||||
|
||||
// Get our consumed modifiers
|
||||
@ -1761,12 +1878,11 @@ fn gtkInputPreeditStart(
|
||||
_: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
//log.debug("preedit start", .{});
|
||||
// log.warn("GTKIM: preedit start", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// Mark that we are now composing a string with a dead key state.
|
||||
// We'll record the string in the preedit-changed callback.
|
||||
// Start our composing state for the input method and reset our
|
||||
// input buffer to empty.
|
||||
self.im_composing = true;
|
||||
self.im_len = 0;
|
||||
}
|
||||
@ -1775,54 +1891,35 @@ fn gtkInputPreeditChanged(
|
||||
ctx: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
// log.warn("GTKIM: preedit change", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
// If there's buffered character, send the characters directly to the surface.
|
||||
if (self.im_composing and self.im_commit_buffered) {
|
||||
defer self.im_commit_buffered = false;
|
||||
defer self.im_len = 0;
|
||||
_ = self.core_surface.keyCallback(.{
|
||||
.action = .press,
|
||||
.key = .invalid,
|
||||
.physical_key = .invalid,
|
||||
.mods = .{},
|
||||
.consumed_mods = .{},
|
||||
.composing = false,
|
||||
.utf8 = self.im_buf[0..self.im_len],
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// Get our pre-edit string that we'll use to show the user.
|
||||
var buf: [*c]u8 = undefined;
|
||||
_ = c.gtk_im_context_get_preedit_string(ctx, &buf, null, null);
|
||||
defer c.g_free(buf);
|
||||
const str = std.mem.sliceTo(buf, 0);
|
||||
|
||||
// If our string becomes empty we ignore this. This can happen after
|
||||
// a commit event when the preedit is being cleared and we don't want
|
||||
// to set im_len to zero. This is safe because preeditstart always sets
|
||||
// im_len to zero.
|
||||
if (str.len == 0) return;
|
||||
|
||||
// Copy the preedit string into the im_buf. This is safe because
|
||||
// commit will always overwrite this.
|
||||
self.im_len = @intCast(@min(self.im_buf.len, str.len));
|
||||
@memcpy(self.im_buf[0..self.im_len], str);
|
||||
// Update our preedit state in Ghostty core
|
||||
self.core_surface.preeditCallback(str) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkInputPreeditEnd(
|
||||
_: *c.GtkIMContext,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
//log.debug("preedit end", .{});
|
||||
// log.warn("GTKIM: preedit end", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
if (!self.in_keypress) return;
|
||||
|
||||
// End our composing state for GTK, allowing us to commit the text.
|
||||
self.im_composing = false;
|
||||
|
||||
// End our preedit state in Ghostty core
|
||||
self.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn gtkInputCommit(
|
||||
@ -1830,38 +1927,45 @@ fn gtkInputCommit(
|
||||
bytes: [*:0]u8,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
// log.warn("GTKIM: input commit", .{});
|
||||
const self = userdataSelf(ud.?);
|
||||
const str = std.mem.sliceTo(bytes, 0);
|
||||
|
||||
// If we're in a key event, then we want to buffer the commit so
|
||||
// that we can send the proper keycallback followed by the char
|
||||
// callback.
|
||||
if (self.in_keypress) {
|
||||
if (str.len <= self.im_buf.len) {
|
||||
@memcpy(self.im_buf[0..str.len], str);
|
||||
self.im_len = @intCast(str.len);
|
||||
|
||||
// If composing is done and character should be committed,
|
||||
// It should be committed in preedit callback.
|
||||
if (self.im_composing) {
|
||||
self.im_commit_buffered = true;
|
||||
}
|
||||
|
||||
// log.debug("input commit len={}", .{self.im_len});
|
||||
} else {
|
||||
// If we're in a keyEvent (i.e. a keyboard event) and we're not composing,
|
||||
// then this is just a normal key press resulting in UTF-8 text. We
|
||||
// want the keyEvent to handle this so that the UTF-8 text can be associated
|
||||
// with a keyboard event.
|
||||
if (!self.im_composing and self.in_keyevent) {
|
||||
if (str.len > self.im_buf.len) {
|
||||
log.warn("not enough buffer space for input method commit", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy our committed text to the buffer
|
||||
@memcpy(self.im_buf[0..str.len], str);
|
||||
self.im_len = @intCast(str.len);
|
||||
|
||||
// log.debug("input commit len={}", .{self.im_len});
|
||||
return;
|
||||
}
|
||||
|
||||
// This prevents staying in composing state after commit even though
|
||||
// input method has changed.
|
||||
// If we reach this point from above it means we're composing OR
|
||||
// not in a keypress. In either case, we want to commit the text
|
||||
// given to us because that's what GTK is asking us to do. If we're
|
||||
// not in a keypress it means that this commit came via a non-keyboard
|
||||
// event (i.e. on-screen keyboard, tablet of some kind, etc.).
|
||||
|
||||
// Committing ends composing state
|
||||
self.im_composing = false;
|
||||
|
||||
// We're not in a keypress, so this was sent from an on-screen emoji
|
||||
// keyboard or something like that. Send the characters directly to
|
||||
// the surface.
|
||||
// End our preedit state. Well-behaved input methods do this for us
|
||||
// by triggering a preedit-end event but some do not (ibus 1.5.29).
|
||||
self.core_surface.preeditCallback(null) catch |err| {
|
||||
log.err("error in preedit callback err={}", .{err});
|
||||
};
|
||||
|
||||
// Send the text to the core surface, associated with no key (an
|
||||
// invalid key, which should produce no PTY encoding).
|
||||
_ = self.core_surface.keyCallback(.{
|
||||
.action = .press,
|
||||
.key = .invalid,
|
||||
@ -1871,7 +1975,7 @@ fn gtkInputCommit(
|
||||
.composing = false,
|
||||
.utf8 = str,
|
||||
}) catch |err| {
|
||||
log.err("error in key callback err={}", .{err});
|
||||
log.warn("error in key callback err={}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
@ -1889,6 +1993,12 @@ fn gtkFocusEnter(_: *c.GtkEventControllerFocus, ud: ?*anyopaque) callconv(.C) vo
|
||||
self.unfocused_widget = null;
|
||||
}
|
||||
|
||||
if (self.pwd) |pwd| {
|
||||
if (self.container.window()) |window| {
|
||||
if (self.app.config.@"window-subtitle" == .@"working-directory") window.setSubtitle(pwd);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify our surface
|
||||
self.core_surface.focusCallback(true) catch |err| {
|
||||
log.err("error in focus callback err={}", .{err});
|
||||
@ -2018,3 +2128,95 @@ pub fn setSplitZoom(self: *Surface, new_split_zoom: bool) void {
|
||||
pub fn toggleSplitZoom(self: *Surface) void {
|
||||
self.setSplitZoom(!self.zoomed_in);
|
||||
}
|
||||
|
||||
/// Handle items being dropped on our surface.
|
||||
fn gtkDrop(
|
||||
_: *c.GtkDropTarget,
|
||||
value: *c.GValue,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c.gboolean {
|
||||
_ = x;
|
||||
_ = y;
|
||||
const self = userdataSelf(ud.?);
|
||||
const alloc = self.app.core_app.alloc;
|
||||
|
||||
if (g_value_holds(value, c.G_TYPE_BOXED)) {
|
||||
var data = std.ArrayList(u8).init(alloc);
|
||||
defer data.deinit();
|
||||
|
||||
var shell_escape_writer: internal_os.ShellEscapeWriter(std.ArrayList(u8).Writer) = .{
|
||||
.child_writer = data.writer(),
|
||||
};
|
||||
const writer = shell_escape_writer.writer();
|
||||
|
||||
const fl: *c.GdkFileList = @ptrCast(c.g_value_get_boxed(value));
|
||||
var l = c.gdk_file_list_get_files(fl);
|
||||
|
||||
while (l != null) : (l = l.*.next) {
|
||||
const file: *c.GFile = @ptrCast(l.*.data);
|
||||
const path = c.g_file_get_path(file) orelse continue;
|
||||
|
||||
writer.writeAll(std.mem.span(path)) catch |err| {
|
||||
log.err("unable to write path to buffer: {}", .{err});
|
||||
continue;
|
||||
};
|
||||
writer.writeAll("\n") catch |err| {
|
||||
log.err("unable to write to buffer: {}", .{err});
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
const string = data.toOwnedSliceSentinel(0) catch |err| {
|
||||
log.err("unable to convert to a slice: {}", .{err});
|
||||
return 1;
|
||||
};
|
||||
defer alloc.free(string);
|
||||
|
||||
self.doPaste(string);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (g_value_holds(value, c.G_TYPE_STRING)) {
|
||||
if (c.g_value_get_string(value)) |string| {
|
||||
self.doPaste(std.mem.span(string));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn doPaste(self: *Surface, data: [:0]const u8) void {
|
||||
if (data.len == 0) return;
|
||||
|
||||
self.core_surface.completeClipboardRequest(.paste, data, false) catch |err| switch (err) {
|
||||
error.UnsafePaste,
|
||||
error.UnauthorizedPaste,
|
||||
=> {
|
||||
ClipboardConfirmationWindow.create(
|
||||
self.app,
|
||||
data,
|
||||
&self.core_surface,
|
||||
.paste,
|
||||
) catch |window_err| {
|
||||
log.err("failed to create clipboard confirmation window err={}", .{window_err});
|
||||
};
|
||||
},
|
||||
error.OutOfMemory,
|
||||
error.NoSpaceLeft,
|
||||
=> log.err("failed to complete clipboard request err={}", .{err}),
|
||||
};
|
||||
}
|
||||
|
||||
/// Check a GValue to see what's type its wrapping. This is equivalent to GTK's
|
||||
/// `G_VALUE_HOLDS` macro but Zig's C translator does not like it.
|
||||
fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
|
||||
if (value_) |value| {
|
||||
if (value.*.g_type == g_type) return true;
|
||||
return c.g_type_check_value_holds(value, g_type) != 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
@ -121,10 +121,63 @@ pub fn remove(self: *Tab) void {
|
||||
self.window.closeTab(self);
|
||||
}
|
||||
|
||||
pub fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
/// 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.close(s.core_surface.needsConfirmQuit()),
|
||||
.split => |s| {
|
||||
if (needsConfirm(s.top_left) or needsConfirm(s.bottom_right)) {
|
||||
const alert = c.gtk_message_dialog_new(
|
||||
tab.window.window,
|
||||
c.GTK_DIALOG_MODAL,
|
||||
c.GTK_MESSAGE_QUESTION,
|
||||
c.GTK_BUTTONS_YES_NO,
|
||||
"Close this tab?",
|
||||
);
|
||||
c.gtk_message_dialog_format_secondary_text(
|
||||
@ptrCast(alert),
|
||||
"All terminal sessions in this tab will be terminated.",
|
||||
);
|
||||
|
||||
// We want the "yes" to appear destructive.
|
||||
const yes_widget = c.gtk_dialog_get_widget_for_response(
|
||||
@ptrCast(alert),
|
||||
c.GTK_RESPONSE_YES,
|
||||
);
|
||||
c.gtk_widget_add_css_class(yes_widget, "destructive-action");
|
||||
|
||||
// We want the "no" to be the default action
|
||||
c.gtk_dialog_set_default_response(
|
||||
@ptrCast(alert),
|
||||
c.GTK_RESPONSE_NO,
|
||||
);
|
||||
|
||||
_ = c.g_signal_connect_data(alert, "response", c.G_CALLBACK(>kTabCloseConfirmation), tab, null, c.G_CONNECT_DEFAULT);
|
||||
c.gtk_widget_show(alert);
|
||||
return;
|
||||
}
|
||||
tab.remove();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkTabCloseConfirmation(
|
||||
alert: *c.GtkMessageDialog,
|
||||
response: c.gint,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
const window = tab.window;
|
||||
window.closeTab(tab);
|
||||
c.gtk_window_destroy(@ptrCast(alert));
|
||||
if (response != c.GTK_RESPONSE_YES) return;
|
||||
tab.remove();
|
||||
}
|
||||
|
||||
fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
@ -135,17 +188,3 @@ fn gtkDestroy(v: *c.GtkWidget, ud: ?*anyopaque) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
tab.destroy(tab.window.app.core_app.alloc);
|
||||
}
|
||||
|
||||
pub fn gtkTabClick(
|
||||
gesture: *c.GtkGestureClick,
|
||||
_: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Tab = @ptrCast(@alignCast(ud));
|
||||
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
|
||||
self.remove();
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ const gtk_key = @import("key.zig");
|
||||
const Notebook = @import("notebook.zig").Notebook;
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
const version = @import("version.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
|
||||
@ -36,7 +37,7 @@ window: *c.GtkWindow,
|
||||
/// The header bar for the window. This is possibly null since it can be
|
||||
/// disabled using gtk-titlebar. This is either an AdwHeaderBar or
|
||||
/// GtkHeaderBar depending on if adw is enabled and linked.
|
||||
header: ?HeaderBar,
|
||||
headerbar: HeaderBar,
|
||||
|
||||
/// The tab overview for the window. This is possibly null since there is no
|
||||
/// taboverview without a AdwApplicationWindow (libadwaita >= 1.4.0).
|
||||
@ -55,6 +56,9 @@ toast_overlay: ?*c.GtkWidget,
|
||||
/// See adwTabOverviewOpen for why we have this.
|
||||
adw_tab_overview_focus_timer: ?c.guint = null,
|
||||
|
||||
/// State and logic for windowing protocol for a window.
|
||||
winproto: winproto.Window,
|
||||
|
||||
pub fn create(alloc: Allocator, app: *App) !*Window {
|
||||
// Allocate a fixed pointer for our window. We try to minimize
|
||||
// allocations but windows and other GUI requirements are so minimal
|
||||
@ -74,11 +78,12 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.window = undefined,
|
||||
.header = null,
|
||||
.headerbar = undefined,
|
||||
.tab_overview = null,
|
||||
.notebook = undefined,
|
||||
.context_menu = undefined,
|
||||
.toast_overlay = undefined,
|
||||
.winproto = .none,
|
||||
};
|
||||
|
||||
// Create the window
|
||||
@ -99,6 +104,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
self.window = gtk_window;
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "terminal-window");
|
||||
|
||||
// GTK4 grabs F10 input by default to focus the menubar icon. We want
|
||||
@ -114,11 +120,6 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_add_css_class(@ptrCast(gtk_window), "window-theme-ghostty");
|
||||
}
|
||||
|
||||
// Remove the window's background if any of the widgets need to be transparent
|
||||
if (app.config.@"background-opacity" < 1) {
|
||||
c.gtk_widget_remove_css_class(@ptrCast(window), "background");
|
||||
}
|
||||
|
||||
// Create our box which will hold our widgets in the main content area.
|
||||
const box = c.gtk_box_new(c.GTK_ORIENTATION_VERTICAL, 0);
|
||||
|
||||
@ -150,82 +151,66 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
break :overview tab_overview;
|
||||
} else null;
|
||||
|
||||
// gtk-titlebar can be used to disable the header bar (but keep
|
||||
// the window manager's decorations). We create this no matter if we
|
||||
// are decorated or not because we can have a keybind to toggle the
|
||||
// decorations.
|
||||
if (app.config.@"gtk-titlebar") {
|
||||
const header = HeaderBar.init(self);
|
||||
// gtk-titlebar can be used to disable the header bar (but keep the window
|
||||
// manager's decorations). We create this no matter if we are decorated or
|
||||
// not because we can have a keybind to toggle the decorations.
|
||||
self.headerbar.init();
|
||||
|
||||
// If we are not decorated then we hide the titlebar.
|
||||
header.setVisible(app.config.@"window-decoration");
|
||||
|
||||
{
|
||||
const btn = c.gtk_menu_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
header.packEnd(btn);
|
||||
}
|
||||
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
|
||||
const btn = switch (app.config.@"gtk-tabs-location") {
|
||||
.top, .bottom, .left, .right => btn: {
|
||||
const btn = c.gtk_toggle_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
|
||||
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
|
||||
_ = c.g_object_bind_property(
|
||||
btn,
|
||||
"active",
|
||||
tab_overview,
|
||||
"open",
|
||||
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
|
||||
);
|
||||
|
||||
break :btn btn;
|
||||
},
|
||||
|
||||
.hidden => btn: {
|
||||
const btn = c.adw_tab_button_new();
|
||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
|
||||
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
||||
break :btn btn;
|
||||
},
|
||||
};
|
||||
|
||||
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
|
||||
header.packEnd(btn);
|
||||
}
|
||||
|
||||
{
|
||||
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
||||
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
||||
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
header.packStart(btn);
|
||||
}
|
||||
|
||||
self.header = header;
|
||||
{
|
||||
const btn = c.gtk_menu_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "Main Menu");
|
||||
c.gtk_menu_button_set_icon_name(@ptrCast(btn), "open-menu-symbolic");
|
||||
c.gtk_menu_button_set_menu_model(@ptrCast(btn), @ptrCast(@alignCast(app.menu)));
|
||||
self.headerbar.packEnd(btn);
|
||||
}
|
||||
|
||||
// If we are disabling decorations then disable them right away.
|
||||
if (!app.config.@"window-decoration") {
|
||||
c.gtk_window_set_decorated(gtk_window, 0);
|
||||
// If we're using an AdwWindow then we can support the tab overview.
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
if (comptime !adwaita.versionAtLeast(1, 4, 0)) unreachable;
|
||||
assert(self.app.config.@"gtk-adwaita" and adwaita.versionAtLeast(1, 4, 0));
|
||||
const btn = switch (app.config.@"gtk-tabs-location") {
|
||||
.top, .bottom, .left, .right => btn: {
|
||||
const btn = c.gtk_toggle_button_new();
|
||||
c.gtk_widget_set_tooltip_text(btn, "View Open Tabs");
|
||||
c.gtk_button_set_icon_name(@ptrCast(btn), "view-grid-symbolic");
|
||||
_ = c.g_object_bind_property(
|
||||
btn,
|
||||
"active",
|
||||
tab_overview,
|
||||
"open",
|
||||
c.G_BINDING_BIDIRECTIONAL | c.G_BINDING_SYNC_CREATE,
|
||||
);
|
||||
|
||||
// Fix any artifacting that may occur in window corners.
|
||||
if (app.config.@"gtk-titlebar") {
|
||||
c.gtk_widget_add_css_class(window, "without-window-decoration-and-with-titlebar");
|
||||
}
|
||||
break :btn btn;
|
||||
},
|
||||
|
||||
.hidden => btn: {
|
||||
const btn = c.adw_tab_button_new();
|
||||
c.adw_tab_button_set_view(@ptrCast(btn), self.notebook.adw.tab_view);
|
||||
c.gtk_actionable_set_action_name(@ptrCast(btn), "overview.open");
|
||||
break :btn btn;
|
||||
},
|
||||
};
|
||||
|
||||
c.gtk_widget_set_focus_on_click(btn, c.FALSE);
|
||||
self.headerbar.packEnd(btn);
|
||||
}
|
||||
|
||||
{
|
||||
const btn = c.gtk_button_new_from_icon_name("tab-new-symbolic");
|
||||
c.gtk_widget_set_tooltip_text(btn, "New Tab");
|
||||
_ = c.g_signal_connect_data(btn, "clicked", c.G_CALLBACK(>kTabNewClick), self, null, c.G_CONNECT_DEFAULT);
|
||||
self.headerbar.packStart(btn);
|
||||
}
|
||||
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::decorated", c.G_CALLBACK(>kWindowNotifyDecorated), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::maximized", c.G_CALLBACK(>kWindowNotifyMaximized), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gtk_window, "notify::fullscreened", c.G_CALLBACK(>kWindowNotifyFullscreened), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// If Adwaita is enabled and is older than 1.4.0 we don't have the tab overview and so we
|
||||
// need to stick the headerbar into the content box.
|
||||
if (!adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
|
||||
if (self.header) |h| {
|
||||
c.gtk_box_append(@ptrCast(box), h.asWidget());
|
||||
}
|
||||
c.gtk_box_append(@ptrCast(box), self.headerbar.asWidget());
|
||||
}
|
||||
|
||||
// In debug we show a warning and apply the 'devel' class to the window.
|
||||
@ -273,10 +258,13 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
}
|
||||
|
||||
self.context_menu = c.gtk_popover_menu_new_from_model(@ptrCast(@alignCast(self.app.context_menu)));
|
||||
c.gtk_widget_set_parent(self.context_menu, window);
|
||||
c.gtk_widget_set_parent(self.context_menu, box);
|
||||
c.gtk_popover_set_has_arrow(@ptrCast(@alignCast(self.context_menu)), 0);
|
||||
c.gtk_widget_set_halign(self.context_menu, c.GTK_ALIGN_START);
|
||||
|
||||
// If we want the window to be maximized, we do that here.
|
||||
if (app.config.maximize) c.gtk_window_maximize(self.window);
|
||||
|
||||
// If we are in fullscreen mode, new windows start fullscreen.
|
||||
if (app.config.fullscreen) c.gtk_window_fullscreen(self.window);
|
||||
|
||||
@ -289,6 +277,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
|
||||
// All of our events
|
||||
_ = c.g_signal_connect_data(self.context_menu, "closed", c.G_CALLBACK(>kRefocusTerm), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "realize", c.G_CALLBACK(>kRealize), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(ec_key_press, "key-pressed", c.G_CALLBACK(>kKeyPressed), self, null, c.G_CONNECT_DEFAULT);
|
||||
@ -299,10 +288,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.versionAtLeast(1, 4, 0) and adwaita.enabled(&self.app.config)) {
|
||||
const toolbar_view: *c.AdwToolbarView = @ptrCast(c.adw_toolbar_view_new());
|
||||
|
||||
if (self.header) |header| {
|
||||
const header_widget = header.asWidget();
|
||||
c.adw_toolbar_view_add_top_bar(toolbar_view, header_widget);
|
||||
}
|
||||
c.adw_toolbar_view_add_top_bar(toolbar_view, self.headerbar.asWidget());
|
||||
|
||||
if (self.app.config.@"gtk-tabs-location" != .hidden) {
|
||||
const tab_bar = c.adw_tab_bar_new();
|
||||
@ -375,10 +361,8 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
box,
|
||||
);
|
||||
} else {
|
||||
c.gtk_window_set_titlebar(gtk_window, self.headerbar.asWidget());
|
||||
c.gtk_window_set_child(gtk_window, box);
|
||||
if (self.header) |h| {
|
||||
c.gtk_window_set_titlebar(gtk_window, h.asWidget());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,6 +370,74 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
c.gtk_widget_show(window);
|
||||
}
|
||||
|
||||
pub fn updateConfig(
|
||||
self: *Window,
|
||||
config: *const configpkg.Config,
|
||||
) !void {
|
||||
self.winproto.updateConfigEvent(config) catch |err| {
|
||||
// We want to continue attempting to make the other config
|
||||
// changes necessary so we just log the error and continue.
|
||||
log.warn("failed to update window protocol config error={}", .{err});
|
||||
};
|
||||
|
||||
// We always resync our appearance whenever the config changes.
|
||||
try self.syncAppearance(config);
|
||||
}
|
||||
|
||||
/// Updates appearance based on config settings. Will be called once upon window
|
||||
/// realization, and every time the config is reloaded.
|
||||
///
|
||||
/// TODO: Many of the initial style settings in `create` could possibly be made
|
||||
/// reactive by moving them here.
|
||||
pub fn syncAppearance(self: *Window, config: *const configpkg.Config) !void {
|
||||
self.winproto.syncAppearance() catch |err| {
|
||||
log.warn("failed to sync winproto appearance error={}", .{err});
|
||||
};
|
||||
|
||||
toggleCssClass(
|
||||
@ptrCast(self.window),
|
||||
"background",
|
||||
config.@"background-opacity" >= 1,
|
||||
);
|
||||
|
||||
// If we are disabling CSDs then disable them right away.
|
||||
const csd_enabled = self.winproto.clientSideDecorationEnabled();
|
||||
c.gtk_window_set_decorated(self.window, @intFromBool(csd_enabled));
|
||||
|
||||
// If we are not decorated then we hide the titlebar.
|
||||
self.headerbar.setVisible(config.@"gtk-titlebar" and csd_enabled);
|
||||
|
||||
// Disable the title buttons (close, maximize, minimize, ...)
|
||||
// *inside* the tab overview if CSDs are disabled.
|
||||
// We do spare the search button, though.
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&self.app.config))
|
||||
{
|
||||
if (self.tab_overview) |tab_overview| {
|
||||
c.adw_tab_overview_set_show_start_title_buttons(
|
||||
@ptrCast(tab_overview),
|
||||
@intFromBool(csd_enabled),
|
||||
);
|
||||
c.adw_tab_overview_set_show_end_title_buttons(
|
||||
@ptrCast(tab_overview),
|
||||
@intFromBool(csd_enabled),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggleCssClass(
|
||||
widget: *c.GtkWidget,
|
||||
class: [:0]const u8,
|
||||
v: bool,
|
||||
) void {
|
||||
if (v) {
|
||||
c.gtk_widget_add_css_class(widget, class);
|
||||
} else {
|
||||
c.gtk_widget_remove_css_class(widget, class);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the GTK actions for the window scope. Actions are how GTK handles
|
||||
/// menus and such. The menu is defined in App.zig but the action is defined
|
||||
/// here. The string name binds them.
|
||||
@ -423,11 +475,23 @@ fn initActions(self: *Window) void {
|
||||
pub fn deinit(self: *Window) void {
|
||||
c.gtk_widget_unparent(@ptrCast(self.context_menu));
|
||||
|
||||
self.winproto.deinit(self.app.core_app.alloc);
|
||||
|
||||
if (self.adw_tab_overview_focus_timer) |timer| {
|
||||
_ = c.g_source_remove(timer);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the title of the window.
|
||||
pub fn setTitle(self: *Window, title: [:0]const u8) void {
|
||||
self.headerbar.setTitle(title);
|
||||
}
|
||||
|
||||
/// Set the subtitle of the window if it has one.
|
||||
pub fn setSubtitle(self: *Window, subtitle: [:0]const u8) void {
|
||||
self.headerbar.setSubtitle(subtitle);
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
||||
const alloc = self.app.core_app.alloc;
|
||||
@ -473,9 +537,9 @@ pub fn moveTab(self: *Window, surface: *Surface, position: c_int) void {
|
||||
self.notebook.moveTab(tab, position);
|
||||
}
|
||||
|
||||
/// Go to the next tab for a surface.
|
||||
/// Go to the last tab for a surface.
|
||||
pub fn gotoLastTab(self: *Window) void {
|
||||
const max = self.notebook.nPages() -| 1;
|
||||
const max = self.notebook.nPages();
|
||||
self.gotoTab(@intCast(max));
|
||||
}
|
||||
|
||||
@ -498,6 +562,15 @@ pub fn toggleTabOverview(self: *Window) void {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle the maximized state for this window.
|
||||
pub fn toggleMaximize(self: *Window) void {
|
||||
if (c.gtk_window_is_maximized(self.window) == 0) {
|
||||
c.gtk_window_maximize(self.window);
|
||||
} else {
|
||||
c.gtk_window_unmaximize(self.window);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle fullscreen for this window.
|
||||
pub fn toggleFullscreen(self: *Window) void {
|
||||
const is_fullscreen = c.gtk_window_is_fullscreen(self.window);
|
||||
@ -510,24 +583,11 @@ pub fn toggleFullscreen(self: *Window) void {
|
||||
|
||||
/// Toggle the window decorations for this window.
|
||||
pub fn toggleWindowDecorations(self: *Window) void {
|
||||
const old_decorated = c.gtk_window_get_decorated(self.window) == 1;
|
||||
const new_decorated = !old_decorated;
|
||||
c.gtk_window_set_decorated(self.window, @intFromBool(new_decorated));
|
||||
|
||||
// Fix any artifacting that may occur in window corners.
|
||||
if (new_decorated) {
|
||||
c.gtk_widget_add_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
|
||||
} else {
|
||||
c.gtk_widget_remove_css_class(@ptrCast(self.window), "without-window-decoration-and-with-titlebar");
|
||||
}
|
||||
|
||||
// If we have a titlebar, then we also show/hide it depending on the
|
||||
// decorated state. GTK tends to consider the titlebar part of the frame
|
||||
// and hides it with decorations, but libadwaita doesn't. This makes it
|
||||
// explicit.
|
||||
if (self.header) |headerbar| {
|
||||
headerbar.setVisible(new_decorated);
|
||||
}
|
||||
self.app.config.@"window-decoration" = switch (self.app.config.@"window-decoration") {
|
||||
.auto, .client, .server => .none,
|
||||
.none => .client,
|
||||
};
|
||||
self.updateConfig(&self.app.config) catch {};
|
||||
}
|
||||
|
||||
/// Grabs focus on the currently selected tab.
|
||||
@ -542,7 +602,7 @@ pub fn onConfigReloaded(self: *Window) void {
|
||||
self.sendToast("Reloaded the configuration");
|
||||
}
|
||||
|
||||
fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
pub fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) return;
|
||||
const toast_overlay = self.toast_overlay orelse return;
|
||||
const toast = c.adw_toast_new(title);
|
||||
@ -550,6 +610,85 @@ fn sendToast(self: *Window, title: [:0]const u8) void {
|
||||
c.adw_toast_overlay_add_toast(@ptrCast(toast_overlay), toast);
|
||||
}
|
||||
|
||||
fn gtkRealize(v: *c.GtkWindow, ud: ?*anyopaque) callconv(.C) bool {
|
||||
const self = userdataSelf(ud.?);
|
||||
|
||||
// Initialize our window protocol logic
|
||||
if (winproto.Window.init(
|
||||
self.app.core_app.alloc,
|
||||
&self.app.winproto,
|
||||
v,
|
||||
&self.app.config,
|
||||
)) |winproto_win| {
|
||||
self.winproto = winproto_win;
|
||||
} else |err| {
|
||||
log.warn("failed to initialize window protocol error={}", .{err});
|
||||
}
|
||||
|
||||
// When we are realized we always setup our appearance
|
||||
self.syncAppearance(&self.app.config) catch |err| {
|
||||
log.err("failed to initialize appearance={}", .{err});
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn gtkWindowNotifyMaximized(
|
||||
_: *c.GObject,
|
||||
_: *c.GParamSpec,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self = userdataSelf(ud orelse return);
|
||||
|
||||
// Only toggle visibility of the header bar when we're using CSDs,
|
||||
// and actually intend on displaying the header bar
|
||||
if (!self.winproto.clientSideDecorationEnabled()) return;
|
||||
|
||||
// If we aren't maximized, we should show the headerbar again
|
||||
// if it was originally visible.
|
||||
const maximized = c.gtk_window_is_maximized(self.window) != 0;
|
||||
if (!maximized) {
|
||||
self.headerbar.setVisible(self.app.config.@"gtk-titlebar");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we are maximized, we should hide the headerbar if requested.
|
||||
if (self.app.config.@"gtk-titlebar-hide-when-maximized") {
|
||||
self.headerbar.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkWindowNotifyDecorated(
|
||||
object: *c.GObject,
|
||||
_: *c.GParamSpec,
|
||||
_: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const is_decorated = c.gtk_window_get_decorated(@ptrCast(object)) == 1;
|
||||
|
||||
// Fix any artifacting that may occur in window corners. The .ssd CSS
|
||||
// class is defined in the GtkWindow documentation:
|
||||
// https://docs.gtk.org/gtk4/class.Window.html#css-nodes. A definition
|
||||
// for .ssd is provided by GTK and Adwaita.
|
||||
toggleCssClass(@ptrCast(object), "ssd", !is_decorated);
|
||||
toggleCssClass(@ptrCast(object), "no-border-radius", !is_decorated);
|
||||
}
|
||||
|
||||
fn gtkWindowNotifyFullscreened(
|
||||
object: *c.GObject,
|
||||
_: *c.GParamSpec,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self = userdataSelf(ud orelse return);
|
||||
const fullscreened = c.gtk_window_is_fullscreen(@ptrCast(object)) != 0;
|
||||
if (!fullscreened) {
|
||||
const csd_enabled = self.winproto.clientSideDecorationEnabled();
|
||||
self.headerbar.setVisible(self.app.config.@"gtk-titlebar" and csd_enabled);
|
||||
return;
|
||||
}
|
||||
|
||||
self.headerbar.setVisible(false);
|
||||
}
|
||||
|
||||
// Note: we MUST NOT use the GtkButton parameter because gtkActionNewTab
|
||||
// sends an undefined value.
|
||||
fn gtkTabNewClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
@ -894,10 +1033,6 @@ fn gtkActionCopy(
|
||||
log.warn("error performing binding action error={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
if (self.app.config.@"adw-toast".@"clipboard-copy") {
|
||||
self.sendToast("Copied to clipboard");
|
||||
}
|
||||
}
|
||||
|
||||
fn gtkActionPaste(
|
||||
|
@ -11,9 +11,14 @@ pub const c = @cImport({
|
||||
// Add in X11-specific GDK backend which we use for specific things
|
||||
// (e.g. X11 window class).
|
||||
@cInclude("gdk/x11/gdkx.h");
|
||||
@cInclude("X11/Xlib.h");
|
||||
@cInclude("X11/Xatom.h");
|
||||
// Xkb for X11 state handling
|
||||
@cInclude("X11/XKBlib.h");
|
||||
}
|
||||
if (build_options.wayland) {
|
||||
@cInclude("gdk/wayland/gdkwayland.h");
|
||||
}
|
||||
|
||||
// generated header files
|
||||
@cInclude("ghostty_resources.h");
|
||||
|
@ -4,70 +4,55 @@ const c = @import("c.zig").c;
|
||||
const Window = @import("Window.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else void;
|
||||
const HeaderBarAdw = @import("headerbar_adw.zig");
|
||||
const HeaderBarGtk = @import("headerbar_gtk.zig");
|
||||
|
||||
pub const HeaderBar = union(enum) {
|
||||
adw: *AdwHeaderBar,
|
||||
gtk: *c.GtkHeaderBar,
|
||||
adw: HeaderBarAdw,
|
||||
gtk: HeaderBarGtk,
|
||||
|
||||
pub fn init(window: *Window) HeaderBar {
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and
|
||||
adwaita.enabled(&window.app.config))
|
||||
{
|
||||
return initAdw();
|
||||
pub fn init(self: *HeaderBar) void {
|
||||
const window: *Window = @fieldParentPtr("headerbar", self);
|
||||
if ((comptime adwaita.versionAtLeast(1, 4, 0)) and adwaita.enabled(&window.app.config)) {
|
||||
HeaderBarAdw.init(self);
|
||||
} else {
|
||||
HeaderBarGtk.init(self);
|
||||
}
|
||||
|
||||
return initGtk();
|
||||
}
|
||||
|
||||
fn initAdw() HeaderBar {
|
||||
const headerbar = c.adw_header_bar_new();
|
||||
return .{ .adw = @ptrCast(headerbar) };
|
||||
}
|
||||
|
||||
fn initGtk() HeaderBar {
|
||||
const headerbar = c.gtk_header_bar_new();
|
||||
return .{ .gtk = @ptrCast(headerbar) };
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBar, visible: bool) void {
|
||||
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
|
||||
switch (self) {
|
||||
inline else => |v| v.setVisible(visible),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn asWidget(self: HeaderBar) *c.GtkWidget {
|
||||
return switch (self) {
|
||||
.adw => |headerbar| @ptrCast(@alignCast(headerbar)),
|
||||
.gtk => |headerbar| @ptrCast(@alignCast(headerbar)),
|
||||
inline else => |v| v.asWidget(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn packEnd(self: HeaderBar, widget: *c.GtkWidget) void {
|
||||
switch (self) {
|
||||
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
);
|
||||
},
|
||||
.gtk => |headerbar| c.gtk_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
),
|
||||
inline else => |v| v.packEnd(widget),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packStart(self: HeaderBar, widget: *c.GtkWidget) void {
|
||||
switch (self) {
|
||||
.adw => |headerbar| if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
);
|
||||
},
|
||||
.gtk => |headerbar| c.gtk_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(headerbar)),
|
||||
widget,
|
||||
),
|
||||
inline else => |v| v.packStart(widget),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBar, title: [:0]const u8) void {
|
||||
switch (self) {
|
||||
inline else => |v| v.setTitle(title),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setSubtitle(self: HeaderBar, subtitle: [:0]const u8) void {
|
||||
switch (self) {
|
||||
inline else => |v| v.setSubtitle(subtitle),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
78
src/apprt/gtk/headerbar_adw.zig
Normal file
78
src/apprt/gtk/headerbar_adw.zig
Normal file
@ -0,0 +1,78 @@
|
||||
const HeaderBarAdw = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
|
||||
const AdwHeaderBar = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwHeaderBar else anyopaque;
|
||||
const AdwWindowTitle = if (adwaita.versionAtLeast(0, 0, 0)) c.AdwWindowTitle else anyopaque;
|
||||
|
||||
/// the window that this headerbar is attached to
|
||||
window: *Window,
|
||||
/// the Adwaita headerbar widget
|
||||
headerbar: *AdwHeaderBar,
|
||||
/// the Adwaita window title widget
|
||||
title: *AdwWindowTitle,
|
||||
|
||||
pub fn init(headerbar: *HeaderBar) void {
|
||||
if (!adwaita.versionAtLeast(0, 0, 0)) return;
|
||||
|
||||
const window: *Window = @fieldParentPtr("headerbar", headerbar);
|
||||
headerbar.* = .{
|
||||
.adw = .{
|
||||
.window = window,
|
||||
.headerbar = @ptrCast(@alignCast(c.adw_header_bar_new())),
|
||||
.title = @ptrCast(@alignCast(c.adw_window_title_new(
|
||||
c.gtk_window_get_title(window.window) orelse "Ghostty",
|
||||
null,
|
||||
))),
|
||||
},
|
||||
};
|
||||
c.adw_header_bar_set_title_widget(
|
||||
headerbar.adw.headerbar,
|
||||
@ptrCast(@alignCast(headerbar.adw.title)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBarAdw, visible: bool) void {
|
||||
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
|
||||
}
|
||||
|
||||
pub fn asWidget(self: HeaderBarAdw) *c.GtkWidget {
|
||||
return @ptrCast(@alignCast(self.headerbar));
|
||||
}
|
||||
|
||||
pub fn packEnd(self: HeaderBarAdw, widget: *c.GtkWidget) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packStart(self: HeaderBarAdw, widget: *c.GtkWidget) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBarAdw, title: [:0]const u8) void {
|
||||
c.gtk_window_set_title(self.window.window, title);
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_window_title_set_title(self.title, title);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setSubtitle(self: HeaderBarAdw, subtitle: [:0]const u8) void {
|
||||
if (comptime adwaita.versionAtLeast(0, 0, 0)) {
|
||||
c.adw_window_title_set_subtitle(self.title, subtitle);
|
||||
}
|
||||
}
|
52
src/apprt/gtk/headerbar_gtk.zig
Normal file
52
src/apprt/gtk/headerbar_gtk.zig
Normal file
@ -0,0 +1,52 @@
|
||||
const HeaderBarGtk = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
const Window = @import("Window.zig");
|
||||
const adwaita = @import("adwaita.zig");
|
||||
|
||||
const HeaderBar = @import("headerbar.zig").HeaderBar;
|
||||
|
||||
/// the window that this headarbar is attached to
|
||||
window: *Window,
|
||||
/// the GTK headerbar widget
|
||||
headerbar: *c.GtkHeaderBar,
|
||||
|
||||
pub fn init(headerbar: *HeaderBar) void {
|
||||
const window: *Window = @fieldParentPtr("headerbar", headerbar);
|
||||
headerbar.* = .{
|
||||
.gtk = .{
|
||||
.window = window,
|
||||
.headerbar = @ptrCast(c.gtk_header_bar_new()),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn setVisible(self: HeaderBarGtk, visible: bool) void {
|
||||
c.gtk_widget_set_visible(self.asWidget(), @intFromBool(visible));
|
||||
}
|
||||
|
||||
pub fn asWidget(self: HeaderBarGtk) *c.GtkWidget {
|
||||
return @ptrCast(@alignCast(self.headerbar));
|
||||
}
|
||||
|
||||
pub fn packEnd(self: HeaderBarGtk, widget: *c.GtkWidget) void {
|
||||
c.gtk_header_bar_pack_end(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn packStart(self: HeaderBarGtk, widget: *c.GtkWidget) void {
|
||||
c.gtk_header_bar_pack_start(
|
||||
@ptrCast(@alignCast(self.headerbar)),
|
||||
widget,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn setTitle(self: HeaderBarGtk, title: [:0]const u8) void {
|
||||
c.gtk_window_set_title(self.window.window, title);
|
||||
}
|
||||
|
||||
pub fn setSubtitle(_: HeaderBarGtk, _: [:0]const u8) void {}
|
@ -143,6 +143,7 @@ const Window = struct {
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "window");
|
||||
c.gtk_widget_add_css_class(@ptrCast(@alignCast(gtk_window)), "inspector-window");
|
||||
|
||||
// Initialize our imgui widget
|
||||
|
@ -2,7 +2,7 @@ const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const input = @import("../../input.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const x11 = @import("x11.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
|
||||
/// Returns a GTK accelerator string from a trigger.
|
||||
pub fn accelFromTrigger(buf: []u8, trigger: input.Binding.Trigger) !?[:0]const u8 {
|
||||
@ -105,34 +105,14 @@ pub fn keyvalUnicodeUnshifted(
|
||||
/// This requires a lot of context because the GdkEvent
|
||||
/// doesn't contain enough on its own.
|
||||
pub fn eventMods(
|
||||
widget: *c.GtkWidget,
|
||||
event: *c.GdkEvent,
|
||||
physical_key: input.Key,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
x11_xkb: ?*x11.Xkb,
|
||||
app_winproto: *winproto.App,
|
||||
) input.Mods {
|
||||
const device = c.gdk_event_get_device(event);
|
||||
|
||||
var mods = mods: {
|
||||
// Add any modifier state events from Xkb if we have them (X11
|
||||
// only). Null back from the Xkb call means there was no modifier
|
||||
// event to read. This likely means that the key event did not
|
||||
// result in a modifier change and we can safely rely on the GDK
|
||||
// state.
|
||||
if (comptime build_options.x11) {
|
||||
const display = c.gtk_widget_get_display(widget);
|
||||
if (x11_xkb) |xkb| {
|
||||
if (xkb.modifier_state_from_notify(display)) |x11_mods| break :mods x11_mods;
|
||||
break :mods translateMods(gtk_mods);
|
||||
}
|
||||
}
|
||||
|
||||
// On Wayland, we have to use the GDK device because the mods sent
|
||||
// to this event do not have the modifier key applied if it was
|
||||
// pressed (i.e. left control).
|
||||
break :mods translateMods(c.gdk_device_get_modifier_state(device));
|
||||
};
|
||||
|
||||
var mods = app_winproto.eventMods(device, gtk_mods);
|
||||
mods.num_lock = c.gdk_device_get_num_lock_state(device) == 1;
|
||||
|
||||
switch (physical_key) {
|
||||
|
@ -17,6 +17,14 @@ pub const NotebookAdw = struct {
|
||||
/// the tab view
|
||||
tab_view: *AdwTabView,
|
||||
|
||||
/// 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(notebook: *Notebook) void {
|
||||
const window: *Window = @fieldParentPtr("notebook", notebook);
|
||||
const app = window.app;
|
||||
@ -38,6 +46,7 @@ pub const NotebookAdw = struct {
|
||||
};
|
||||
|
||||
_ = c.g_signal_connect_data(tab_view, "page-attached", c.G_CALLBACK(&adwPageAttached), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "close-page", c.G_CALLBACK(&adwClosePage), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "create-window", c.G_CALLBACK(&adwTabViewCreateWindow), window, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(tab_view, "notify::selected-page", c.G_CALLBACK(&adwSelectPage), window, null, c.G_CONNECT_DEFAULT);
|
||||
}
|
||||
@ -112,11 +121,24 @@ pub const NotebookAdw = struct {
|
||||
pub fn closeTab(self: *NotebookAdw, tab: *Tab) void {
|
||||
if (comptime !adwaita.versionAtLeast(0, 0, 0)) unreachable;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
const page = c.adw_tab_view_get_page(self.tab_view, @ptrCast(tab.box)) orelse return;
|
||||
c.adw_tab_view_close_page(self.tab_view, page);
|
||||
|
||||
// If we have no more tabs we close the window
|
||||
if (self.nPages() == 0) {
|
||||
const window = tab.window.window;
|
||||
|
||||
// libadw versions <= 1.3.x leak the final page view
|
||||
// which causes our surface to not properly cleanup. We
|
||||
// unref to force the cleanup. This will trigger a critical
|
||||
@ -128,7 +150,9 @@ pub const NotebookAdw = struct {
|
||||
c.g_object_unref(tab.box);
|
||||
}
|
||||
|
||||
c.gtk_window_destroy(tab.window.window);
|
||||
// `self` will become invalid after this call because it will have
|
||||
// been freed up as part of the process of closing the window.
|
||||
c.gtk_window_destroy(window);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -143,6 +167,28 @@ fn adwPageAttached(_: *AdwTabView, page: *c.AdwTabPage, _: c_int, ud: ?*anyopaqu
|
||||
window.focusCurrentTab();
|
||||
}
|
||||
|
||||
fn adwClosePage(
|
||||
_: *AdwTabView,
|
||||
page: *c.AdwTabPage,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) c.gboolean {
|
||||
const child = c.adw_tab_page_get_child(page);
|
||||
const tab: *Tab = @ptrCast(@alignCast(c.g_object_get_data(
|
||||
@ptrCast(child),
|
||||
Tab.GHOSTTY_TAB,
|
||||
) orelse return 0));
|
||||
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const notebook = window.notebook.adw;
|
||||
c.adw_tab_view_close_page_finish(
|
||||
notebook.tab_view,
|
||||
page,
|
||||
@intFromBool(notebook.forcing_close),
|
||||
);
|
||||
if (!notebook.forcing_close) tab.closeWithConfirmation();
|
||||
return 1;
|
||||
}
|
||||
|
||||
fn adwTabViewCreateWindow(
|
||||
_: *AdwTabView,
|
||||
ud: ?*anyopaque,
|
||||
@ -159,5 +205,5 @@ fn adwSelectPage(_: *c.GObject, _: *c.GParamSpec, ud: ?*anyopaque) void {
|
||||
const window: *Window = @ptrCast(@alignCast(ud.?));
|
||||
const page = c.adw_tab_view_get_selected_page(window.notebook.adw.tab_view) orelse return;
|
||||
const title = c.adw_tab_page_get_title(page);
|
||||
c.gtk_window_set_title(window.window, title);
|
||||
window.setTitle(std.mem.span(title));
|
||||
}
|
||||
|
@ -157,8 +157,8 @@ pub const NotebookGtk = struct {
|
||||
c.gtk_gesture_single_set_button(@ptrCast(gesture_tab_click), 0);
|
||||
c.gtk_widget_add_controller(label_box_widget, @ptrCast(gesture_tab_click));
|
||||
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(&Tab.gtkTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(&Tab.gtkTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(label_close, "clicked", c.G_CALLBACK(>kTabCloseClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
_ = c.g_signal_connect_data(gesture_tab_click, "pressed", c.G_CALLBACK(>kTabClick), tab, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Tab settings
|
||||
c.gtk_notebook_set_tab_reorderable(self.notebook, box_widget, 1);
|
||||
@ -259,7 +259,7 @@ fn gtkSwitchPage(_: *c.GtkNotebook, page: *c.GtkWidget, _: usize, ud: ?*anyopaqu
|
||||
const gtk_label_box = @as(*c.GtkWidget, @ptrCast(c.gtk_notebook_get_tab_label(self.notebook, page)));
|
||||
const gtk_label = @as(*c.GtkLabel, @ptrCast(c.gtk_widget_get_first_child(gtk_label_box)));
|
||||
const label_text = c.gtk_label_get_text(gtk_label);
|
||||
c.gtk_window_set_title(window.window, label_text);
|
||||
window.setTitle(std.mem.span(label_text));
|
||||
}
|
||||
|
||||
fn gtkNotebookCreateWindow(
|
||||
@ -283,3 +283,22 @@ fn gtkNotebookCreateWindow(
|
||||
|
||||
return newWindow.notebook.gtk.notebook;
|
||||
}
|
||||
|
||||
fn gtkTabCloseClick(_: *c.GtkButton, ud: ?*anyopaque) callconv(.C) void {
|
||||
const tab: *Tab = @ptrCast(@alignCast(ud));
|
||||
tab.closeWithConfirmation();
|
||||
}
|
||||
|
||||
fn gtkTabClick(
|
||||
gesture: *c.GtkGestureClick,
|
||||
_: c.gint,
|
||||
_: c.gdouble,
|
||||
_: c.gdouble,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Tab = @ptrCast(@alignCast(ud));
|
||||
const gtk_button = c.gtk_gesture_single_get_current_button(@ptrCast(gesture));
|
||||
if (gtk_button == c.GDK_BUTTON_MIDDLE) {
|
||||
self.closeWithConfirmation();
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,11 @@ label.size-overlay.hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
window.without-window-decoration-and-with-titlebar {
|
||||
window.ssd.no-border-radius {
|
||||
/* Without clearing the border radius, at least on Mutter with
|
||||
* gtk-titlebar=true and gtk-adwaita=false, there is some window artifacting
|
||||
* that this will mitigate.
|
||||
*/
|
||||
border-radius: 0 0;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,11 @@ const c = @import("c.zig").c;
|
||||
/// 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(
|
||||
@ -26,6 +31,20 @@ pub inline fn atLeast(
|
||||
// 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.
|
||||
@ -44,15 +63,18 @@ test "atLeast" {
|
||||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
|
||||
try testing.expect(atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
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(!atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(!atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!atLeast(c.GTK_MAJOR_VERSION + 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(!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(atLeast(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(atLeast(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION + 1, c.GTK_MICRO_VERSION));
|
||||
try testing.expect(atLeast(c.GTK_MAJOR_VERSION - 1, c.GTK_MINOR_VERSION, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(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(atLeast(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
try testing.expect(fun(c.GTK_MAJOR_VERSION, c.GTK_MINOR_VERSION - 1, c.GTK_MICRO_VERSION + 1));
|
||||
}
|
||||
}
|
||||
|
134
src/apprt/gtk/winproto.zig
Normal file
134
src/apprt/gtk/winproto.zig
Normal file
@ -0,0 +1,134 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const Config = @import("../../config.zig").Config;
|
||||
const input = @import("../../input.zig");
|
||||
const key = @import("key.zig");
|
||||
|
||||
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: *c.GdkDisplay,
|
||||
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: ?*c.GdkDevice,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) input.Mods {
|
||||
return switch (self.*) {
|
||||
inline else => |*v| v.eventMods(device, gtk_mods),
|
||||
} orelse key.translateMods(gtk_mods);
|
||||
}
|
||||
};
|
||||
|
||||
/// 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,
|
||||
window: *c.GtkWindow,
|
||||
config: *const Config,
|
||||
) !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,
|
||||
window,
|
||||
config,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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 updateConfigEvent(
|
||||
self: *Window,
|
||||
config: *const Config,
|
||||
) !void {
|
||||
switch (self.*) {
|
||||
inline else => |*v| try v.updateConfigEvent(config),
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
};
|
||||
}
|
||||
};
|
64
src/apprt/gtk/winproto/noop.zig
Normal file
64
src/apprt/gtk/winproto/noop.zig
Normal file
@ -0,0 +1,64 @@
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("../c.zig").c;
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.winproto_noop);
|
||||
|
||||
pub const App = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *c.GdkDisplay,
|
||||
_: [:0]const u8,
|
||||
_: *const Config,
|
||||
) !?App {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn eventMods(
|
||||
_: *App,
|
||||
_: ?*c.GdkDevice,
|
||||
_: c.GdkModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Window = struct {
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
_: *App,
|
||||
_: *c.GtkWindow,
|
||||
_: *const Config,
|
||||
) !Window {
|
||||
return .{};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
_: *Window,
|
||||
_: *const Config,
|
||||
) !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;
|
||||
}
|
||||
};
|
302
src/apprt/gtk/winproto/wayland.zig
Normal file
302
src/apprt/gtk/winproto/wayland.zig
Normal file
@ -0,0 +1,302 @@
|
||||
//! Wayland protocol implementation for the Ghostty GTK apprt.
|
||||
const std = @import("std");
|
||||
const wayland = @import("wayland");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("../c.zig").c;
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const input = @import("../../../input.zig");
|
||||
|
||||
const wl = wayland.client.wl;
|
||||
const org = wayland.client.org;
|
||||
|
||||
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,
|
||||
|
||||
default_deco_mode: ?org.KdeKwinServerDecorationManager.Mode = null,
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *c.GdkDisplay,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = config;
|
||||
_ = app_id;
|
||||
|
||||
// Check if we're actually on Wayland
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(gdk_display)),
|
||||
c.gdk_wayland_display_get_type(),
|
||||
) == 0) return null;
|
||||
|
||||
const display: *wl.Display = @ptrCast(c.gdk_wayland_display_get_wl_display(
|
||||
gdk_display,
|
||||
) 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;
|
||||
|
||||
if (context.kde_decoration_manager != null) {
|
||||
// FIXME: Roundtrip again because we have to wait for the decoration
|
||||
// manager to respond with the preferred default mode. Ew.
|
||||
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,
|
||||
_: ?*c.GdkDevice,
|
||||
_: c.GdkModifierType,
|
||||
) ?input.Mods {
|
||||
return null;
|
||||
}
|
||||
|
||||
fn registryListener(
|
||||
registry: *wl.Registry,
|
||||
event: wl.Registry.Event,
|
||||
context: *Context,
|
||||
) void {
|
||||
switch (event) {
|
||||
// https://wayland.app/protocols/wayland#wl_registry:event:global
|
||||
.global => |global| {
|
||||
log.debug("wl_registry.global: interface={s}", .{global.interface});
|
||||
|
||||
if (registryBind(
|
||||
org.KdeKwinBlurManager,
|
||||
registry,
|
||||
global,
|
||||
)) |blur_manager| {
|
||||
context.kde_blur_manager = blur_manager;
|
||||
} else if (registryBind(
|
||||
org.KdeKwinServerDecorationManager,
|
||||
registry,
|
||||
global,
|
||||
)) |deco_manager| {
|
||||
context.kde_decoration_manager = deco_manager;
|
||||
deco_manager.setListener(*Context, decoManagerListener, context);
|
||||
}
|
||||
},
|
||||
|
||||
// We don't handle removal events
|
||||
.global_remove => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Bind a Wayland interface to a global object. Returns non-null
|
||||
/// if the binding was successful, otherwise null.
|
||||
///
|
||||
/// The type T is the Wayland interface type that we're requesting.
|
||||
/// This function will verify that the global object is the correct
|
||||
/// interface and version before binding.
|
||||
fn registryBind(
|
||||
comptime T: type,
|
||||
registry: *wl.Registry,
|
||||
global: anytype,
|
||||
) ?*T {
|
||||
if (std.mem.orderZ(
|
||||
u8,
|
||||
global.interface,
|
||||
T.interface.name,
|
||||
) != .eq) return null;
|
||||
|
||||
return registry.bind(global.name, T, T.generated_version) catch |err| {
|
||||
log.warn("error binding interface {s} error={}", .{
|
||||
global.interface,
|
||||
err,
|
||||
});
|
||||
return 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 {
|
||||
config: DerivedConfig,
|
||||
|
||||
/// 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,
|
||||
|
||||
/// Object that controls the decoration mode (client/server/auto)
|
||||
/// of the window.
|
||||
decoration: ?*org.KdeKwinServerDecoration,
|
||||
|
||||
const DerivedConfig = struct {
|
||||
blur: bool,
|
||||
window_decoration: Config.WindowDecoration,
|
||||
|
||||
pub fn init(config: *const Config) DerivedConfig {
|
||||
return .{
|
||||
.blur = config.@"background-blur".enabled(),
|
||||
.window_decoration = config.@"window-decoration",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
app: *App,
|
||||
gtk_window: *c.GtkWindow,
|
||||
config: *const Config,
|
||||
) !Window {
|
||||
_ = alloc;
|
||||
|
||||
const gdk_surface = c.gtk_native_get_surface(
|
||||
@ptrCast(gtk_window),
|
||||
) 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.
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(gdk_surface)),
|
||||
c.gdk_wayland_surface_get_type(),
|
||||
) == 0) return error.NotWaylandSurface;
|
||||
|
||||
const wl_surface: *wl.Surface = @ptrCast(c.gdk_wayland_surface_get_wl_surface(
|
||||
gdk_surface,
|
||||
) 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;
|
||||
};
|
||||
|
||||
return .{
|
||||
.config = DerivedConfig.init(config),
|
||||
.surface = wl_surface,
|
||||
.app_context = app.context,
|
||||
.blur_token = null,
|
||||
.decoration = deco,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = alloc;
|
||||
if (self.blur_token) |blur| blur.release();
|
||||
if (self.decoration) |deco| deco.release();
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
self: *Window,
|
||||
config: *const Config,
|
||||
) !void {
|
||||
self.config = DerivedConfig.init(config);
|
||||
}
|
||||
|
||||
pub fn resizeEvent(_: *Window) !void {}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
try self.syncBlur();
|
||||
try self.syncDecoration();
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
// Compositor doesn't support the SSD protocol
|
||||
if (self.decoration == null) return true;
|
||||
|
||||
return switch (self.getDecorationMode()) {
|
||||
.Client => true,
|
||||
.Server, .None => false,
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
/// Update the blur state of the window.
|
||||
fn syncBlur(self: *Window) !void {
|
||||
const manager = self.app_context.kde_blur_manager orelse return;
|
||||
const blur = self.config.blur;
|
||||
|
||||
if (self.blur_token) |tok| {
|
||||
// Only release token when transitioning from blurred -> not blurred
|
||||
if (!blur) {
|
||||
manager.unset(self.surface);
|
||||
tok.release();
|
||||
self.blur_token = null;
|
||||
}
|
||||
} else {
|
||||
// Only acquire token when transitioning from not blurred -> blurred
|
||||
if (blur) {
|
||||
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.config.window_decoration) {
|
||||
.auto => self.app_context.default_deco_mode orelse .Client,
|
||||
.client => .Client,
|
||||
.server => .Server,
|
||||
.none => .None,
|
||||
};
|
||||
}
|
||||
};
|
302
src/apprt/gtk/winproto/x11.zig
Normal file
302
src/apprt/gtk/winproto/x11.zig
Normal file
@ -0,0 +1,302 @@
|
||||
//! 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 c = @import("../c.zig").c;
|
||||
const input = @import("../../../input.zig");
|
||||
const Config = @import("../../../config.zig").Config;
|
||||
const adwaita = @import("../adwaita.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
pub const App = struct {
|
||||
display: *c.Display,
|
||||
base_event_code: c_int,
|
||||
kde_blur_atom: c.Atom,
|
||||
|
||||
pub fn init(
|
||||
alloc: Allocator,
|
||||
gdk_display: *c.GdkDisplay,
|
||||
app_id: [:0]const u8,
|
||||
config: *const Config,
|
||||
) !?App {
|
||||
_ = alloc;
|
||||
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(gdk_display)),
|
||||
c.gdk_x11_display_get_type(),
|
||||
) == 0) return null;
|
||||
|
||||
// Get our X11 display
|
||||
const display: *c.Display = c.gdk_x11_display_get_xdisplay(
|
||||
gdk_display,
|
||||
) orelse return error.NoX11Display;
|
||||
|
||||
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.
|
||||
c.g_set_prgname(x11_program_name);
|
||||
c.gdk_x11_display_set_program_class(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(
|
||||
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(
|
||||
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 = display,
|
||||
.base_event_code = base_event_code,
|
||||
.kde_blur_atom = c.gdk_x11_get_xatom_by_name_for_display(
|
||||
gdk_display,
|
||||
"_KDE_NET_WM_BLUR_BEHIND_REGION",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
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: ?*c.GdkDevice,
|
||||
gtk_mods: c.GdkModifierType,
|
||||
) ?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(self.display, c.QueuedAfterReading) == 0) return null;
|
||||
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = c.XPeekEvent(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 const Window = struct {
|
||||
app: *App,
|
||||
config: DerivedConfig,
|
||||
window: c.Window,
|
||||
gtk_window: *c.GtkWindow,
|
||||
blur_region: Region,
|
||||
|
||||
const DerivedConfig = struct {
|
||||
blur: bool,
|
||||
has_decoration: bool,
|
||||
|
||||
pub fn init(config: *const Config) DerivedConfig {
|
||||
return .{
|
||||
.blur = config.@"background-blur".enabled(),
|
||||
.has_decoration = switch (config.@"window-decoration") {
|
||||
.none => false,
|
||||
.auto, .client, .server => true,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(
|
||||
_: Allocator,
|
||||
app: *App,
|
||||
gtk_window: *c.GtkWindow,
|
||||
config: *const Config,
|
||||
) !Window {
|
||||
const surface = c.gtk_native_get_surface(
|
||||
@ptrCast(gtk_window),
|
||||
) orelse return error.NotX11Surface;
|
||||
|
||||
// Check if we're actually on X11
|
||||
if (c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(surface)),
|
||||
c.gdk_x11_surface_get_type(),
|
||||
) == 0) return error.NotX11Surface;
|
||||
|
||||
const blur_region: Region = blur: {
|
||||
if ((comptime !adwaita.versionAtLeast(0, 0, 0)) or
|
||||
!adwaita.enabled(config)) break :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;
|
||||
c.gtk_native_get_surface_transform(
|
||||
@ptrCast(gtk_window),
|
||||
&x,
|
||||
&y,
|
||||
);
|
||||
|
||||
break :blur .{
|
||||
.x = @intFromFloat(x),
|
||||
.y = @intFromFloat(y),
|
||||
};
|
||||
};
|
||||
|
||||
return .{
|
||||
.app = app,
|
||||
.config = DerivedConfig.init(config),
|
||||
.window = c.gdk_x11_surface_get_xid(surface),
|
||||
.gtk_window = gtk_window,
|
||||
.blur_region = blur_region,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Window, alloc: Allocator) void {
|
||||
_ = self;
|
||||
_ = alloc;
|
||||
}
|
||||
|
||||
pub fn updateConfigEvent(
|
||||
self: *Window,
|
||||
config: *const Config,
|
||||
) !void {
|
||||
self.config = DerivedConfig.init(config);
|
||||
}
|
||||
|
||||
pub fn resizeEvent(self: *Window) !void {
|
||||
// The blur region must update with window resizes
|
||||
self.blur_region.width = c.gtk_widget_get_width(@ptrCast(self.gtk_window));
|
||||
self.blur_region.height = c.gtk_widget_get_height(@ptrCast(self.gtk_window));
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn syncAppearance(self: *Window) !void {
|
||||
try self.syncBlur();
|
||||
}
|
||||
|
||||
pub fn clientSideDecorationEnabled(self: Window) bool {
|
||||
return self.config.has_decoration;
|
||||
}
|
||||
|
||||
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 blur = self.config.blur;
|
||||
log.debug("set blur={}, window xid={}, region={}", .{
|
||||
blur,
|
||||
self.window,
|
||||
self.blur_region,
|
||||
});
|
||||
|
||||
if (blur) {
|
||||
_ = c.XChangeProperty(
|
||||
self.app.display,
|
||||
self.window,
|
||||
self.app.kde_blur_atom,
|
||||
c.XA_CARDINAL,
|
||||
// Despite what you might think, the "32" here does NOT mean
|
||||
// that the data should be in u32s. Instead, they should be
|
||||
// c_longs, which on any 64-bit architecture would be obviously
|
||||
// 64 bits. WTF?!
|
||||
32,
|
||||
c.PropModeReplace,
|
||||
// SAFETY: Region is an extern struct that has the same
|
||||
// representation of 4 c_longs put next to each other.
|
||||
// Therefore, reinterpretation should be safe.
|
||||
// We don't have to care about endianness either since
|
||||
// Xlib converts it to network byte order for us.
|
||||
@ptrCast(std.mem.asBytes(&self.blur_region)),
|
||||
4,
|
||||
);
|
||||
} else {
|
||||
_ = c.XDeleteProperty(
|
||||
self.app.display,
|
||||
self.window,
|
||||
self.app.kde_blur_atom,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Region = extern struct {
|
||||
x: c_long = 0,
|
||||
y: c_long = 0,
|
||||
width: c_long = 0,
|
||||
height: c_long = 0,
|
||||
};
|
@ -1,119 +0,0 @@
|
||||
/// Utility functions for X11 handling.
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const c = @import("c.zig").c;
|
||||
const input = @import("../../input.zig");
|
||||
|
||||
const log = std.log.scoped(.gtk_x11);
|
||||
|
||||
/// Returns true if the passed in display is an X11 display.
|
||||
pub fn is_display(display: ?*c.GdkDisplay) bool {
|
||||
if (comptime !build_options.x11) return false;
|
||||
return c.g_type_check_instance_is_a(
|
||||
@ptrCast(@alignCast(display orelse return false)),
|
||||
c.gdk_x11_display_get_type(),
|
||||
) != 0;
|
||||
}
|
||||
|
||||
/// Returns true if the app is running on X11
|
||||
pub fn is_current_display_server() bool {
|
||||
if (comptime !build_options.x11) return false;
|
||||
const display = c.gdk_display_get_default();
|
||||
return is_display(display);
|
||||
}
|
||||
|
||||
pub const Xkb = struct {
|
||||
base_event_code: c_int,
|
||||
|
||||
/// Initialize an Xkb struct for the given GDK display. If the display isn't
|
||||
/// backed by X then this will return null.
|
||||
pub fn init(display_: ?*c.GdkDisplay) !?Xkb {
|
||||
if (comptime !build_options.x11) return null;
|
||||
|
||||
// Display should never be null but we just treat that as a non-X11
|
||||
// display so that the caller can just ignore it and not unwrap it.
|
||||
const display = display_ orelse return null;
|
||||
|
||||
// If the display isn't X11, then we don't need to do anything.
|
||||
if (!is_display(display)) return null;
|
||||
|
||||
log.debug("Xkb.init: initializing Xkb", .{});
|
||||
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
|
||||
var result: Xkb = .{
|
||||
.base_event_code = 0,
|
||||
};
|
||||
|
||||
log.debug("Xkb.init: running XkbQueryExtension", .{});
|
||||
var opcode: c_int = 0;
|
||||
var base_error_code: c_int = 0;
|
||||
var major = c.XkbMajorVersion;
|
||||
var minor = c.XkbMinorVersion;
|
||||
if (c.XkbQueryExtension(
|
||||
xdisplay,
|
||||
&opcode,
|
||||
&result.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(
|
||||
xdisplay,
|
||||
c.XkbUseCoreKbd,
|
||||
c.XkbStateNotify,
|
||||
c.XkbModifierStateMask,
|
||||
c.XkbModifierStateMask,
|
||||
) == 0) {
|
||||
log.err("Fatal: error initializing Xkb extension: error executing XkbSelectEventDetails", .{});
|
||||
return error.XkbInitializationError;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 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 modifier_state_from_notify(self: Xkb, display_: ?*c.GdkDisplay) ?input.Mods {
|
||||
if (comptime !build_options.x11) return null;
|
||||
|
||||
const display = display_ orelse return null;
|
||||
|
||||
// Shoutout to Mozilla for figuring out a clean way to do this, this is
|
||||
// paraphrased from Firefox/Gecko in widget/gtk/nsGtkKeyUtils.cpp.
|
||||
const xdisplay = c.gdk_x11_display_get_xdisplay(display);
|
||||
if (c.XEventsQueued(xdisplay, c.QueuedAfterReading) == 0) return null;
|
||||
|
||||
var nextEvent: c.XEvent = undefined;
|
||||
_ = c.XPeekEvent(xdisplay, &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;
|
||||
}
|
||||
};
|
527
src/build/Config.zig
Normal file
527
src/build/Config.zig
Normal file
@ -0,0 +1,527 @@
|
||||
/// Build configuration. This is the configuration that is populated
|
||||
/// during `zig build` to control the rest of the build process.
|
||||
const Config = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const apprt = @import("../apprt.zig");
|
||||
const font = @import("../font/main.zig");
|
||||
const renderer = @import("../renderer.zig");
|
||||
const Command = @import("../Command.zig");
|
||||
const WasmTarget = @import("../os/wasm/target.zig").Target;
|
||||
|
||||
const gtk = @import("gtk.zig");
|
||||
const GitVersion = @import("GitVersion.zig");
|
||||
|
||||
/// The version of the next release.
|
||||
///
|
||||
/// TODO: When Zig 0.14 is released, derive this from build.zig.zon directly.
|
||||
/// Until then this MUST match build.zig.zon and should always be the
|
||||
/// _next_ version to release.
|
||||
const app_version: std.SemanticVersion = .{ .major = 1, .minor = 0, .patch = 2 };
|
||||
|
||||
/// Standard build configuration options.
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
target: std.Build.ResolvedTarget,
|
||||
wasm_target: WasmTarget,
|
||||
|
||||
/// Comptime interfaces
|
||||
app_runtime: apprt.Runtime = .none,
|
||||
renderer: renderer.Impl = .opengl,
|
||||
font_backend: font.Backend = .freetype,
|
||||
|
||||
/// Feature flags
|
||||
adwaita: bool = false,
|
||||
x11: bool = false,
|
||||
wayland: bool = false,
|
||||
sentry: bool = true,
|
||||
wasm_shared: bool = true,
|
||||
|
||||
/// Ghostty exe properties
|
||||
exe_entrypoint: ExeEntrypoint = .ghostty,
|
||||
version: std.SemanticVersion = .{ .major = 0, .minor = 0, .patch = 0 },
|
||||
|
||||
/// Binary properties
|
||||
pie: bool = false,
|
||||
strip: bool = false,
|
||||
patch_rpath: ?[]const u8 = null,
|
||||
|
||||
/// Artifacts
|
||||
flatpak: bool = false,
|
||||
emit_test_exe: bool = false,
|
||||
emit_bench: bool = false,
|
||||
emit_helpgen: bool = false,
|
||||
emit_docs: bool = false,
|
||||
emit_webdata: bool = false,
|
||||
emit_xcframework: bool = false,
|
||||
emit_terminfo: bool = false,
|
||||
emit_termcap: bool = false,
|
||||
|
||||
/// Environmental properties
|
||||
env: std.process.EnvMap,
|
||||
|
||||
pub fn init(b: *std.Build) !Config {
|
||||
// Setup our standard Zig target and optimize options, i.e.
|
||||
// `-Doptimize` and `-Dtarget`.
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const target = target: {
|
||||
var result = b.standardTargetOptions(.{});
|
||||
|
||||
// If we're building for macOS and we're on macOS, we need to
|
||||
// use a generic target to workaround compilation issues.
|
||||
if (result.result.os.tag == .macos and builtin.target.isDarwin()) {
|
||||
result = genericMacOSTarget(b, null);
|
||||
}
|
||||
|
||||
// If we have no minimum OS version, we set the default based on
|
||||
// our tag. Not all tags have a minimum so this may be null.
|
||||
if (result.query.os_version_min == null) {
|
||||
result.query.os_version_min = osVersionMin(result.result.os.tag);
|
||||
}
|
||||
|
||||
break :target result;
|
||||
};
|
||||
|
||||
// This is set to true when we're building a system package. For now
|
||||
// this is trivially detected using the "system_package_mode" bool
|
||||
// but we may want to make this more sophisticated in the future.
|
||||
const system_package: bool = b.graph.system_package_mode;
|
||||
|
||||
// This specifies our target wasm runtime. For now only one semi-usable
|
||||
// one exists so this is hardcoded.
|
||||
const wasm_target: WasmTarget = .browser;
|
||||
|
||||
// Determine whether GTK supports X11 and Wayland. This is always safe
|
||||
// to run even on non-Linux platforms because any failures result in
|
||||
// defaults.
|
||||
const gtk_targets = gtk.targets(b);
|
||||
|
||||
// We use env vars throughout the build so we grab them immediately here.
|
||||
var env = try std.process.getEnvMap(b.allocator);
|
||||
errdefer env.deinit();
|
||||
|
||||
var config: Config = .{
|
||||
.optimize = optimize,
|
||||
.target = target,
|
||||
.wasm_target = wasm_target,
|
||||
.env = env,
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Comptime Interfaces
|
||||
|
||||
config.font_backend = b.option(
|
||||
font.Backend,
|
||||
"font-backend",
|
||||
"The font backend to use for discovery and rasterization.",
|
||||
) orelse font.Backend.default(target.result, wasm_target);
|
||||
|
||||
config.app_runtime = b.option(
|
||||
apprt.Runtime,
|
||||
"app-runtime",
|
||||
"The app runtime to use. Not all values supported on all platforms.",
|
||||
) orelse apprt.Runtime.default(target.result);
|
||||
|
||||
config.renderer = b.option(
|
||||
renderer.Impl,
|
||||
"renderer",
|
||||
"The app runtime to use. Not all values supported on all platforms.",
|
||||
) orelse renderer.Impl.default(target.result, wasm_target);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Feature Flags
|
||||
|
||||
config.adwaita = b.option(
|
||||
bool,
|
||||
"gtk-adwaita",
|
||||
"Enables the use of Adwaita when using the GTK rendering backend.",
|
||||
) orelse true;
|
||||
|
||||
config.flatpak = b.option(
|
||||
bool,
|
||||
"flatpak",
|
||||
"Build for Flatpak (integrates with Flatpak APIs). Only has an effect targeting Linux.",
|
||||
) orelse false;
|
||||
|
||||
config.sentry = b.option(
|
||||
bool,
|
||||
"sentry",
|
||||
"Build with Sentry crash reporting. Default for macOS is true, false for any other system.",
|
||||
) orelse sentry: {
|
||||
switch (target.result.os.tag) {
|
||||
.macos, .ios => break :sentry true,
|
||||
|
||||
// Note its false for linux because the crash reports on Linux
|
||||
// don't have much useful information.
|
||||
else => break :sentry false,
|
||||
}
|
||||
};
|
||||
|
||||
config.wayland = b.option(
|
||||
bool,
|
||||
"gtk-wayland",
|
||||
"Enables linking against Wayland libraries when using the GTK rendering backend.",
|
||||
) orelse gtk_targets.wayland;
|
||||
|
||||
config.x11 = b.option(
|
||||
bool,
|
||||
"gtk-x11",
|
||||
"Enables linking against X11 libraries when using the GTK rendering backend.",
|
||||
) orelse gtk_targets.x11;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Ghostty Exe Properties
|
||||
|
||||
const version_string = b.option(
|
||||
[]const u8,
|
||||
"version-string",
|
||||
"A specific version string to use for the build. " ++
|
||||
"If not specified, git will be used. This must be a semantic version.",
|
||||
);
|
||||
|
||||
config.version = if (version_string) |v|
|
||||
// If an explicit version is given, we always use it.
|
||||
try std.SemanticVersion.parse(v)
|
||||
else version: {
|
||||
// If no explicit version is given, we try to detect it from git.
|
||||
const vsn = GitVersion.detect(b) catch |err| switch (err) {
|
||||
// If Git isn't available we just make an unknown dev version.
|
||||
error.GitNotFound,
|
||||
error.GitNotRepository,
|
||||
=> break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
.pre = "dev",
|
||||
.build = "0000000",
|
||||
},
|
||||
|
||||
else => return err,
|
||||
};
|
||||
if (vsn.tag) |tag| {
|
||||
// Tip releases behave just like any other pre-release so we skip.
|
||||
if (!std.mem.eql(u8, tag, "tip")) {
|
||||
const expected = b.fmt("v{d}.{d}.{d}", .{
|
||||
app_version.major,
|
||||
app_version.minor,
|
||||
app_version.patch,
|
||||
});
|
||||
|
||||
if (!std.mem.eql(u8, tag, expected)) {
|
||||
@panic("tagged releases must be in vX.Y.Z format matching build.zig");
|
||||
}
|
||||
|
||||
break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
break :version .{
|
||||
.major = app_version.major,
|
||||
.minor = app_version.minor,
|
||||
.patch = app_version.patch,
|
||||
.pre = vsn.branch,
|
||||
.build = vsn.short_hash,
|
||||
};
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Binary Properties
|
||||
|
||||
// On NixOS, the built binary from `zig build` needs to patch the rpath
|
||||
// into the built binary for it to be portable across the NixOS system
|
||||
// it was built for. We default this to true if we can detect we're in
|
||||
// a Nix shell and have LD_LIBRARY_PATH set.
|
||||
config.patch_rpath = b.option(
|
||||
[]const u8,
|
||||
"patch-rpath",
|
||||
"Inject the LD_LIBRARY_PATH as the rpath in the built binary. " ++
|
||||
"This defaults to LD_LIBRARY_PATH if we're in a Nix shell environment on NixOS.",
|
||||
) orelse patch_rpath: {
|
||||
// We only do the patching if we're targeting our own CPU and its Linux.
|
||||
if (!(target.result.os.tag == .linux) or !target.query.isNativeCpu()) break :patch_rpath null;
|
||||
|
||||
// If we're in a nix shell we default to doing this.
|
||||
// Note: we purposely never deinit envmap because we leak the strings
|
||||
if (env.get("IN_NIX_SHELL") == null) break :patch_rpath null;
|
||||
break :patch_rpath env.get("LD_LIBRARY_PATH");
|
||||
};
|
||||
|
||||
config.pie = b.option(
|
||||
bool,
|
||||
"pie",
|
||||
"Build a Position Independent Executable. Default true for system packages.",
|
||||
) orelse system_package;
|
||||
|
||||
config.strip = b.option(
|
||||
bool,
|
||||
"strip",
|
||||
"Strip the final executable. Default true for fast and small releases",
|
||||
) orelse switch (optimize) {
|
||||
.Debug => false,
|
||||
.ReleaseSafe => false,
|
||||
.ReleaseFast, .ReleaseSmall => true,
|
||||
};
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Artifacts to Emit
|
||||
|
||||
config.emit_test_exe = b.option(
|
||||
bool,
|
||||
"emit-test-exe",
|
||||
"Build and install test executables with 'build'",
|
||||
) orelse false;
|
||||
|
||||
config.emit_bench = b.option(
|
||||
bool,
|
||||
"emit-bench",
|
||||
"Build and install the benchmark executables.",
|
||||
) orelse false;
|
||||
|
||||
config.emit_helpgen = b.option(
|
||||
bool,
|
||||
"emit-helpgen",
|
||||
"Build and install the helpgen executable.",
|
||||
) orelse false;
|
||||
|
||||
config.emit_docs = b.option(
|
||||
bool,
|
||||
"emit-docs",
|
||||
"Build and install auto-generated documentation (requires pandoc)",
|
||||
) orelse emit_docs: {
|
||||
// If we are emitting any other artifacts then we default to false.
|
||||
if (config.emit_bench or
|
||||
config.emit_test_exe or
|
||||
config.emit_helpgen) break :emit_docs false;
|
||||
|
||||
// We always emit docs in system package mode.
|
||||
if (system_package) break :emit_docs true;
|
||||
|
||||
// We only default to true if we can find pandoc.
|
||||
const path = Command.expandPath(b.allocator, "pandoc") catch
|
||||
break :emit_docs false;
|
||||
defer if (path) |p| b.allocator.free(p);
|
||||
break :emit_docs path != null;
|
||||
};
|
||||
|
||||
config.emit_terminfo = b.option(
|
||||
bool,
|
||||
"emit-terminfo",
|
||||
"Install Ghostty terminfo source file",
|
||||
) orelse switch (target.result.os.tag) {
|
||||
.windows => true,
|
||||
else => switch (optimize) {
|
||||
.Debug => true,
|
||||
.ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
|
||||
},
|
||||
};
|
||||
|
||||
config.emit_termcap = b.option(
|
||||
bool,
|
||||
"emit-termcap",
|
||||
"Install Ghostty termcap file",
|
||||
) orelse switch (optimize) {
|
||||
.Debug => true,
|
||||
.ReleaseSafe, .ReleaseFast, .ReleaseSmall => false,
|
||||
};
|
||||
|
||||
config.emit_webdata = b.option(
|
||||
bool,
|
||||
"emit-webdata",
|
||||
"Build the website data for the website.",
|
||||
) orelse false;
|
||||
|
||||
config.emit_xcframework = b.option(
|
||||
bool,
|
||||
"emit-xcframework",
|
||||
"Build and install the xcframework for the macOS library.",
|
||||
) orelse builtin.target.isDarwin() and
|
||||
target.result.os.tag == .macos and
|
||||
config.app_runtime == .none and
|
||||
(!config.emit_bench and
|
||||
!config.emit_test_exe and
|
||||
!config.emit_helpgen);
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// System Packages
|
||||
|
||||
// These are all our dependencies that can be used with system
|
||||
// packages if they exist. We set them up here so that we can set
|
||||
// their defaults early. The first call configures the integration and
|
||||
// subsequent calls just return the configured value. This lets them
|
||||
// show up properly in `--help`.
|
||||
|
||||
{
|
||||
// These dependencies we want to default false if we're on macOS.
|
||||
// On macOS we don't want to use system libraries because we
|
||||
// generally want a fat binary. This can be overridden with the
|
||||
// `-fsys` flag.
|
||||
for (&[_][]const u8{
|
||||
"freetype",
|
||||
"harfbuzz",
|
||||
"fontconfig",
|
||||
"libpng",
|
||||
"zlib",
|
||||
"oniguruma",
|
||||
}) |dep| {
|
||||
_ = b.systemIntegrationOption(
|
||||
dep,
|
||||
.{
|
||||
// If we're not on darwin we want to use whatever the
|
||||
// default is via the system package mode
|
||||
.default = if (target.result.isDarwin()) false else null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// These default to false because they're rarely available as
|
||||
// system packages so we usually want to statically link them.
|
||||
for (&[_][]const u8{
|
||||
"glslang",
|
||||
"spirv-cross",
|
||||
"simdutf",
|
||||
}) |dep| {
|
||||
_ = b.systemIntegrationOption(dep, .{ .default = false });
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/// Configure the build options with our values.
|
||||
pub fn addOptions(self: *const Config, step: *std.Build.Step.Options) !void {
|
||||
// We need to break these down individual because addOption doesn't
|
||||
// support all types.
|
||||
step.addOption(bool, "flatpak", self.flatpak);
|
||||
step.addOption(bool, "adwaita", self.adwaita);
|
||||
step.addOption(bool, "x11", self.x11);
|
||||
step.addOption(bool, "wayland", self.wayland);
|
||||
step.addOption(bool, "sentry", self.sentry);
|
||||
step.addOption(apprt.Runtime, "app_runtime", self.app_runtime);
|
||||
step.addOption(font.Backend, "font_backend", self.font_backend);
|
||||
step.addOption(renderer.Impl, "renderer", self.renderer);
|
||||
step.addOption(ExeEntrypoint, "exe_entrypoint", self.exe_entrypoint);
|
||||
step.addOption(WasmTarget, "wasm_target", self.wasm_target);
|
||||
step.addOption(bool, "wasm_shared", self.wasm_shared);
|
||||
|
||||
// Our version. We also add the string version so we don't need
|
||||
// to do any allocations at runtime. This has to be long enough to
|
||||
// accommodate realistic large branch names for dev versions.
|
||||
var buf: [1024]u8 = undefined;
|
||||
step.addOption(std.SemanticVersion, "app_version", self.version);
|
||||
step.addOption([:0]const u8, "app_version_string", try std.fmt.bufPrintZ(
|
||||
&buf,
|
||||
"{}",
|
||||
.{self.version},
|
||||
));
|
||||
step.addOption(
|
||||
ReleaseChannel,
|
||||
"release_channel",
|
||||
channel: {
|
||||
const pre = self.version.pre orelse break :channel .stable;
|
||||
if (pre.len == 0) break :channel .stable;
|
||||
break :channel .tip;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Rehydrate our Config from the comptime options. Note that not all
|
||||
/// options are available at comptime, so look closely at this implementation
|
||||
/// to see what is and isn't available.
|
||||
pub fn fromOptions() Config {
|
||||
const options = @import("build_options");
|
||||
return .{
|
||||
// Unused at runtime.
|
||||
.optimize = undefined,
|
||||
.target = undefined,
|
||||
.env = undefined,
|
||||
|
||||
.version = options.app_version,
|
||||
.flatpak = options.flatpak,
|
||||
.adwaita = options.adwaita,
|
||||
.app_runtime = std.meta.stringToEnum(apprt.Runtime, @tagName(options.app_runtime)).?,
|
||||
.font_backend = std.meta.stringToEnum(font.Backend, @tagName(options.font_backend)).?,
|
||||
.renderer = std.meta.stringToEnum(renderer.Impl, @tagName(options.renderer)).?,
|
||||
.exe_entrypoint = std.meta.stringToEnum(ExeEntrypoint, @tagName(options.exe_entrypoint)).?,
|
||||
.wasm_target = std.meta.stringToEnum(WasmTarget, @tagName(options.wasm_target)).?,
|
||||
.wasm_shared = options.wasm_shared,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the minimum OS version for the given OS tag. This shouldn't
|
||||
/// be used generally, it should only be used for Darwin-based OS currently.
|
||||
pub fn osVersionMin(tag: std.Target.Os.Tag) ?std.Target.Query.OsVersion {
|
||||
return switch (tag) {
|
||||
// We support back to the earliest officially supported version
|
||||
// of macOS by Apple. EOL versions are not supported.
|
||||
.macos => .{ .semver = .{
|
||||
.major = 13,
|
||||
.minor = 0,
|
||||
.patch = 0,
|
||||
} },
|
||||
|
||||
// iOS 17 picked arbitrarily
|
||||
.ios => .{ .semver = .{
|
||||
.major = 17,
|
||||
.minor = 0,
|
||||
.patch = 0,
|
||||
} },
|
||||
|
||||
// This should never happen currently. If we add a new target then
|
||||
// we should add a new case here.
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
// Returns a ResolvedTarget for a mac with a `target.result.cpu.model.name` of `generic`.
|
||||
// `b.standardTargetOptions()` returns a more specific cpu like `apple_a15`.
|
||||
//
|
||||
// This is used to workaround compilation issues on macOS.
|
||||
// (see for example https://github.com/mitchellh/ghostty/issues/1640).
|
||||
pub fn genericMacOSTarget(
|
||||
b: *std.Build,
|
||||
arch: ?std.Target.Cpu.Arch,
|
||||
) std.Build.ResolvedTarget {
|
||||
return b.resolveTargetQuery(.{
|
||||
.cpu_arch = arch orelse builtin.target.cpu.arch,
|
||||
.os_tag = .macos,
|
||||
.os_version_min = osVersionMin(.macos),
|
||||
});
|
||||
}
|
||||
|
||||
/// The possible entrypoints for the exe artifact. This has no effect on
|
||||
/// other artifact types (i.e. lib, wasm_module).
|
||||
///
|
||||
/// The whole existence of this enum is to workaround the fact that Zig
|
||||
/// doesn't allow the main function to be in a file in a subdirctory
|
||||
/// from the "root" of the module, and I don't want to pollute our root
|
||||
/// directory with a bunch of individual zig files for each entrypoint.
|
||||
///
|
||||
/// Therefore, main.zig uses this to switch between the different entrypoints.
|
||||
pub const ExeEntrypoint = enum {
|
||||
ghostty,
|
||||
helpgen,
|
||||
mdgen_ghostty_1,
|
||||
mdgen_ghostty_5,
|
||||
webgen_config,
|
||||
webgen_actions,
|
||||
webgen_commands,
|
||||
bench_parser,
|
||||
bench_stream,
|
||||
bench_codepoint_width,
|
||||
bench_grapheme_break,
|
||||
bench_page_init,
|
||||
};
|
||||
|
||||
/// The release channel for the build.
|
||||
pub const ReleaseChannel = enum {
|
||||
/// Unstable builds on every commit.
|
||||
tip,
|
||||
|
||||
/// Stable tagged releases.
|
||||
stable,
|
||||
};
|
69
src/build/GhosttyBench.zig
Normal file
69
src/build/GhosttyBench.zig
Normal file
@ -0,0 +1,69 @@
|
||||
//! GhosttyBench generates all the Ghostty benchmark helper binaries.
|
||||
const GhosttyBench = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const Config = @import("Config.zig");
|
||||
const SharedDeps = @import("SharedDeps.zig");
|
||||
|
||||
steps: []*std.Build.Step.Compile,
|
||||
|
||||
pub fn init(
|
||||
b: *std.Build,
|
||||
deps: *const SharedDeps,
|
||||
) !GhosttyBench {
|
||||
var steps = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);
|
||||
errdefer steps.deinit();
|
||||
|
||||
// Open the directory ./src/bench
|
||||
const c_dir_path = b.pathFromRoot("src/bench");
|
||||
var c_dir = try std.fs.cwd().openDir(c_dir_path, .{ .iterate = true });
|
||||
defer c_dir.close();
|
||||
|
||||
// Go through and add each as a step
|
||||
var c_dir_it = c_dir.iterate();
|
||||
while (try c_dir_it.next()) |entry| {
|
||||
// Get the index of the last '.' so we can strip the extension.
|
||||
const index = std.mem.lastIndexOfScalar(u8, entry.name, '.') orelse continue;
|
||||
if (index == 0) continue;
|
||||
|
||||
// If it doesn't end in 'zig' then ignore
|
||||
if (!std.mem.eql(u8, entry.name[index + 1 ..], "zig")) continue;
|
||||
|
||||
// Name of the conformance app and full path to the entrypoint.
|
||||
const name = entry.name[0..index];
|
||||
|
||||
// Executable builder.
|
||||
const bin_name = try std.fmt.allocPrint(b.allocator, "bench-{s}", .{name});
|
||||
const c_exe = b.addExecutable(.{
|
||||
.name = bin_name,
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = deps.config.target,
|
||||
|
||||
// We always want our benchmarks to be in release mode.
|
||||
.optimize = .ReleaseFast,
|
||||
});
|
||||
c_exe.linkLibC();
|
||||
|
||||
// Update our entrypoint
|
||||
var enum_name: [64]u8 = undefined;
|
||||
@memcpy(enum_name[0..name.len], name);
|
||||
std.mem.replaceScalar(u8, enum_name[0..name.len], '-', '_');
|
||||
|
||||
var buf: [64]u8 = undefined;
|
||||
const new_deps = try deps.changeEntrypoint(b, std.meta.stringToEnum(
|
||||
Config.ExeEntrypoint,
|
||||
try std.fmt.bufPrint(&buf, "bench_{s}", .{enum_name[0..name.len]}),
|
||||
).?);
|
||||
|
||||
_ = try new_deps.add(c_exe);
|
||||
|
||||
try steps.append(c_exe);
|
||||
}
|
||||
|
||||
return .{ .steps = steps.items };
|
||||
}
|
||||
|
||||
pub fn install(self: *const GhosttyBench) void {
|
||||
const b = self.steps[0].step.owner;
|
||||
for (self.steps) |step| b.installArtifact(step);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user