Merge branch 'ghostty-org:main' into hu_HU_localization
4
.github/workflows/release-pr.yml
vendored
@ -94,7 +94,7 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
@ -246,7 +246,7 @@ jobs:
|
||||
- name: Build Ghostty.app
|
||||
run: |
|
||||
cd macos
|
||||
sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
xcodebuild -target Ghostty -configuration Release
|
||||
|
||||
# We inject the "build number" as simply the number of commits since HEAD.
|
||||
|
6
.github/workflows/release-tip.yml
vendored
@ -173,7 +173,7 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
@ -388,7 +388,7 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
@ -563,7 +563,7 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: XCode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
# Setup Sparkle
|
||||
- name: Setup Sparkle
|
||||
|
17
.github/workflows/test.yml
vendored
@ -286,7 +286,7 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Xcode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: get the Zig deps
|
||||
id: deps
|
||||
@ -328,17 +328,6 @@ jobs:
|
||||
- name: Xcode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
# TODO(tahoe):
|
||||
# https://developer.apple.com/documentation/xcode-release-notes/xcode-26-release-notes#Interface-Builder
|
||||
# We allow this step to fail because if our image already has
|
||||
# the workaround in place this will fail.
|
||||
- name: Xcode 26 Beta 17A5241e Metal Workaround
|
||||
continue-on-error: true
|
||||
run: |
|
||||
xcodebuild -downloadComponent metalToolchain -exportPath /tmp/MyMetalExport/
|
||||
sed -i '' -e 's/17A5241c/17A5241e/g' /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle/ExportMetadata.plist
|
||||
xcodebuild -importComponent metalToolchain -importPath /tmp/MyMetalExport/MetalToolchain-17A5241c.exportedBundle
|
||||
|
||||
- name: get the Zig deps
|
||||
id: deps
|
||||
run: nix build -L .#deps && echo "deps=$(readlink ./result)" >> $GITHUB_OUTPUT
|
||||
@ -377,7 +366,7 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Xcode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: get the Zig deps
|
||||
id: deps
|
||||
@ -695,7 +684,7 @@ jobs:
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Xcode Select
|
||||
run: sudo xcode-select -s /Applications/Xcode_16.4.app
|
||||
run: sudo xcode-select -s /Applications/Xcode_26.0.app
|
||||
|
||||
- name: get the Zig deps
|
||||
id: deps
|
||||
|
@ -11,6 +11,9 @@ zig-out/
|
||||
# macos is managed by XCode GUI
|
||||
macos/
|
||||
|
||||
# produced by Icon Composer on macOS
|
||||
images/Ghostty.icon/icon.json
|
||||
|
||||
# website dev run
|
||||
website/.next
|
||||
|
||||
|
22
README.md
@ -224,6 +224,28 @@ macOS users don't require any additional dependencies.
|
||||
> source tarballs, see the
|
||||
> [website](http://ghostty.org/docs/install/build).
|
||||
|
||||
### Xcode Version and SDKs
|
||||
|
||||
Building the Ghostty macOS app requires that Xcode, the macOS SDK,
|
||||
and the iOS SDK are all installed.
|
||||
|
||||
A common issue is that the incorrect version of Xcode is either
|
||||
installed or selected. Use the `xcode-select` command to
|
||||
ensure that the correct version of Xcode is selected:
|
||||
|
||||
```shell-session
|
||||
sudo xcode-select --switch /Applications/Xcode-beta.app
|
||||
```
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Main branch development of Ghostty is preparing for the next major
|
||||
> macOS release, Tahoe (macOS 26). Therefore, the main branch requires
|
||||
> **Xcode 26 and the macOS 26 SDK**.
|
||||
>
|
||||
> You do not need to be running on macOS 26 to build Ghostty, you can
|
||||
> still use Xcode 26 beta on macOS 15 stable.
|
||||
|
||||
### Linting
|
||||
|
||||
#### Prettier
|
||||
|
@ -8,8 +8,8 @@
|
||||
|
||||
.libxev = .{
|
||||
// mitchellh/libxev
|
||||
.url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz",
|
||||
.url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
|
||||
.hash = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
|
||||
.lazy = true,
|
||||
},
|
||||
.vaxis = .{
|
||||
@ -103,8 +103,8 @@
|
||||
// Other
|
||||
.apple_sdk = .{ .path = "./pkg/apple-sdk" },
|
||||
.iterm2_themes = .{
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz",
|
||||
.hash = "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj",
|
||||
.url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
|
||||
.hash = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
|
||||
.lazy = true,
|
||||
},
|
||||
},
|
||||
|
12
build.zig.zon.json
generated
@ -54,20 +54,20 @@
|
||||
"url": "https://deps.files.ghostty.org/imgui-1220bc6b9daceaf7c8c60f3c3998058045ba0c5c5f48ae255ff97776d9cd8bfc6402.tar.gz",
|
||||
"hash": "sha256-oF/QHgTPEat4Hig4fGIdLkIPHmBEyOJ6JeYD6pnveGA="
|
||||
},
|
||||
"N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj": {
|
||||
"N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX": {
|
||||
"name": "iterm2_themes",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz",
|
||||
"hash": "sha256-TcrTZUCVetvAFGX0fBqg3zlG90flljScNr/OYR/MJ5Y="
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
|
||||
"hash": "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U="
|
||||
},
|
||||
"N-V-__8AAJrvXQCqAT8Mg9o_tk6m0yf5Fz-gCNEOKLyTSerD": {
|
||||
"name": "libpng",
|
||||
"url": "https://deps.files.ghostty.org/libpng-1220aa013f0c83da3fb64ea6d327f9173fa008d10e28bc9349eac3463457723b1c66.tar.gz",
|
||||
"hash": "sha256-/syVtGzwXo4/yKQUdQ4LparQDYnp/fF16U/wQcrxoDo="
|
||||
},
|
||||
"libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz": {
|
||||
"libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3": {
|
||||
"name": "libxev",
|
||||
"url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
|
||||
"hash": "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o="
|
||||
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
|
||||
"hash": "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU="
|
||||
},
|
||||
"N-V-__8AAG3RoQEyRC2Vw7Qoro5SYBf62IHn3HjqtNVY6aWK": {
|
||||
"name": "libxml2",
|
||||
|
12
build.zig.zon.nix
generated
@ -170,11 +170,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj";
|
||||
name = "N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX";
|
||||
path = fetchZigArtifact {
|
||||
name = "iterm2_themes";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz";
|
||||
hash = "sha256-TcrTZUCVetvAFGX0fBqg3zlG90flljScNr/OYR/MJ5Y=";
|
||||
url = "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz";
|
||||
hash = "sha256-C93MSyNgyB+uhvzMQETDXr7839hFyX7NfTMp4HUKs3U=";
|
||||
};
|
||||
}
|
||||
{
|
||||
@ -186,11 +186,11 @@ in
|
||||
};
|
||||
}
|
||||
{
|
||||
name = "libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz";
|
||||
name = "libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3";
|
||||
path = fetchZigArtifact {
|
||||
name = "libxev";
|
||||
url = "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz";
|
||||
hash = "sha256-oKZqA9d79jHnp/HsqJWQE33Ffn5Ee5G4VnlQepQuY4o=";
|
||||
url = "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz";
|
||||
hash = "sha256-VwFByDoptqiN5UkolFQ7TbRhwMERReD9Er2pjxTCYIU=";
|
||||
};
|
||||
}
|
||||
{
|
||||
|
4
build.zig.zon.txt
generated
@ -27,8 +27,8 @@ https://deps.files.ghostty.org/ziglyph-b89d43d1e3fb01b6074bc1f7fc980324b04d26a5.
|
||||
https://deps.files.ghostty.org/zlib-1220fed0c74e1019b3ee29edae2051788b080cd96e90d56836eea857b0b966742efb.tar.gz
|
||||
https://github.com/glfw/glfw/archive/e7ea71be039836da3a98cea55ae5569cb5eb885c.tar.gz
|
||||
https://github.com/jcollie/ghostty-gobject/releases/download/0.14.0-2025-03-18-21-1/ghostty-gobject-0.14.0-2025-03-18-21-1.tar.zst
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz
|
||||
https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz
|
||||
https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz
|
||||
https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz
|
||||
https://github.com/mitchellh/zig-objc/archive/3ab0d37c7d6b933d6ded1b3a35b6b60f05590a98.tar.gz
|
||||
https://github.com/natecraddock/zf/archive/7aacbe6d155d64d15937ca95ca6c014905eb531f.tar.gz
|
||||
https://github.com/vancluever/z2d/archive/1bf4bc81819385f4b24596445c9a7cf3b3592b08.tar.gz
|
||||
|
BIN
dist/macos/Ghostty.icns
vendored
17
dist/macos/Info.plist
vendored
@ -1,17 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>ghostty</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.mitchellh.ghostty</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Ghostty</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Ghostty</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>Ghostty.icns</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -67,9 +67,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/10f216f54a32cee6f1f2e3aa01812650a209c16b.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAMJWXQSVe0xzU_4UVRTDVERqnNpQU4wfS0FRGRRj",
|
||||
"sha256": "4dcad36540957adbc01465f47c1aa0df3946f747e596349c36bfce611fcc2796"
|
||||
"url": "https://github.com/mbadolato/iTerm2-Color-Schemes/archive/e436898274ecb89c055da476a8188aa4f79ffb17.tar.gz",
|
||||
"dest": "vendor/p/N-V-__8AAHncWgThrlpsuJJ2BIINQ6L7SO6SUOT1pEL8UQaX",
|
||||
"sha256": "0bddcc4b2360c81fae86fccc4044c35ebefcdfd845c97ecd7d3329e0750ab375"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
@ -79,9 +79,9 @@
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://github.com/mitchellh/libxev/archive/3df9337a9e84450a58a2c4af434ec1a036f7b494.tar.gz",
|
||||
"dest": "vendor/p/libxev-0.0.0-86vtc-ziEgDbLP0vihUn1MhsxNKY4GJEga6BEr7oyHpz",
|
||||
"sha256": "a0a66a03d77bf631e7a7f1eca89590137dc57e7e447b91b85679507a942e638a"
|
||||
"url": "https://github.com/mitchellh/libxev/archive/9bc52324d4f0c036a3b244e992680a9fb217bbd3.tar.gz",
|
||||
"dest": "vendor/p/libxev-0.0.0-86vtc5b1EgB7vFmt9Tk7ySteR5AeEHW7xcR6gK9dMUD3",
|
||||
"sha256": "570141c83a29b6a88de5492894543b4db461c0c11145e0fd12bda98f14c26085"
|
||||
},
|
||||
{
|
||||
"type": "archive",
|
||||
|
BIN
images/Ghostty.icon/Assets/Ghostty.png
Normal file
After Width: | Height: | Size: 104 KiB |
BIN
images/Ghostty.icon/Assets/Inner Bevel 6px.png
Normal file
After Width: | Height: | Size: 426 KiB |
BIN
images/Ghostty.icon/Assets/Screen Effects.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
images/Ghostty.icon/Assets/Screen.png
Normal file
After Width: | Height: | Size: 140 KiB |
BIN
images/Ghostty.icon/Assets/gloss.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
170
images/Ghostty.icon/icon.json
Normal file
@ -0,0 +1,170 @@
|
||||
{
|
||||
"color-space-for-untagged-svg-colors" : "display-p3",
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"display-p3:0.87945,0.87945,0.87945,1.00000",
|
||||
"display-p3:0.40000,0.40000,0.40392,1.00000"
|
||||
]
|
||||
},
|
||||
"groups" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "overlay",
|
||||
"fill" : {
|
||||
"linear-gradient" : [
|
||||
"srgb:1.00000,1.00000,1.00000,1.00000",
|
||||
"srgb:0.00000,0.00000,0.00000,1.00000"
|
||||
]
|
||||
},
|
||||
"hidden" : false,
|
||||
"image-name" : "gloss.png",
|
||||
"name" : "GlossTop",
|
||||
"opacity" : 0.25,
|
||||
"position" : {
|
||||
"scale" : 0.98,
|
||||
"translation-in-points" : [
|
||||
0.90625,
|
||||
-236.4609375
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : "automatic",
|
||||
"hidden" : false,
|
||||
"image-name" : "gloss.png",
|
||||
"name" : "gloss",
|
||||
"position" : {
|
||||
"scale" : 0.98,
|
||||
"translation-in-points" : [
|
||||
0.90625,
|
||||
-236.4609375
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group 4",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : true,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode" : "overlay",
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "overlay",
|
||||
"fill" : "automatic",
|
||||
"glass" : false,
|
||||
"hidden" : false,
|
||||
"image-name" : "Screen Effects.png",
|
||||
"name" : "Screen Effects"
|
||||
},
|
||||
{
|
||||
"blend-mode" : "overlay",
|
||||
"fill" : "automatic",
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "Screen Effects.png",
|
||||
"name" : "Screen Effects"
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group 3",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blur-material" : null,
|
||||
"layers" : [
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : "automatic",
|
||||
"hidden" : false,
|
||||
"image-name" : "Ghostty.png",
|
||||
"name" : "Ghostty",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
-185.015625,
|
||||
-143.8359375
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"fill" : {
|
||||
"solid" : "extended-srgb:0.00000,0.47843,1.00000,1.00000"
|
||||
},
|
||||
"glass" : true,
|
||||
"hidden" : false,
|
||||
"image-name" : "Ghostty.png",
|
||||
"name" : "GhosttyBlur",
|
||||
"position" : {
|
||||
"scale" : 1,
|
||||
"translation-in-points" : [
|
||||
-186.59375,
|
||||
-143.8359375
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"hidden" : false,
|
||||
"image-name" : "Screen.png",
|
||||
"name" : "Screen"
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group 2",
|
||||
"shadow" : {
|
||||
"kind" : "neutral",
|
||||
"opacity" : 0.5
|
||||
},
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"blend-mode" : "normal",
|
||||
"blur-material" : null,
|
||||
"hidden" : false,
|
||||
"layers" : [
|
||||
{
|
||||
"image-name" : "Inner Bevel 6px.png",
|
||||
"name" : "Inner Bevel 6px"
|
||||
}
|
||||
],
|
||||
"lighting" : "individual",
|
||||
"name" : "Group 1",
|
||||
"shadow" : {
|
||||
"kind" : "layer-color",
|
||||
"opacity" : 0.2
|
||||
},
|
||||
"specular" : false,
|
||||
"translucency" : {
|
||||
"enabled" : false,
|
||||
"value" : 0.5
|
||||
}
|
||||
}
|
||||
],
|
||||
"supported-platforms" : {
|
||||
"circles" : [
|
||||
"watchOS"
|
||||
],
|
||||
"squares" : "shared"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 454 KiB After Width: | Height: | Size: 2.3 MiB |
BIN
images/icons/icon_1024@2x.png
Normal file
After Width: | Height: | Size: 2.3 MiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 232 KiB |
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 652 KiB |
BIN
images/icons/icon_512@2x.png
Normal file
After Width: | Height: | Size: 652 KiB |
@ -385,6 +385,11 @@ typedef struct {
|
||||
bool rectangle;
|
||||
} ghostty_selection_s;
|
||||
|
||||
typedef struct {
|
||||
const char* key;
|
||||
const char* value;
|
||||
} ghostty_env_var_s;
|
||||
|
||||
typedef struct {
|
||||
void* nsview;
|
||||
} ghostty_platform_macos_s;
|
||||
@ -406,6 +411,9 @@ typedef struct {
|
||||
float font_size;
|
||||
const char* working_directory;
|
||||
const char* command;
|
||||
ghostty_env_var_s* env_vars;
|
||||
size_t env_var_count;
|
||||
const char* initial_input;
|
||||
} ghostty_surface_config_s;
|
||||
|
||||
typedef struct {
|
||||
@ -807,7 +815,8 @@ void ghostty_app_set_color_scheme(ghostty_app_t, ghostty_color_scheme_e);
|
||||
|
||||
ghostty_surface_config_s ghostty_surface_config_new();
|
||||
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t, ghostty_surface_config_s*);
|
||||
ghostty_surface_t ghostty_surface_new(ghostty_app_t,
|
||||
const ghostty_surface_config_s*);
|
||||
void ghostty_surface_free(ghostty_surface_t);
|
||||
void* ghostty_surface_userdata(ghostty_surface_t);
|
||||
ghostty_app_t ghostty_surface_app(ghostty_surface_t);
|
||||
|
@ -1,74 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "macOS-AppIcon-1024px.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-16px-16pt@1x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-32px-16pt@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-32px-32pt@1x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-64px-32pt@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-128px-128pt@1x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-256px-128pt@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-256px-128pt@2x 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-512px-256pt@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-512px.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "macOS-AppIcon-1024px 1.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 454 KiB |
Before Width: | Height: | Size: 454 KiB |
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 666 B |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 216 KiB |
Before Width: | Height: | Size: 4.4 KiB |
@ -13,6 +13,11 @@
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
|
||||
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A511940E2E050590007258CC /* CloseTerminalIntent.swift */; };
|
||||
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194102E05A480007258CC /* QuickTerminalIntent.swift */; };
|
||||
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
|
||||
A51194172E05D964007258CC /* PermissionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194162E05D95E007258CC /* PermissionRequest.swift */; };
|
||||
A51194192E05DFC4007258CC /* IntentPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194182E05DFBB007258CC /* IntentPermission.swift */; };
|
||||
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 */; };
|
||||
@ -53,7 +58,10 @@
|
||||
A54B0CED2D0CFB7700CBEFF8 /* ColorizedGhosttyIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */; };
|
||||
A54B0CEF2D0D2E2800CBEFF8 /* ColorizedGhosttyIconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */; };
|
||||
A54D786C2CA7978E001B19B1 /* BaseTerminalController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */; };
|
||||
A55685E029A03A9F004303CE /* AppError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A55685DF29A03A9F004303CE /* AppError.swift */; };
|
||||
A553F4062E05E93000257779 /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A51194122E05D003007258CC /* Optional+Extension.swift */; };
|
||||
A553F4072E05E93D00257779 /* Array+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A586366A2DF0A98900E04A10 /* Array+Extension.swift */; };
|
||||
A553F4132E06EB1600257779 /* Ghostty.icon in Resources */ = {isa = PBXBuildFile; fileRef = A553F4122E06EB1600257779 /* Ghostty.icon */; };
|
||||
A553F4142E06EB1600257779 /* Ghostty.icon in Resources */ = {isa = PBXBuildFile; fileRef = A553F4122E06EB1600257779 /* Ghostty.icon */; };
|
||||
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */; };
|
||||
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */; };
|
||||
A5593FE32DF8D78600B47B10 /* TerminalHiddenTitlebar.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */; };
|
||||
@ -119,6 +127,18 @@
|
||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
||||
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
||||
A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */; };
|
||||
A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */; };
|
||||
A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */; };
|
||||
A5E408322E02FEDF0035FEAC /* TerminalEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */; };
|
||||
A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */; };
|
||||
A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */; };
|
||||
A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */; };
|
||||
A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */; };
|
||||
A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E4083F2E04532A0035FEAC /* CommandEntity.swift */; };
|
||||
A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */; };
|
||||
A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408442E0483F80035FEAC /* KeybindIntent.swift */; };
|
||||
A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E408462E0485270035FEAC /* InputIntent.swift */; };
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5FEB2FF2ABB69450068369E /* main.swift */; };
|
||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */; };
|
||||
C159E81D2B66A06B00FDFE9C /* OSColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */; };
|
||||
@ -138,6 +158,11 @@
|
||||
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>"; };
|
||||
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
A511940E2E050590007258CC /* CloseTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseTerminalIntent.swift; sourceTree = "<group>"; };
|
||||
A51194102E05A480007258CC /* QuickTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickTerminalIntent.swift; sourceTree = "<group>"; };
|
||||
A51194122E05D003007258CC /* Optional+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extension.swift"; sourceTree = "<group>"; };
|
||||
A51194162E05D95E007258CC /* PermissionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionRequest.swift; sourceTree = "<group>"; };
|
||||
A51194182E05DFBB007258CC /* IntentPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentPermission.swift; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51544FD2DFB1110009E85D8 /* TitlebarTabsTahoeTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitlebarTabsTahoeTerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A51544FF2DFB112E009E85D8 /* TerminalTabsTitlebarTahoe.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalTabsTitlebarTahoe.xib; sourceTree = "<group>"; };
|
||||
@ -170,7 +195,7 @@
|
||||
A54B0CEC2D0CFB7300CBEFF8 /* ColorizedGhosttyIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIcon.swift; sourceTree = "<group>"; };
|
||||
A54B0CEE2D0D2E2400CBEFF8 /* ColorizedGhosttyIconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorizedGhosttyIconImage.swift; sourceTree = "<group>"; };
|
||||
A54D786B2CA79788001B19B1 /* BaseTerminalController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTerminalController.swift; sourceTree = "<group>"; };
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppError.swift; sourceTree = "<group>"; };
|
||||
A553F4122E06EB1600257779 /* Ghostty.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; name = Ghostty.icon; path = ../images/Ghostty.icon; sourceTree = SOURCE_ROOT; };
|
||||
A5593FDE2DF8D57100B47B10 /* TerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A5593FE02DF8D73400B47B10 /* HiddenTitlebarTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenTitlebarTerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A5593FE22DF8D78600B47B10 /* TerminalHiddenTitlebar.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TerminalHiddenTitlebar.xib; sourceTree = "<group>"; };
|
||||
@ -238,6 +263,18 @@
|
||||
A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ClipboardConfirmation.xib; sourceTree = "<group>"; };
|
||||
A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationController.swift; sourceTree = "<group>"; };
|
||||
A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClipboardConfirmationView.swift; sourceTree = "<group>"; };
|
||||
A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabGroupCloseCoordinator.swift; sourceTree = "<group>"; };
|
||||
A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewTerminalIntent.swift; sourceTree = "<group>"; };
|
||||
A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyIntentError.swift; sourceTree = "<group>"; };
|
||||
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalEntity.swift; sourceTree = "<group>"; };
|
||||
A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTerminalDetailsIntent.swift; sourceTree = "<group>"; };
|
||||
A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Surface.swift; sourceTree = "<group>"; };
|
||||
A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Command.swift; sourceTree = "<group>"; };
|
||||
A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Error.swift; sourceTree = "<group>"; };
|
||||
A5E4083F2E04532A0035FEAC /* CommandEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandEntity.swift; sourceTree = "<group>"; };
|
||||
A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteIntent.swift; sourceTree = "<group>"; };
|
||||
A5E408442E0483F80035FEAC /* KeybindIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindIntent.swift; sourceTree = "<group>"; };
|
||||
A5E408462E0485270035FEAC /* InputIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputIntent.swift; sourceTree = "<group>"; };
|
||||
A5FEB2FF2ABB69450068369E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = "<group>"; };
|
||||
AEE8B3442B9AA39600260C5E /* NSPasteboard+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Extension.swift"; sourceTree = "<group>"; };
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OSColor+Extension.swift"; sourceTree = "<group>"; };
|
||||
@ -297,6 +334,7 @@
|
||||
A56D58872ACDE6BE00508D2C /* Services */,
|
||||
A59630982AEE1C4400D64628 /* Terminal */,
|
||||
A5CBD05A2CA0C5910017A1AE /* QuickTerminal */,
|
||||
A5E4082C2E0237270035FEAC /* App Intents */,
|
||||
A5E112912AF73E4D00C6E0C2 /* ClipboardConfirmation */,
|
||||
A57D79252C9C8782001D522E /* Secure Input */,
|
||||
A58636622DEF955100E04A10 /* Splits */,
|
||||
@ -320,12 +358,14 @@
|
||||
A5333E1B2B5A1CE3008AEFF7 /* CrossKit.swift */,
|
||||
A5CBD0572C9F30860017A1AE /* Cursor.swift */,
|
||||
A5D0AF3C2B37804400D21823 /* CodableBridge.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
A58636702DF298F700E04A10 /* ExpiringUndoManager.swift */,
|
||||
A52FFF582CAA4FF1000C6A5B /* Fullscreen.swift */,
|
||||
A59630962AEE163600D64628 /* HostingWindow.swift */,
|
||||
A5CA378B2D2A4DE800931030 /* KeyboardLayout.swift */,
|
||||
A59FB5D02AE0DEA7009128F3 /* MetalView.swift */,
|
||||
A5CBD0552C9E65A50017A1AE /* DraggableWindowView.swift */,
|
||||
A51194162E05D95E007258CC /* PermissionRequest.swift */,
|
||||
A5E408292E022E9B0035FEAC /* TabGroupCloseCoordinator.swift */,
|
||||
A5CA378D2D31D6C100931030 /* Weak.swift */,
|
||||
C1F26EE72B76CBFC00404083 /* VibrantLayer.h */,
|
||||
C1F26EE82B76CBFC00404083 /* VibrantLayer.m */,
|
||||
@ -428,12 +468,14 @@
|
||||
A5333E152B59DE8E008AEFF7 /* SurfaceView_UIKit.swift */,
|
||||
A59FB5CE2AE0DB50009128F3 /* InspectorView.swift */,
|
||||
A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */,
|
||||
A5E408392E0449BB0035FEAC /* Ghostty.Command.swift */,
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */,
|
||||
A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */,
|
||||
A5E4083B2E044DB40035FEAC /* Ghostty.Error.swift */,
|
||||
A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */,
|
||||
A5278A9A2AA05B2600CD3039 /* Ghostty.Input.swift */,
|
||||
A56D58852ACDDB4100508D2C /* Ghostty.Shell.swift */,
|
||||
A55685DF29A03A9F004303CE /* AppError.swift */,
|
||||
A5E408372E03C7D80035FEAC /* Ghostty.Surface.swift */,
|
||||
A52FFF5A2CAA54A8000C6A5B /* FullscreenMode+Extension.swift */,
|
||||
A5CF66D32D289CEA00139794 /* NSEvent+Extension.swift */,
|
||||
);
|
||||
@ -476,6 +518,7 @@
|
||||
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
|
||||
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
|
||||
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
|
||||
A51194122E05D003007258CC /* Optional+Extension.swift */,
|
||||
C159E81C2B66A06B00FDFE9C /* OSColor+Extension.swift */,
|
||||
A599CDAF2CF103F20049FA26 /* NSAppearance+Extension.swift */,
|
||||
A5A2A3CB2D444AB80033CF96 /* NSApplication+Extension.swift */,
|
||||
@ -536,6 +579,7 @@
|
||||
children = (
|
||||
A571AB1C2A206FC600248498 /* Ghostty-Info.plist */,
|
||||
A5B30538299BEAAB0047F10C /* Assets.xcassets */,
|
||||
A553F4122E06EB1600257779 /* Ghostty.icon */,
|
||||
A5B3053D299BEAAB0047F10C /* Ghostty.entitlements */,
|
||||
A51BFC282B30F26D00E92F16 /* GhosttyDebug.entitlements */,
|
||||
3B39CAA42B33949B00DABEB8 /* GhosttyReleaseLocal.entitlements */,
|
||||
@ -595,6 +639,32 @@
|
||||
path = ClipboardConfirmation;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5E4082C2E0237270035FEAC /* App Intents */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5E408412E0453370035FEAC /* Entities */,
|
||||
A511940E2E050590007258CC /* CloseTerminalIntent.swift */,
|
||||
A5E4082D2E0237410035FEAC /* NewTerminalIntent.swift */,
|
||||
A5E408332E03200F0035FEAC /* GetTerminalDetailsIntent.swift */,
|
||||
A51194102E05A480007258CC /* QuickTerminalIntent.swift */,
|
||||
A5E408422E047D060035FEAC /* CommandPaletteIntent.swift */,
|
||||
A5E408462E0485270035FEAC /* InputIntent.swift */,
|
||||
A5E408442E0483F80035FEAC /* KeybindIntent.swift */,
|
||||
A5E4082F2E0271320035FEAC /* GhosttyIntentError.swift */,
|
||||
A51194182E05DFBB007258CC /* IntentPermission.swift */,
|
||||
);
|
||||
path = "App Intents";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
A5E408412E0453370035FEAC /* Entities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
A5E408312E02FEDC0035FEAC /* TerminalEntity.swift */,
|
||||
A5E4083F2E04532A0035FEAC /* CommandEntity.swift */,
|
||||
);
|
||||
path = Entities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@ -682,6 +752,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
FC9ABA9C2D0F53F80020D4C8 /* bash-completion in Resources */,
|
||||
A553F4142E06EB1600257779 /* Ghostty.icon in Resources */,
|
||||
A5593FE52DF8DE3000B47B10 /* TerminalTabsTitlebarVentura.xib in Resources */,
|
||||
29C15B1D2CDC3B2900520DD4 /* bat in Resources */,
|
||||
A586167C2B7703CC009BDB1D /* fish in Resources */,
|
||||
@ -710,6 +781,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A53D0C952B53B4D800305CE6 /* Assets.xcassets in Resources */,
|
||||
A553F4132E06EB1600257779 /* Ghostty.icon in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -721,6 +793,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5AEB1652D5BE7D000513529 /* LastWindowPosition.swift in Sources */,
|
||||
A5E408432E047D0B0035FEAC /* CommandPaletteIntent.swift in Sources */,
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A54B0CEB2D0CFB4C00CBEFF8 /* NSImage+Extension.swift in Sources */,
|
||||
A5874D9D2DAD786100E83852 /* NSWindow+Extension.swift in Sources */,
|
||||
@ -730,17 +803,22 @@
|
||||
CFBB5FEA2D231E5000FD62EE /* QuickTerminalSpaceBehavior.swift in Sources */,
|
||||
A54B0CE92D0CECD100CBEFF8 /* ColorizedGhosttyIconView.swift in Sources */,
|
||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */,
|
||||
A51194132E05D006007258CC /* Optional+Extension.swift in Sources */,
|
||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */,
|
||||
C1F26EA72B738B9900404083 /* NSView+Extension.swift in Sources */,
|
||||
A586366F2DF25D8600E04A10 /* Duration+Extension.swift in Sources */,
|
||||
A5CF66D42D289CEE00139794 /* NSEvent+Extension.swift in Sources */,
|
||||
A5E408342E0320140035FEAC /* GetTerminalDetailsIntent.swift in Sources */,
|
||||
A5CBD0642CA122E70017A1AE /* QuickTerminalPosition.swift in Sources */,
|
||||
A596309C2AEE1C9E00D64628 /* TerminalController.swift in Sources */,
|
||||
A5E408322E02FEDF0035FEAC /* TerminalEntity.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 */,
|
||||
A511940F2E050595007258CC /* CloseTerminalIntent.swift in Sources */,
|
||||
A5E408382E03C7DA0035FEAC /* Ghostty.Surface.swift in Sources */,
|
||||
A5593FE72DF927D200B47B10 /* TransparentTitlebarTerminalWindow.swift in Sources */,
|
||||
A5A2A3CA2D4445E30033CF96 /* Dock.swift in Sources */,
|
||||
A586365F2DEE6C2300E04A10 /* SplitTree.swift in Sources */,
|
||||
@ -749,6 +827,7 @@
|
||||
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */,
|
||||
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
A51194112E05A483007258CC /* QuickTerminalIntent.swift in Sources */,
|
||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
||||
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */,
|
||||
A58636712DF298FB00E04A10 /* ExpiringUndoManager.swift in Sources */,
|
||||
@ -756,6 +835,8 @@
|
||||
A51BFC2B2B30F6BE00E92F16 /* UpdateDelegate.swift in Sources */,
|
||||
A5CBD06B2CA322430017A1AE /* GlobalEventTap.swift in Sources */,
|
||||
AEE8B3452B9AA39600260C5E /* NSPasteboard+Extension.swift in Sources */,
|
||||
A51194172E05D964007258CC /* PermissionRequest.swift in Sources */,
|
||||
A51194192E05DFC4007258CC /* IntentPermission.swift in Sources */,
|
||||
A52FFF5D2CAB4D08000C6A5B /* NSScreen+Extension.swift in Sources */,
|
||||
A53426352A7DA53D00EBB7A2 /* AppDelegate.swift in Sources */,
|
||||
A5CBD0582C9F30960017A1AE /* Cursor.swift in Sources */,
|
||||
@ -769,29 +850,36 @@
|
||||
A5B4EA852DFE691B0022C3A2 /* NSMenuItem+Extension.swift in Sources */,
|
||||
A5874D992DAD751B00E83852 /* CGS.swift in Sources */,
|
||||
A586366B2DF0A98C00E04A10 /* Array+Extension.swift in Sources */,
|
||||
A5E408472E04852B0035FEAC /* InputIntent.swift in Sources */,
|
||||
A51544FE2DFB111C009E85D8 /* TitlebarTabsTahoeTerminalWindow.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 */,
|
||||
A5E408302E0271320035FEAC /* GhosttyIntentError.swift in Sources */,
|
||||
A5E4083A2E0449BD0035FEAC /* Ghostty.Command.swift in Sources */,
|
||||
A5E408452E0483FD0035FEAC /* KeybindIntent.swift in Sources */,
|
||||
A5FEB3002ABB69450068369E /* main.swift in Sources */,
|
||||
A53A297F2DB4480F00B6E02C /* EventModifiers+Extension.swift in Sources */,
|
||||
A5E4082E2E0237460035FEAC /* NewTerminalIntent.swift in Sources */,
|
||||
A53A297B2DB2E49700B6E02C /* CommandPalette.swift in Sources */,
|
||||
A55B7BB829B6F53A0055DE60 /* Package.swift in Sources */,
|
||||
A51B78472AF4B58B00F3EDB9 /* TitlebarTabsVenturaTerminalWindow.swift in Sources */,
|
||||
A57D79272C9C879B001D522E /* SecureInput.swift in Sources */,
|
||||
A5CEAFDC29B8009000646FDA /* SplitView.swift in Sources */,
|
||||
A5593FE12DF8D74000B47B10 /* HiddenTitlebarTerminalWindow.swift in Sources */,
|
||||
A5E4083C2E044DB50035FEAC /* Ghostty.Error.swift in Sources */,
|
||||
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 */,
|
||||
A599CDB02CF103F60049FA26 /* NSAppearance+Extension.swift in Sources */,
|
||||
A52FFF572CA90484000C6A5B /* QuickTerminalScreen.swift in Sources */,
|
||||
A5CC36132C9CD72D004D6760 /* SecureInputOverlay.swift in Sources */,
|
||||
A5E408402E04532C0035FEAC /* CommandEntity.swift in Sources */,
|
||||
A5E4082A2E022E9E0035FEAC /* TabGroupCloseCoordinator.swift in Sources */,
|
||||
A535B9DA299C569B0017E2E4 /* ErrorView.swift in Sources */,
|
||||
A53A29882DB69D2F00B6E02C /* TerminalCommandPalette.swift in Sources */,
|
||||
A51BFC202B2FB64F00E92F16 /* AboutController.swift in Sources */,
|
||||
@ -812,6 +900,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
A5CBD0592C9F37B10017A1AE /* Backport.swift in Sources */,
|
||||
A553F4062E05E93000257779 /* Optional+Extension.swift in Sources */,
|
||||
A53D0C942B53B43700305CE6 /* iOSApp.swift in Sources */,
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */,
|
||||
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||
@ -821,6 +910,7 @@
|
||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
||||
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,
|
||||
A5985CD82C320C4500C57AD3 /* String+Extension.swift in Sources */,
|
||||
A553F4072E05E93D00257779 /* Array+Extension.swift in Sources */,
|
||||
C159E89D2B69A2EF00FDFE9C /* OSColor+Extension.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@ -886,7 +976,7 @@
|
||||
3B39CAA32B33946300DABEB8 /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@ -1056,7 +1146,7 @@
|
||||
A5B30541299BEAAB0047F10C /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@ -1110,7 +1200,7 @@
|
||||
A5B30542299BEAAB0047F10C /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
@ -1163,7 +1253,7 @@
|
||||
A5D449A82B53AE7B000F5B83 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -1202,7 +1292,7 @@
|
||||
A5D449A92B53AE7B000F5B83 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
@ -1241,7 +1331,7 @@
|
||||
A5D449AA2B53AE7B000F5B83 /* ReleaseLocal */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Ghostty;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
|
@ -92,7 +92,10 @@ class AppDelegate: NSObject,
|
||||
lazy var undoManager = ExpiringUndoManager()
|
||||
|
||||
/// Our quick terminal. This starts out uninitialized and only initializes if used.
|
||||
private var quickController: QuickTerminalController? = nil
|
||||
private(set) lazy var quickController = QuickTerminalController(
|
||||
ghostty,
|
||||
position: derivedConfig.quickTerminalPosition
|
||||
)
|
||||
|
||||
/// Manages updates
|
||||
let updaterController: SPUStandardUpdaterController
|
||||
@ -167,7 +170,7 @@ class AppDelegate: NSObject,
|
||||
|
||||
// This registers the Ghostty => Services menu to exist.
|
||||
NSApp.servicesMenu = menuServices
|
||||
|
||||
|
||||
// Setup a local event monitor for app-level keyboard shortcuts. See
|
||||
// localEventHandler for more info why.
|
||||
_ = NSEvent.addLocalMonitorForEvents(
|
||||
@ -286,7 +289,7 @@ class AppDelegate: NSObject,
|
||||
// NOTE(mitchellh): I don't think we need this check at all anymore. I'm keeping it
|
||||
// here because I don't want to remove it in a patch release cycle but we should
|
||||
// target removing it soon.
|
||||
if (self.quickController == nil && windows.allSatisfy { !$0.isVisible }) {
|
||||
if (windows.allSatisfy { !$0.isVisible }) {
|
||||
return .terminateNow
|
||||
}
|
||||
|
||||
@ -381,10 +384,17 @@ class AppDelegate: NSObject,
|
||||
config.workingDirectory = filename
|
||||
_ = TerminalController.newTab(ghostty, withBaseConfig: config)
|
||||
} else {
|
||||
// When opening a file, open a new window with that file as the command,
|
||||
// and its parent directory as the working directory.
|
||||
config.command = filename
|
||||
// When opening a file, we want to execute the file. To do this, we
|
||||
// don't override the command directly, because it won't load the
|
||||
// profile/rc files for the shell, which is super important on macOS
|
||||
// due to things like Homebrew. Instead, we set the command to
|
||||
// `<filename>; exit` which is what Terminal and iTerm2 do.
|
||||
config.initialInput = "\(filename); exit\n"
|
||||
|
||||
// Set the parent directory to our working directory so that relative
|
||||
// paths in scripts work.
|
||||
config.workingDirectory = (filename as NSString).deletingLastPathComponent
|
||||
|
||||
_ = TerminalController.newWindow(ghostty, withBaseConfig: config)
|
||||
}
|
||||
|
||||
@ -919,14 +929,6 @@ class AppDelegate: NSObject,
|
||||
}
|
||||
|
||||
@IBAction func toggleQuickTerminal(_ sender: Any) {
|
||||
if quickController == nil {
|
||||
quickController = QuickTerminalController(
|
||||
ghostty,
|
||||
position: derivedConfig.quickTerminalPosition
|
||||
)
|
||||
}
|
||||
|
||||
guard let quickController = self.quickController else { return }
|
||||
quickController.toggle()
|
||||
}
|
||||
|
||||
|
35
macos/Sources/Features/App Intents/CloseTerminalIntent.swift
Normal file
@ -0,0 +1,35 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
import GhosttyKit
|
||||
|
||||
struct CloseTerminalIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Close Terminal"
|
||||
static var description = IntentDescription("Close an existing terminal.")
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to close.",
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surfaceView = terminal.surfaceView else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
guard let controller = surfaceView.window?.windowController as? BaseTerminalController else {
|
||||
return .result()
|
||||
}
|
||||
|
||||
controller.closeSurface(surfaceView, withConfirmation: false)
|
||||
return .result()
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
|
||||
/// App intent that invokes a command palette entry.
|
||||
@available(macOS 14.0, *)
|
||||
struct CommandPaletteIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Invoke Command Palette Action"
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to base available commands from."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@Parameter(
|
||||
title: "Command",
|
||||
description: "The command to invoke.",
|
||||
optionsProvider: CommandQuery()
|
||||
)
|
||||
var command: CommandEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
let performed = surface.perform(action: command.action)
|
||||
return .result(value: performed)
|
||||
}
|
||||
}
|
128
macos/Sources/Features/App Intents/Entities/CommandEntity.swift
Normal file
@ -0,0 +1,128 @@
|
||||
import AppIntents
|
||||
|
||||
// MARK: AppEntity
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
struct CommandEntity: AppEntity {
|
||||
let id: ID
|
||||
|
||||
// Note: for macOS 26 we can move all the properties to @ComputedProperty.
|
||||
|
||||
@Property(title: "Title")
|
||||
var title: String
|
||||
|
||||
@Property(title: "Description")
|
||||
var description: String
|
||||
|
||||
@Property(title: "Action")
|
||||
var action: String
|
||||
|
||||
/// The underlying data model
|
||||
let command: Ghostty.Command
|
||||
|
||||
/// A command identifier is a composite key based on the terminal and action.
|
||||
struct ID: Hashable {
|
||||
let terminalId: TerminalEntity.ID
|
||||
let actionKey: String
|
||||
|
||||
init(terminalId: TerminalEntity.ID, actionKey: String) {
|
||||
self.terminalId = terminalId
|
||||
self.actionKey = actionKey
|
||||
}
|
||||
}
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
||||
TypeDisplayRepresentation(name: "Command Palette Command")
|
||||
}
|
||||
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
DisplayRepresentation(
|
||||
title: LocalizedStringResource(stringLiteral: command.title),
|
||||
subtitle: LocalizedStringResource(stringLiteral: command.description),
|
||||
)
|
||||
}
|
||||
|
||||
static var defaultQuery = CommandQuery()
|
||||
|
||||
init(_ command: Ghostty.Command, for terminal: TerminalEntity) {
|
||||
self.id = .init(terminalId: terminal.id, actionKey: command.actionKey)
|
||||
self.command = command
|
||||
self.title = command.title
|
||||
self.description = command.description
|
||||
self.action = command.action
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
extension CommandEntity.ID: RawRepresentable {
|
||||
var rawValue: String {
|
||||
return "\(terminalId):\(actionKey)"
|
||||
}
|
||||
|
||||
init?(rawValue: String) {
|
||||
let components = rawValue.split(separator: ":", maxSplits: 1)
|
||||
guard components.count == 2 else { return nil }
|
||||
|
||||
guard let terminalId = TerminalEntity.ID(uuidString: String(components[0])) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.terminalId = terminalId
|
||||
self.actionKey = String(components[1])
|
||||
}
|
||||
}
|
||||
|
||||
// Required by AppEntity
|
||||
@available(macOS 14.0, *)
|
||||
extension CommandEntity.ID: EntityIdentifierConvertible {
|
||||
static func entityIdentifier(for entityIdentifierString: String) -> CommandEntity.ID? {
|
||||
.init(rawValue: entityIdentifierString)
|
||||
}
|
||||
|
||||
var entityIdentifierString: String {
|
||||
rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: EntityQuery
|
||||
|
||||
@available(macOS 14.0, *)
|
||||
struct CommandQuery: EntityQuery {
|
||||
// Inject our terminal parameter from our command palette intent.
|
||||
@IntentParameterDependency<CommandPaletteIntent>(\.$terminal)
|
||||
var commandPaletteIntent
|
||||
|
||||
@MainActor
|
||||
func entities(for identifiers: [CommandEntity.ID]) async throws -> [CommandEntity] {
|
||||
// Extract unique terminal IDs to avoid fetching duplicates
|
||||
let terminalIds = Set(identifiers.map(\.terminalId))
|
||||
let terminals = try await TerminalEntity.defaultQuery.entities(for: Array(terminalIds))
|
||||
|
||||
// Build a cache of terminals and their available commands
|
||||
// This avoids repeated command fetching for the same terminal
|
||||
typealias Tuple = (terminal: TerminalEntity, commands: [Ghostty.Command])
|
||||
let commandMap: [TerminalEntity.ID: Tuple] =
|
||||
terminals.reduce(into: [:]) { result, terminal in
|
||||
guard let commands = try? terminal.surfaceModel?.commands() else { return }
|
||||
result[terminal.id] = (terminal: terminal, commands: commands)
|
||||
}
|
||||
|
||||
// Map each identifier to its corresponding CommandEntity. If a command doesn't
|
||||
// exist it maps to nil and is removed via compactMap.
|
||||
return identifiers.compactMap { id in
|
||||
guard let (terminal, commands) = commandMap[id.terminalId],
|
||||
let command = commands.first(where: { $0.actionKey == id.actionKey }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return CommandEntity(command, for: terminal)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func suggestedEntities() async throws -> [CommandEntity] {
|
||||
guard let terminal = commandPaletteIntent?.terminal,
|
||||
let surface = terminal.surfaceModel else { return [] }
|
||||
return try surface.commands().map { CommandEntity($0, for: terminal) }
|
||||
}
|
||||
}
|
139
macos/Sources/Features/App Intents/Entities/TerminalEntity.swift
Normal file
@ -0,0 +1,139 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
|
||||
struct TerminalEntity: AppEntity {
|
||||
let id: UUID
|
||||
|
||||
@Property(title: "Title")
|
||||
var title: String
|
||||
|
||||
@Property(title: "Working Directory")
|
||||
var workingDirectory: String?
|
||||
|
||||
@Property(title: "Kind")
|
||||
var kind: Kind
|
||||
|
||||
@MainActor
|
||||
@DeferredProperty(title: "Full Contents")
|
||||
@available(macOS 26.0, *)
|
||||
var screenContents: String? {
|
||||
get async {
|
||||
guard let surfaceView else { return nil }
|
||||
return surfaceView.cachedScreenContents.get()
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@DeferredProperty(title: "Visible Contents")
|
||||
@available(macOS 26.0, *)
|
||||
var visibleContents: String? {
|
||||
get async {
|
||||
guard let surfaceView else { return nil }
|
||||
return surfaceView.cachedVisibleContents.get()
|
||||
}
|
||||
}
|
||||
|
||||
var screenshot: Image?
|
||||
|
||||
static var typeDisplayRepresentation: TypeDisplayRepresentation {
|
||||
TypeDisplayRepresentation(name: "Terminal")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var displayRepresentation: DisplayRepresentation {
|
||||
var rep = DisplayRepresentation(title: "\(title)")
|
||||
if let screenshot,
|
||||
let nsImage = ImageRenderer(content: screenshot).nsImage,
|
||||
let data = nsImage.tiffRepresentation {
|
||||
rep.image = .init(data: data)
|
||||
}
|
||||
|
||||
return rep
|
||||
}
|
||||
|
||||
/// Returns the view associated with this entity. This may no longer exist.
|
||||
@MainActor
|
||||
var surfaceView: Ghostty.SurfaceView? {
|
||||
Self.defaultQuery.all.first { $0.uuid == self.id }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var surfaceModel: Ghostty.Surface? {
|
||||
surfaceView?.surfaceModel
|
||||
}
|
||||
|
||||
static var defaultQuery = TerminalQuery()
|
||||
|
||||
init(_ view: Ghostty.SurfaceView) {
|
||||
self.id = view.uuid
|
||||
self.title = view.title
|
||||
self.workingDirectory = view.pwd
|
||||
self.screenshot = view.screenshot()
|
||||
|
||||
// Determine the kind based on the window controller type
|
||||
if view.window?.windowController is QuickTerminalController {
|
||||
self.kind = .quick
|
||||
} else {
|
||||
self.kind = .normal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension TerminalEntity {
|
||||
enum Kind: String, AppEnum {
|
||||
case normal
|
||||
case quick
|
||||
|
||||
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Kind")
|
||||
|
||||
static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
|
||||
.normal: .init(title: "Normal"),
|
||||
.quick: .init(title: "Quick")
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
struct TerminalQuery: EntityStringQuery, EnumerableEntityQuery {
|
||||
@MainActor
|
||||
func entities(for identifiers: [TerminalEntity.ID]) async throws -> [TerminalEntity] {
|
||||
return all.filter {
|
||||
identifiers.contains($0.uuid)
|
||||
}.map {
|
||||
TerminalEntity($0)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func entities(matching string: String) async throws -> [TerminalEntity] {
|
||||
return all.filter {
|
||||
$0.title.localizedCaseInsensitiveContains(string)
|
||||
}.map {
|
||||
TerminalEntity($0)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func allEntities() async throws -> [TerminalEntity] {
|
||||
return all.map { TerminalEntity($0) }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func suggestedEntities() async throws -> [TerminalEntity] {
|
||||
return try await allEntities()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var all: [Ghostty.SurfaceView] {
|
||||
// Find all of our terminal windows. This will include the quick terminal
|
||||
// but only if it was previously opened.
|
||||
let controllers = NSApp.windows.compactMap {
|
||||
$0.windowController as? BaseTerminalController
|
||||
}
|
||||
|
||||
// Get all our surfaces
|
||||
return controllers.flatMap {
|
||||
$0.surfaceTree.root?.leaves() ?? []
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
|
||||
/// App intent that retrieves details about a specific terminal.
|
||||
struct GetTerminalDetailsIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Get Details of Terminal"
|
||||
|
||||
@Parameter(
|
||||
title: "Detail",
|
||||
description: "The detail to extract about a terminal."
|
||||
)
|
||||
var detail: TerminalDetail
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to extract information about."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
|
||||
static var parameterSummary: some ParameterSummary {
|
||||
Summary("Get \(\.$detail) from \(\.$terminal)")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<String?> {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
switch detail {
|
||||
case .title: return .result(value: terminal.title)
|
||||
case .workingDirectory: return .result(value: terminal.workingDirectory)
|
||||
case .allContents:
|
||||
guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound }
|
||||
return .result(value: view.cachedScreenContents.get())
|
||||
case .selectedText:
|
||||
guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound }
|
||||
return .result(value: view.accessibilitySelectedText())
|
||||
case .visibleText:
|
||||
guard let view = terminal.surfaceView else { throw GhosttyIntentError.surfaceNotFound }
|
||||
return .result(value: view.cachedVisibleContents.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: TerminalDetail
|
||||
|
||||
enum TerminalDetail: String {
|
||||
case title
|
||||
case workingDirectory
|
||||
case allContents
|
||||
case selectedText
|
||||
case visibleText
|
||||
}
|
||||
|
||||
extension TerminalDetail: AppEnum {
|
||||
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Detail")
|
||||
|
||||
static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
|
||||
.title: .init(title: "Title"),
|
||||
.workingDirectory: .init(title: "Working Directory"),
|
||||
.allContents: .init(title: "Full Contents"),
|
||||
.selectedText: .init(title: "Selected Text"),
|
||||
.visibleText: .init(title: "Visible Text"),
|
||||
]
|
||||
}
|
13
macos/Sources/Features/App Intents/GhosttyIntentError.swift
Normal file
@ -0,0 +1,13 @@
|
||||
enum GhosttyIntentError: Error, CustomLocalizedStringResourceConvertible {
|
||||
case appUnavailable
|
||||
case surfaceNotFound
|
||||
case permissionDenied
|
||||
|
||||
var localizedStringResource: LocalizedStringResource {
|
||||
switch self {
|
||||
case .appUnavailable: "The Ghostty app isn't properly initialized."
|
||||
case .surfaceNotFound: "The terminal no longer exists."
|
||||
case .permissionDenied: "Ghostty doesn't allow Shortcuts."
|
||||
}
|
||||
}
|
||||
}
|
317
macos/Sources/Features/App Intents/InputIntent.swift
Normal file
@ -0,0 +1,317 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
|
||||
/// App intent to input text in a terminal.
|
||||
struct InputTextIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Input Text to Terminal"
|
||||
|
||||
@Parameter(
|
||||
title: "Text",
|
||||
description: "The text to input to the terminal. The text will be inputted as if it was pasted.",
|
||||
inputOptions: String.IntentInputOptions(
|
||||
capitalizationType: .none,
|
||||
multiline: true,
|
||||
autocorrect: false,
|
||||
smartQuotes: false,
|
||||
smartDashes: false
|
||||
)
|
||||
)
|
||||
var text: String
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to scope this action to."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
surface.sendText(text)
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
/// App intent to trigger a keyboard event.
|
||||
struct KeyEventIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Send Keyboard Event to Terminal"
|
||||
static var description = IntentDescription("Simulate a keyboard event. This will not handle text encoding; use the 'Input Text' action for that.")
|
||||
|
||||
@Parameter(
|
||||
title: "Key",
|
||||
description: "The key to send to the terminal.",
|
||||
default: .enter
|
||||
)
|
||||
var key: Ghostty.Input.Key
|
||||
|
||||
@Parameter(
|
||||
title: "Modifier(s)",
|
||||
description: "The modifiers to send with the key event.",
|
||||
default: []
|
||||
)
|
||||
var mods: [KeyEventMods]
|
||||
|
||||
@Parameter(
|
||||
title: "Event Type",
|
||||
description: "A key press or release.",
|
||||
default: .press
|
||||
)
|
||||
var action: Ghostty.Input.Action
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to scope this action to."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
// Convert KeyEventMods array to Ghostty.Input.Mods
|
||||
let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
|
||||
result.union(mod.ghosttyMod)
|
||||
}
|
||||
|
||||
let keyEvent = Ghostty.Input.KeyEvent(
|
||||
key: key,
|
||||
action: action,
|
||||
mods: ghosttyMods
|
||||
)
|
||||
surface.sendKeyEvent(keyEvent)
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: MouseButtonIntent
|
||||
|
||||
/// App intent to trigger a mouse button event.
|
||||
struct MouseButtonIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Send Mouse Button Event to Terminal"
|
||||
|
||||
@Parameter(
|
||||
title: "Button",
|
||||
description: "The mouse button to press or release.",
|
||||
default: .left
|
||||
)
|
||||
var button: Ghostty.Input.MouseButton
|
||||
|
||||
@Parameter(
|
||||
title: "Action",
|
||||
description: "Whether to press or release the button.",
|
||||
default: .press
|
||||
)
|
||||
var action: Ghostty.Input.MouseState
|
||||
|
||||
@Parameter(
|
||||
title: "Modifier(s)",
|
||||
description: "The modifiers to send with the mouse event.",
|
||||
default: []
|
||||
)
|
||||
var mods: [KeyEventMods]
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to scope this action to."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
// Convert KeyEventMods array to Ghostty.Input.Mods
|
||||
let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
|
||||
result.union(mod.ghosttyMod)
|
||||
}
|
||||
|
||||
let mouseEvent = Ghostty.Input.MouseButtonEvent(
|
||||
action: action,
|
||||
button: button,
|
||||
mods: ghosttyMods
|
||||
)
|
||||
surface.sendMouseButton(mouseEvent)
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
/// App intent to send a mouse position event.
|
||||
struct MousePosIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Send Mouse Position Event to Terminal"
|
||||
static var description = IntentDescription("Send a mouse position event to the terminal. This reports the cursor position for mouse tracking.")
|
||||
|
||||
@Parameter(
|
||||
title: "X Position",
|
||||
description: "The horizontal position of the mouse cursor in pixels.",
|
||||
default: 0
|
||||
)
|
||||
var x: Double
|
||||
|
||||
@Parameter(
|
||||
title: "Y Position",
|
||||
description: "The vertical position of the mouse cursor in pixels.",
|
||||
default: 0
|
||||
)
|
||||
var y: Double
|
||||
|
||||
@Parameter(
|
||||
title: "Modifier(s)",
|
||||
description: "The modifiers to send with the mouse position event.",
|
||||
default: []
|
||||
)
|
||||
var mods: [KeyEventMods]
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to scope this action to."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
// Convert KeyEventMods array to Ghostty.Input.Mods
|
||||
let ghosttyMods = mods.reduce(Ghostty.Input.Mods()) { result, mod in
|
||||
result.union(mod.ghosttyMod)
|
||||
}
|
||||
|
||||
let mousePosEvent = Ghostty.Input.MousePosEvent(
|
||||
x: x,
|
||||
y: y,
|
||||
mods: ghosttyMods
|
||||
)
|
||||
surface.sendMousePos(mousePosEvent)
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
/// App intent to send a mouse scroll event.
|
||||
struct MouseScrollIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Send Mouse Scroll Event to Terminal"
|
||||
static var description = IntentDescription("Send a mouse scroll event to the terminal with configurable precision and momentum.")
|
||||
|
||||
@Parameter(
|
||||
title: "X Scroll Delta",
|
||||
description: "The horizontal scroll amount.",
|
||||
default: 0
|
||||
)
|
||||
var x: Double
|
||||
|
||||
@Parameter(
|
||||
title: "Y Scroll Delta",
|
||||
description: "The vertical scroll amount.",
|
||||
default: 0
|
||||
)
|
||||
var y: Double
|
||||
|
||||
@Parameter(
|
||||
title: "High Precision",
|
||||
description: "Whether this is a high-precision scroll event (e.g., from trackpad).",
|
||||
default: false
|
||||
)
|
||||
var precision: Bool
|
||||
|
||||
@Parameter(
|
||||
title: "Momentum Phase",
|
||||
description: "The momentum phase for inertial scrolling.",
|
||||
default: Ghostty.Input.Momentum.none
|
||||
)
|
||||
var momentum: Ghostty.Input.Momentum
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to scope this action to."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
let scrollEvent = Ghostty.Input.MouseScrollEvent(
|
||||
x: x,
|
||||
y: y,
|
||||
mods: .init(precision: precision, momentum: momentum)
|
||||
)
|
||||
surface.sendMouseScroll(scrollEvent)
|
||||
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Mods
|
||||
|
||||
enum KeyEventMods: String, AppEnum, CaseIterable {
|
||||
case shift
|
||||
case control
|
||||
case option
|
||||
case command
|
||||
|
||||
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Modifier Key")
|
||||
|
||||
static var caseDisplayRepresentations: [KeyEventMods : DisplayRepresentation] = [
|
||||
.shift: "Shift",
|
||||
.control: "Control",
|
||||
.option: "Option",
|
||||
.command: "Command"
|
||||
]
|
||||
|
||||
var ghosttyMod: Ghostty.Input.Mods {
|
||||
switch self {
|
||||
case .shift: .shift
|
||||
case .control: .ctrl
|
||||
case .option: .alt
|
||||
case .command: .super
|
||||
}
|
||||
}
|
||||
}
|
57
macos/Sources/Features/App Intents/IntentPermission.swift
Normal file
@ -0,0 +1,57 @@
|
||||
import AppKit
|
||||
|
||||
/// Requests permission for Shortcuts app to interact with Ghostty
|
||||
///
|
||||
/// This function displays a permission dialog asking the user to allow Shortcuts
|
||||
/// to interact with Ghostty. The permission is automatically cached for 10 minutes
|
||||
/// if the user selects "Allow", meaning subsequent intent calls won't show the dialog
|
||||
/// again during that time period.
|
||||
///
|
||||
/// The permission uses a shared UserDefaults key across all intents, so granting
|
||||
/// permission for one intent allows all Ghostty intents to execute without additional
|
||||
/// prompts for the duration of the cache period.
|
||||
///
|
||||
/// - Returns: `true` if permission is granted, `false` if denied
|
||||
///
|
||||
/// ## Usage
|
||||
/// Add this check at the beginning of any App Intent's `perform()` method:
|
||||
/// ```swift
|
||||
/// @MainActor
|
||||
/// func perform() async throws -> some IntentResult {
|
||||
/// guard await requestIntentPermission() else {
|
||||
/// throw GhosttyIntentError.permissionDenied
|
||||
/// }
|
||||
/// // ... continue with intent implementation
|
||||
/// }
|
||||
/// ```
|
||||
func requestIntentPermission() async -> Bool {
|
||||
await withCheckedContinuation { continuation in
|
||||
Task { @MainActor in
|
||||
if let delegate = NSApp.delegate as? AppDelegate {
|
||||
switch (delegate.ghostty.config.macosShortcuts) {
|
||||
case .allow:
|
||||
continuation.resume(returning: true)
|
||||
return
|
||||
|
||||
case .deny:
|
||||
continuation.resume(returning: false)
|
||||
return
|
||||
|
||||
case .ask:
|
||||
// Continue with the permission dialog
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PermissionRequest.show(
|
||||
"com.mitchellh.ghostty.shortcutsPermission",
|
||||
message: "Allow Shortcuts to interact with Ghostty?",
|
||||
allowDuration: .forever,
|
||||
rememberDuration: nil,
|
||||
) { response in
|
||||
continuation.resume(returning: response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
35
macos/Sources/Features/App Intents/KeybindIntent.swift
Normal file
@ -0,0 +1,35 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
|
||||
struct KeybindIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Invoke a Keybind Action"
|
||||
|
||||
@Parameter(
|
||||
title: "Terminal",
|
||||
description: "The terminal to invoke the action on."
|
||||
)
|
||||
var terminal: TerminalEntity
|
||||
|
||||
@Parameter(
|
||||
title: "Action",
|
||||
description: "The keybind action to invoke. This can be any valid keybind action you could put in a configuration file."
|
||||
)
|
||||
var action: String
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = [.background, .foreground]
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<Bool> {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let surface = terminal.surfaceModel else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
let performed = surface.perform(action: action)
|
||||
return .result(value: performed)
|
||||
}
|
||||
}
|
168
macos/Sources/Features/App Intents/NewTerminalIntent.swift
Normal file
@ -0,0 +1,168 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
import GhosttyKit
|
||||
|
||||
/// App intent that allows creating a new terminal window or tab.
|
||||
///
|
||||
/// This requires macOS 15 or greater because we use features of macOS 15 here.
|
||||
@available(macOS 15.0, *)
|
||||
struct NewTerminalIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "New Terminal"
|
||||
static var description = IntentDescription("Create a new terminal.")
|
||||
|
||||
@Parameter(
|
||||
title: "Location",
|
||||
description: "The location that the terminal should be created.",
|
||||
default: .window
|
||||
)
|
||||
var location: NewTerminalLocation
|
||||
|
||||
@Parameter(
|
||||
title: "Command",
|
||||
description: "Command to execute within your configured shell.",
|
||||
)
|
||||
var command: String?
|
||||
|
||||
@Parameter(
|
||||
title: "Working Directory",
|
||||
description: "The working directory to open in the terminal.",
|
||||
supportedContentTypes: [.folder]
|
||||
)
|
||||
var workingDirectory: IntentFile?
|
||||
|
||||
@Parameter(
|
||||
title: "Environment Variables",
|
||||
description: "Environment variables in `KEY=VALUE` format.",
|
||||
default: []
|
||||
)
|
||||
var env: [String]
|
||||
|
||||
@Parameter(
|
||||
title: "Parent Terminal",
|
||||
description: "The terminal to inherit the base configuration from."
|
||||
)
|
||||
var parent: TerminalEntity?
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .foreground(.immediate)
|
||||
|
||||
@available(macOS, obsoleted: 26.0, message: "Replaced by supportedModes")
|
||||
static var openAppWhenRun = true
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<TerminalEntity?> {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
guard let appDelegate = NSApp.delegate as? AppDelegate else {
|
||||
throw GhosttyIntentError.appUnavailable
|
||||
}
|
||||
let ghostty = appDelegate.ghostty
|
||||
|
||||
var config = Ghostty.SurfaceConfiguration()
|
||||
|
||||
// We don't run command as "command" and instead use "initialInput" so
|
||||
// that we can get all the login scripts to setup things like PATH.
|
||||
if let command {
|
||||
config.initialInput = "\(command); exit\n"
|
||||
}
|
||||
|
||||
// If we were given a working directory then open that directory
|
||||
if let url = workingDirectory?.fileURL {
|
||||
let dir = url.hasDirectoryPath ? url : url.deletingLastPathComponent()
|
||||
config.workingDirectory = dir.path(percentEncoded: false)
|
||||
}
|
||||
|
||||
// Parse environment variables from KEY=VALUE format
|
||||
for envVar in env {
|
||||
if let separatorIndex = envVar.firstIndex(of: "=") {
|
||||
let key = String(envVar[..<separatorIndex])
|
||||
let value = String(envVar[envVar.index(after: separatorIndex)...])
|
||||
config.environmentVariables[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if we have a parent and get it
|
||||
let parent: Ghostty.SurfaceView?
|
||||
if let parentParam = self.parent {
|
||||
guard let view = parentParam.surfaceView else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
parent = view
|
||||
} else if let preferred = TerminalController.preferredParent {
|
||||
parent = preferred.focusedSurface ?? preferred.surfaceTree.root?.leftmostLeaf()
|
||||
} else {
|
||||
parent = nil
|
||||
}
|
||||
|
||||
switch location {
|
||||
case .window:
|
||||
let newController = TerminalController.newWindow(
|
||||
ghostty,
|
||||
withBaseConfig: config,
|
||||
withParent: parent?.window)
|
||||
if let view = newController.surfaceTree.root?.leftmostLeaf() {
|
||||
return .result(value: TerminalEntity(view))
|
||||
}
|
||||
|
||||
case .tab:
|
||||
let newController = TerminalController.newTab(
|
||||
ghostty,
|
||||
from: parent?.window,
|
||||
withBaseConfig: config)
|
||||
if let view = newController?.surfaceTree.root?.leftmostLeaf() {
|
||||
return .result(value: TerminalEntity(view))
|
||||
}
|
||||
|
||||
case .splitLeft, .splitRight, .splitUp, .splitDown:
|
||||
guard let parent,
|
||||
let controller = parent.window?.windowController as? BaseTerminalController else {
|
||||
throw GhosttyIntentError.surfaceNotFound
|
||||
}
|
||||
|
||||
if let view = controller.newSplit(
|
||||
at: parent,
|
||||
direction: location.splitDirection!
|
||||
) {
|
||||
return .result(value: TerminalEntity(view))
|
||||
}
|
||||
}
|
||||
|
||||
return .result(value: .none)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: NewTerminalLocation
|
||||
|
||||
enum NewTerminalLocation: String {
|
||||
case tab
|
||||
case window
|
||||
case splitLeft = "split:left"
|
||||
case splitRight = "split:right"
|
||||
case splitUp = "split:up"
|
||||
case splitDown = "split:down"
|
||||
|
||||
var splitDirection: SplitTree<Ghostty.SurfaceView>.NewDirection? {
|
||||
switch self {
|
||||
case .splitLeft: return .left
|
||||
case .splitRight: return .right
|
||||
case .splitUp: return .up
|
||||
case .splitDown: return .down
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NewTerminalLocation: AppEnum {
|
||||
static var typeDisplayRepresentation = TypeDisplayRepresentation(name: "Terminal Location")
|
||||
|
||||
static var caseDisplayRepresentations: [Self: DisplayRepresentation] = [
|
||||
.tab: .init(title: "Tab"),
|
||||
.window: .init(title: "Window"),
|
||||
.splitLeft: .init(title: "Split Left"),
|
||||
.splitRight: .init(title: "Split Right"),
|
||||
.splitUp: .init(title: "Split Up"),
|
||||
.splitDown: .init(title: "Split Down"),
|
||||
]
|
||||
}
|
32
macos/Sources/Features/App Intents/QuickTerminalIntent.swift
Normal file
@ -0,0 +1,32 @@
|
||||
import AppKit
|
||||
import AppIntents
|
||||
|
||||
struct QuickTerminalIntent: AppIntent {
|
||||
static var title: LocalizedStringResource = "Open the Quick Terminal"
|
||||
static var description = IntentDescription("Open the Quick Terminal. If it is already open, then do nothing.")
|
||||
|
||||
@available(macOS 26.0, *)
|
||||
static var supportedModes: IntentModes = .background
|
||||
|
||||
@MainActor
|
||||
func perform() async throws -> some IntentResult & ReturnsValue<[TerminalEntity]> {
|
||||
guard await requestIntentPermission() else {
|
||||
throw GhosttyIntentError.permissionDenied
|
||||
}
|
||||
|
||||
guard let delegate = NSApp.delegate as? AppDelegate else {
|
||||
throw GhosttyIntentError.appUnavailable
|
||||
}
|
||||
|
||||
// This is safe to call even if it is already shown.
|
||||
let c = delegate.quickController
|
||||
c.animateIn()
|
||||
|
||||
// Grab all our terminals
|
||||
let terminals = c.surfaceTree.root?.leaves().map {
|
||||
TerminalEntity($0)
|
||||
} ?? []
|
||||
|
||||
return .result(value: terminals)
|
||||
}
|
||||
}
|
@ -4,12 +4,26 @@ extension View {
|
||||
/// Returns the ghostty icon to use for views.
|
||||
func ghosttyIconImage() -> Image {
|
||||
#if os(macOS)
|
||||
// If we have a specific icon set, then use that
|
||||
if let delegate = NSApplication.shared.delegate as? AppDelegate,
|
||||
let nsImage = delegate.appIcon {
|
||||
return Image(nsImage: nsImage)
|
||||
}
|
||||
|
||||
// Grab the icon from the running application. This is the best way
|
||||
// I've found so far to get the proper icon for our current icon
|
||||
// tinting and so on with macOS Tahoe
|
||||
if let icon = NSRunningApplication.current.icon {
|
||||
return Image(nsImage: icon)
|
||||
}
|
||||
|
||||
// Get our defined application icon image.
|
||||
if let nsImage = NSApp.applicationIconImage {
|
||||
return Image(nsImage: nsImage)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Fall back to a static representation
|
||||
return Image("AppIconImage")
|
||||
}
|
||||
}
|
||||
|
@ -17,33 +17,19 @@ struct TerminalCommandPaletteView: View {
|
||||
|
||||
// The commands available to the command palette.
|
||||
private var commandOptions: [CommandOption] {
|
||||
guard let surface = surfaceView.surface else { return [] }
|
||||
|
||||
var ptr: UnsafeMutablePointer<ghostty_command_s>? = nil
|
||||
var count: Int = 0
|
||||
ghostty_surface_commands(surface, &ptr, &count)
|
||||
guard let ptr else { return [] }
|
||||
|
||||
let buffer = UnsafeBufferPointer(start: ptr, count: count)
|
||||
return Array(buffer).filter { c in
|
||||
let key = String(cString: c.action_key)
|
||||
switch (key) {
|
||||
case "toggle_tab_overview",
|
||||
"toggle_window_decorations",
|
||||
"show_gtk_inspector":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}.map { c in
|
||||
let action = String(cString: c.action)
|
||||
return CommandOption(
|
||||
title: String(cString: c.title),
|
||||
description: String(cString: c.description),
|
||||
symbols: ghosttyConfig.keyboardShortcut(for: action)?.keyList
|
||||
) {
|
||||
onAction(action)
|
||||
guard let surface = surfaceView.surfaceModel else { return [] }
|
||||
do {
|
||||
return try surface.commands().map { c in
|
||||
return CommandOption(
|
||||
title: c.title,
|
||||
description: c.description,
|
||||
symbols: ghosttyConfig.keyboardShortcut(for: c.action)?.keyList
|
||||
) {
|
||||
onAction(c.action)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +42,11 @@ class QuickTerminalController: BaseTerminalController {
|
||||
) {
|
||||
self.position = position
|
||||
self.derivedConfig = DerivedConfig(ghostty.config)
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: tree)
|
||||
|
||||
// Important detail here: we initialize with an empty surface tree so
|
||||
// that we don't start a terminal process. This gets started when the
|
||||
// first terminal is shown in `animateIn`.
|
||||
super.init(ghostty, baseConfig: base, surfaceTree: .init())
|
||||
|
||||
// Setup our notifications for behaviors
|
||||
let center = NotificationCenter.default
|
||||
@ -218,19 +222,19 @@ class QuickTerminalController: BaseTerminalController {
|
||||
}
|
||||
}
|
||||
|
||||
override func closeSurfaceNode(
|
||||
override func closeSurface(
|
||||
_ node: SplitTree<Ghostty.SurfaceView>.Node,
|
||||
withConfirmation: Bool = true
|
||||
) {
|
||||
// If this isn't the root then we're dealing with a split closure.
|
||||
if surfaceTree.root != node {
|
||||
super.closeSurfaceNode(node, withConfirmation: withConfirmation)
|
||||
super.closeSurface(node, withConfirmation: withConfirmation)
|
||||
return
|
||||
}
|
||||
|
||||
// If this isn't a final leaf then we're dealing with a split closure
|
||||
guard case .leaf(let surface) = node else {
|
||||
super.closeSurfaceNode(node, withConfirmation: withConfirmation)
|
||||
super.closeSurface(node, withConfirmation: withConfirmation)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -193,6 +193,46 @@ class BaseTerminalController: NSWindowController,
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
/// Create a new split.
|
||||
@discardableResult
|
||||
func newSplit(
|
||||
at oldView: Ghostty.SurfaceView,
|
||||
direction: SplitTree<Ghostty.SurfaceView>.NewDirection,
|
||||
baseConfig config: Ghostty.SurfaceConfiguration? = nil
|
||||
) -> Ghostty.SurfaceView? {
|
||||
// We can only create new splits for surfaces in our tree.
|
||||
guard surfaceTree.root?.node(view: oldView) != nil else { return nil }
|
||||
|
||||
// Create a new surface view
|
||||
guard let ghostty_app = ghostty.app else { return nil }
|
||||
let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
|
||||
|
||||
// Do the split
|
||||
let newTree: SplitTree<Ghostty.SurfaceView>
|
||||
do {
|
||||
newTree = try surfaceTree.insert(
|
||||
view: newView,
|
||||
at: oldView,
|
||||
direction: direction)
|
||||
} catch {
|
||||
// If splitting fails for any reason (it should not), then we just log
|
||||
// and return. The new view we created will be deinitialized and its
|
||||
// no big deal.
|
||||
Ghostty.logger.warning("failed to insert split: \(error)")
|
||||
return nil
|
||||
}
|
||||
|
||||
replaceSurfaceTree(
|
||||
newTree,
|
||||
moveFocusTo: newView,
|
||||
moveFocusFrom: oldView,
|
||||
undoAction: "New Split")
|
||||
|
||||
return newView
|
||||
}
|
||||
|
||||
/// Called when the surfaceTree variable changed.
|
||||
///
|
||||
/// Subclasses should call super first.
|
||||
@ -260,6 +300,46 @@ class BaseTerminalController: NSWindowController,
|
||||
self.alert = alert
|
||||
}
|
||||
|
||||
/// Close a surface from a view.
|
||||
func closeSurface(
|
||||
_ view: Ghostty.SurfaceView,
|
||||
withConfirmation: Bool = true
|
||||
) {
|
||||
guard let node = surfaceTree.root?.node(view: view) else { return }
|
||||
closeSurface(node, withConfirmation: withConfirmation)
|
||||
}
|
||||
|
||||
/// Close a surface node (which may contain splits), requesting confirmation if necessary.
|
||||
///
|
||||
/// This will also insert the proper undo stack information in.
|
||||
func closeSurface(
|
||||
_ node: SplitTree<Ghostty.SurfaceView>.Node,
|
||||
withConfirmation: Bool = true
|
||||
) {
|
||||
// This node must be part of our tree
|
||||
guard surfaceTree.contains(node) else { return }
|
||||
|
||||
// If the child process is not alive, then we exit immediately
|
||||
guard withConfirmation else {
|
||||
removeSurfaceNode(node)
|
||||
return
|
||||
}
|
||||
|
||||
// Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog
|
||||
// due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that
|
||||
// confirmationDialog allows the user to Cmd-W close the alert, but when doing
|
||||
// so SwiftUI does not update any of the bindings to note that window is no longer
|
||||
// being shown, and provides no callback to detect this.
|
||||
confirmClose(
|
||||
messageText: "Close Terminal?",
|
||||
informativeText: "The terminal still has a running process. If you close the terminal the process will be killed."
|
||||
) { [weak self] in
|
||||
if let self {
|
||||
self.removeSurfaceNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Split Tree Management
|
||||
|
||||
/// Find the next surface to focus when a node is being closed.
|
||||
@ -420,42 +500,11 @@ class BaseTerminalController: NSWindowController,
|
||||
@objc private func ghosttyDidCloseSurface(_ notification: Notification) {
|
||||
guard let target = notification.object as? Ghostty.SurfaceView else { return }
|
||||
guard let node = surfaceTree.root?.node(view: target) else { return }
|
||||
closeSurfaceNode(
|
||||
closeSurface(
|
||||
node,
|
||||
withConfirmation: (notification.userInfo?["process_alive"] as? Bool) ?? false)
|
||||
}
|
||||
|
||||
/// Close a surface node (which may contain splits), requesting confirmation if necessary.
|
||||
///
|
||||
/// This will also insert the proper undo stack information in.
|
||||
func closeSurfaceNode(
|
||||
_ node: SplitTree<Ghostty.SurfaceView>.Node,
|
||||
withConfirmation: Bool = true
|
||||
) {
|
||||
// This node must be part of our tree
|
||||
guard surfaceTree.contains(node) else { return }
|
||||
|
||||
// If the child process is not alive, then we exit immediately
|
||||
guard withConfirmation else {
|
||||
removeSurfaceNode(node)
|
||||
return
|
||||
}
|
||||
|
||||
// Confirm close. We use an NSAlert instead of a SwiftUI confirmationDialog
|
||||
// due to SwiftUI bugs (see Ghostty #560). To repeat from #560, the bug is that
|
||||
// confirmationDialog allows the user to Cmd-W close the alert, but when doing
|
||||
// so SwiftUI does not update any of the bindings to note that window is no longer
|
||||
// being shown, and provides no callback to detect this.
|
||||
confirmClose(
|
||||
messageText: "Close Terminal?",
|
||||
informativeText: "The terminal still has a running process. If you close the terminal the process will be killed."
|
||||
) { [weak self] in
|
||||
if let self {
|
||||
self.removeSurfaceNode(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidNewSplit(_ notification: Notification) {
|
||||
// The target must be within our tree
|
||||
guard let oldView = notification.object as? Ghostty.SurfaceView else { return }
|
||||
@ -477,30 +526,7 @@ class BaseTerminalController: NSWindowController,
|
||||
default: return
|
||||
}
|
||||
|
||||
// Create a new surface view
|
||||
guard let ghostty_app = ghostty.app else { return }
|
||||
let newView = Ghostty.SurfaceView(ghostty_app, baseConfig: config)
|
||||
|
||||
// Do the split
|
||||
let newTree: SplitTree<Ghostty.SurfaceView>
|
||||
do {
|
||||
newTree = try surfaceTree.insert(
|
||||
view: newView,
|
||||
at: oldView,
|
||||
direction: splitDirection)
|
||||
} catch {
|
||||
// If splitting fails for any reason (it should not), then we just log
|
||||
// and return. The new view we created will be deinitialized and its
|
||||
// no big deal.
|
||||
Ghostty.logger.warning("failed to insert split: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
replaceSurfaceTree(
|
||||
newTree,
|
||||
moveFocusTo: newView,
|
||||
moveFocusFrom: oldView,
|
||||
undoAction: "New Split")
|
||||
newSplit(at: oldView, direction: splitDirection, baseConfig: config)
|
||||
}
|
||||
|
||||
@objc private func ghosttyDidEqualizeSplits(_ notification: Notification) {
|
||||
|
@ -5,18 +5,25 @@ import Combine
|
||||
import GhosttyKit
|
||||
|
||||
/// A classic, tabbed terminal experience.
|
||||
class TerminalController: BaseTerminalController {
|
||||
class TerminalController: BaseTerminalController, TabGroupCloseCoordinator.Controller {
|
||||
override var windowNibName: NSNib.Name? {
|
||||
let defaultValue = "Terminal"
|
||||
|
||||
guard let appDelegate = NSApp.delegate as? AppDelegate else { return defaultValue }
|
||||
let config = appDelegate.ghostty.config
|
||||
|
||||
// If we have no window decorations, there's no reason to do anything but
|
||||
// the default titlebar (because there will be no titlebar).
|
||||
if !config.windowDecorations {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
let nib = switch config.macosTitlebarStyle {
|
||||
case "native": "Terminal"
|
||||
case "hidden": "TerminalHiddenTitlebar"
|
||||
case "transparent": "TerminalTransparentTitlebar"
|
||||
case "tabs":
|
||||
if #available(macOS 26.0, *), hasLiquidGlass() {
|
||||
if #available(macOS 26.0, *) {
|
||||
"TerminalTabsTitlebarTahoe"
|
||||
} else {
|
||||
"TerminalTabsTitlebarVentura"
|
||||
@ -169,7 +176,7 @@ class TerminalController: BaseTerminalController {
|
||||
private static var lastCascadePoint = NSPoint(x: 0, y: 0)
|
||||
|
||||
// The preferred parent terminal controller.
|
||||
private static var preferredParent: TerminalController? {
|
||||
static var preferredParent: TerminalController? {
|
||||
all.first {
|
||||
$0.window?.isMainWindow ?? false
|
||||
} ?? all.last
|
||||
@ -519,13 +526,13 @@ class TerminalController: BaseTerminalController {
|
||||
}
|
||||
|
||||
/// This is called anytime a node in the surface tree is being removed.
|
||||
override func closeSurfaceNode(
|
||||
override func closeSurface(
|
||||
_ node: SplitTree<Ghostty.SurfaceView>.Node,
|
||||
withConfirmation: Bool = true
|
||||
) {
|
||||
// If this isn't the root then we're dealing with a split closure.
|
||||
if surfaceTree.root != node {
|
||||
super.closeSurfaceNode(node, withConfirmation: withConfirmation)
|
||||
super.closeSurface(node, withConfirmation: withConfirmation)
|
||||
return
|
||||
}
|
||||
|
||||
@ -882,14 +889,20 @@ class TerminalController: BaseTerminalController {
|
||||
ghostty.newTab(surface: surface)
|
||||
}
|
||||
|
||||
//MARK: - NSWindowDelegate
|
||||
// MARK: NSWindowDelegate
|
||||
|
||||
// TabGroupCloseCoordinator.Controller
|
||||
lazy private(set) var tabGroupCloseCoordinator = TabGroupCloseCoordinator()
|
||||
|
||||
override func windowShouldClose(_ sender: NSWindow) -> Bool {
|
||||
// If we have tabs, then this should only close the tab.
|
||||
if window?.tabGroup?.windows.count ?? 0 > 1 {
|
||||
closeTab(sender)
|
||||
} else {
|
||||
closeWindow(sender)
|
||||
tabGroupCloseCoordinator.windowShouldClose(sender) { [weak self] scope in
|
||||
guard let self else { return }
|
||||
switch (scope) {
|
||||
case .tab: closeTab(nil)
|
||||
case .window:
|
||||
guard self.window?.isFirstWindowInTabGroup ?? false else { return }
|
||||
closeWindow(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// We will always explicitly close the window using the above
|
||||
@ -1001,20 +1014,14 @@ class TerminalController: BaseTerminalController {
|
||||
|
||||
@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.
|
||||
closeWindowImmediately()
|
||||
return
|
||||
}
|
||||
|
||||
// If have one window then we just do a normal close
|
||||
if tabGroup.windows.count == 1 {
|
||||
closeWindowImmediately()
|
||||
return
|
||||
}
|
||||
// We need to check all the windows in our tab group for confirmation
|
||||
// if we're closing the window. If we don't have a tabgroup for any
|
||||
// reason we check ourselves.
|
||||
let windows: [NSWindow] = window.tabGroup?.windows ?? [window]
|
||||
|
||||
// Check if any windows require close confirmation.
|
||||
let needsConfirm = tabGroup.windows.contains { tabWindow in
|
||||
let needsConfirm = windows.contains { tabWindow in
|
||||
guard let controller = tabWindow.windowController as? TerminalController else {
|
||||
return false
|
||||
}
|
||||
@ -1270,4 +1277,3 @@ extension TerminalController: NSMenuItemValidation {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,16 +58,19 @@ class TerminalWindow: NSWindow {
|
||||
hideWindowButtons()
|
||||
}
|
||||
|
||||
// Create our reset zoom titlebar accessory.
|
||||
resetZoomAccessory.layoutAttribute = .right
|
||||
resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView(
|
||||
viewModel: viewModel,
|
||||
action: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.terminalController?.splitZoom(self)
|
||||
}))
|
||||
addTitlebarAccessoryViewController(resetZoomAccessory)
|
||||
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
// Create our reset zoom titlebar accessory. We have to have a title
|
||||
// to do this or AppKit triggers an assertion.
|
||||
if styleMask.contains(.titled) {
|
||||
resetZoomAccessory.layoutAttribute = .right
|
||||
resetZoomAccessory.view = NSHostingView(rootView: ResetZoomAccessoryView(
|
||||
viewModel: viewModel,
|
||||
action: { [weak self] in
|
||||
guard let self else { return }
|
||||
self.terminalController?.splitZoom(self)
|
||||
}))
|
||||
addTitlebarAccessoryViewController(resetZoomAccessory)
|
||||
resetZoomAccessory.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
// Setup the accessory view for tabs that shows our keyboard shortcuts,
|
||||
// zoomed state, etc. Note I tried to use SwiftUI here but ran into issues
|
||||
@ -447,7 +450,7 @@ extension TerminalWindow {
|
||||
// The padding from the top that the view appears. This was all just manually
|
||||
// measured based on the OS.
|
||||
var topPadding: CGFloat {
|
||||
if #available(macOS 26.0, *), hasLiquidGlass() {
|
||||
if #available(macOS 26.0, *) {
|
||||
return viewModel.hasToolbar ? 10 : 5
|
||||
} else {
|
||||
return viewModel.hasToolbar ? 9 : 4
|
||||
|
@ -45,11 +45,13 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
||||
|
||||
override func update() {
|
||||
super.update()
|
||||
|
||||
|
||||
// On macOS 13 to 15, we need to hide the NSVisualEffectView in order to allow our
|
||||
// titlebar to be truly transparent.
|
||||
if !effectViewIsHidden && !hasLiquidGlass() {
|
||||
hideEffectView()
|
||||
if #unavailable(macOS 26) {
|
||||
if !effectViewIsHidden {
|
||||
hideEffectView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +67,7 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
||||
// references changed (e.g. tabGroup is new).
|
||||
setupKVO()
|
||||
|
||||
if #available(macOS 26.0, *), hasLiquidGlass() {
|
||||
if #available(macOS 26.0, *) {
|
||||
syncAppearanceTahoe(surfaceConfig)
|
||||
} else {
|
||||
syncAppearanceVentura(surfaceConfig)
|
||||
|
@ -1,3 +0,0 @@
|
||||
enum AppError: Error {
|
||||
case surfaceCreateError
|
||||
}
|
46
macos/Sources/Ghostty/Ghostty.Command.swift
Normal file
@ -0,0 +1,46 @@
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// `ghostty_command_s`
|
||||
struct Command: Sendable {
|
||||
private let cValue: ghostty_command_s
|
||||
|
||||
/// The title of the command.
|
||||
var title: String {
|
||||
String(cString: cValue.title)
|
||||
}
|
||||
|
||||
/// Human-friendly description of what this command will do.
|
||||
var description: String {
|
||||
String(cString: cValue.description)
|
||||
}
|
||||
|
||||
/// The full action that must be performed to invoke this command.
|
||||
var action: String {
|
||||
String(cString: cValue.action)
|
||||
}
|
||||
|
||||
/// Only the key portion of the action so you can compare action types, e.g. `goto_split`
|
||||
/// instead of `goto_split:left`.
|
||||
var actionKey: String {
|
||||
String(cString: cValue.action_key)
|
||||
}
|
||||
|
||||
/// True if this can be performed on this target.
|
||||
var isSupported: Bool {
|
||||
!Self.unsupportedActionKeys.contains(actionKey)
|
||||
}
|
||||
|
||||
/// Unsupported action keys, because they either don't make sense in the context of our
|
||||
/// target platform or they just aren't implemented yet.
|
||||
static let unsupportedActionKeys: [String] = [
|
||||
"toggle_tab_overview",
|
||||
"toggle_window_decorations",
|
||||
"show_gtk_inspector",
|
||||
]
|
||||
|
||||
init(cValue: ghostty_command_s) {
|
||||
self.cValue = cValue
|
||||
}
|
||||
}
|
||||
}
|
@ -558,6 +558,17 @@ extension Ghostty {
|
||||
_ = ghostty_config_get(config, &v, key, UInt(key.count))
|
||||
return v
|
||||
}
|
||||
|
||||
var macosShortcuts: MacShortcuts {
|
||||
let defaultValue = MacShortcuts.ask
|
||||
guard let config = self.config else { return defaultValue }
|
||||
var v: UnsafePointer<Int8>? = nil
|
||||
let key = "macos-shortcuts"
|
||||
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 MacShortcuts(rawValue: str) ?? defaultValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -584,6 +595,12 @@ extension Ghostty.Config {
|
||||
case always
|
||||
}
|
||||
|
||||
enum MacShortcuts: String {
|
||||
case allow
|
||||
case deny
|
||||
case ask
|
||||
}
|
||||
|
||||
enum ResizeOverlay : String {
|
||||
case always
|
||||
case never
|
||||
|
12
macos/Sources/Ghostty/Ghostty.Error.swift
Normal file
@ -0,0 +1,12 @@
|
||||
extension Ghostty {
|
||||
/// Possible errors from internal Ghostty calls.
|
||||
enum Error: Swift.Error, CustomLocalizedStringResourceConvertible {
|
||||
case apiFailed
|
||||
|
||||
var localizedStringResource: LocalizedStringResource {
|
||||
switch self {
|
||||
case .apiFailed: return "libghostty API call failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
149
macos/Sources/Ghostty/Ghostty.Surface.swift
Normal file
@ -0,0 +1,149 @@
|
||||
import GhosttyKit
|
||||
|
||||
extension Ghostty {
|
||||
/// Represents a single surface within Ghostty.
|
||||
///
|
||||
/// NOTE(mitchellh): This is a work-in-progress class as part of a general refactor
|
||||
/// of our Ghostty data model. At the time of writing there's still a ton of surface
|
||||
/// functionality that is not encapsulated in this class. It is planned to migrate that
|
||||
/// all over.
|
||||
///
|
||||
/// Wraps a `ghostty_surface_t`
|
||||
final class Surface: Sendable {
|
||||
private let surface: ghostty_surface_t
|
||||
|
||||
/// Read the underlying C value for this surface. This is unsafe because the value will be
|
||||
/// freed when the Surface class is deinitialized.
|
||||
var unsafeCValue: ghostty_surface_t {
|
||||
surface
|
||||
}
|
||||
|
||||
/// Initialize from the C structure.
|
||||
init(cSurface: ghostty_surface_t) {
|
||||
self.surface = cSurface
|
||||
}
|
||||
|
||||
deinit {
|
||||
// deinit is not guaranteed to happen on the main actor and our API
|
||||
// calls into libghostty must happen there so we capture the surface
|
||||
// value so we don't capture `self` and then we detach it in a task.
|
||||
// We can't wait for the task to succeed so this will happen sometime
|
||||
// but that's okay.
|
||||
let surface = self.surface
|
||||
Task.detached { @MainActor in
|
||||
ghostty_surface_free(surface)
|
||||
}
|
||||
}
|
||||
|
||||
/// Send text to the terminal as if it was typed. This doesn't send the key events so keyboard
|
||||
/// shortcuts and other encodings do not take effect.
|
||||
@MainActor
|
||||
func sendText(_ text: String) {
|
||||
let len = text.utf8CString.count
|
||||
if (len == 0) { return }
|
||||
|
||||
text.withCString { ptr in
|
||||
// len includes the null terminator so we do len - 1
|
||||
ghostty_surface_text(surface, ptr, UInt(len - 1))
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a key event to the terminal.
|
||||
///
|
||||
/// This sends the full key event including modifiers, action type, and text to the terminal.
|
||||
/// Unlike `sendText`, this method processes keyboard shortcuts, key bindings, and terminal
|
||||
/// encoding based on the complete key event information.
|
||||
///
|
||||
/// - Parameter event: The key event to send to the terminal
|
||||
@MainActor
|
||||
func sendKeyEvent(_ event: Input.KeyEvent) {
|
||||
event.withCValue { cEvent in
|
||||
ghostty_surface_key(surface, cEvent)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the terminal has captured mouse input.
|
||||
///
|
||||
/// When the mouse is captured, the terminal application is receiving mouse events
|
||||
/// directly rather than the host system handling them. This typically occurs when
|
||||
/// a terminal application enables mouse reporting mode.
|
||||
@MainActor
|
||||
var mouseCaptured: Bool {
|
||||
ghostty_surface_mouse_captured(surface)
|
||||
}
|
||||
|
||||
/// Send a mouse button event to the terminal.
|
||||
///
|
||||
/// This sends a complete mouse button event including the button state (press/release),
|
||||
/// which button was pressed, and any modifier keys that were held during the event.
|
||||
/// The terminal processes this event according to its mouse handling configuration.
|
||||
///
|
||||
/// - Parameter event: The mouse button event to send to the terminal
|
||||
@MainActor
|
||||
func sendMouseButton(_ event: Input.MouseButtonEvent) {
|
||||
ghostty_surface_mouse_button(
|
||||
surface,
|
||||
event.action.cMouseState,
|
||||
event.button.cMouseButton,
|
||||
event.mods.cMods)
|
||||
}
|
||||
|
||||
/// Send a mouse position event to the terminal.
|
||||
///
|
||||
/// This reports the current mouse position to the terminal, which may be used
|
||||
/// for mouse tracking, hover effects, or other position-dependent features.
|
||||
/// The terminal will only receive these events if mouse reporting is enabled.
|
||||
///
|
||||
/// - Parameter event: The mouse position event to send to the terminal
|
||||
@MainActor
|
||||
func sendMousePos(_ event: Input.MousePosEvent) {
|
||||
ghostty_surface_mouse_pos(
|
||||
surface,
|
||||
event.x,
|
||||
event.y,
|
||||
event.mods.cMods)
|
||||
}
|
||||
|
||||
/// Send a mouse scroll event to the terminal.
|
||||
///
|
||||
/// This sends scroll wheel input to the terminal with delta values for both
|
||||
/// horizontal and vertical scrolling, along with precision and momentum information.
|
||||
/// The terminal processes this according to its scroll handling configuration.
|
||||
///
|
||||
/// - Parameter event: The mouse scroll event to send to the terminal
|
||||
@MainActor
|
||||
func sendMouseScroll(_ event: Input.MouseScrollEvent) {
|
||||
ghostty_surface_mouse_scroll(
|
||||
surface,
|
||||
event.x,
|
||||
event.y,
|
||||
event.mods.cScrollMods)
|
||||
}
|
||||
|
||||
/// Perform a keybinding action.
|
||||
///
|
||||
/// The action can be any valid keybind parameter. e.g. `keybind = goto_tab:4`
|
||||
/// you can perform `goto_tab:4` with this.
|
||||
///
|
||||
/// Returns true if the action was performed. Invalid actions return false.
|
||||
@MainActor
|
||||
func perform(action: String) -> Bool {
|
||||
let len = action.utf8CString.count
|
||||
if (len == 0) { return false }
|
||||
return action.withCString { cString in
|
||||
ghostty_surface_binding_action(surface, cString, UInt(len - 1))
|
||||
}
|
||||
}
|
||||
|
||||
/// Command options for this surface.
|
||||
@MainActor
|
||||
func commands() throws -> [Command] {
|
||||
var ptr: UnsafeMutablePointer<ghostty_command_s>? = nil
|
||||
var count: Int = 0
|
||||
ghostty_surface_commands(surface, &ptr, &count)
|
||||
guard let ptr else { throw Error.apiFailed }
|
||||
let buffer = UnsafeBufferPointer(start: ptr, count: count)
|
||||
return Array(buffer).map { Command(cValue: $0) }.filter { $0.isSupported }
|
||||
}
|
||||
}
|
||||
}
|
@ -337,9 +337,9 @@ extension Ghostty {
|
||||
|
||||
private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
|
||||
guard let inspector = self.inspector else { return }
|
||||
guard let key = Ghostty.keycodeToKey[event.keyCode] else { return }
|
||||
guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_inspector_key(inspector, action, key, mods)
|
||||
ghostty_inspector_key(inspector, action, key.cKey, mods)
|
||||
}
|
||||
|
||||
// MARK: NSTextInputClient
|
||||
|
@ -19,6 +19,15 @@ struct Ghostty {
|
||||
static let userNotificationActionShow = "com.mitchellh.ghostty.userNotification.Show"
|
||||
}
|
||||
|
||||
// MARK: C Extensions
|
||||
|
||||
/// A command is fully self-contained so it is Sendable.
|
||||
extension ghostty_command_s: @unchecked @retroactive Sendable {}
|
||||
|
||||
/// A surface is sendable because it is just a reference type. Using the surface in parameters
|
||||
/// may be unsafe but the value itself is safe to send across threads.
|
||||
extension ghostty_surface_t: @unchecked @retroactive Sendable {}
|
||||
|
||||
// MARK: Build Info
|
||||
|
||||
extension Ghostty {
|
||||
|
@ -79,7 +79,7 @@ extension Ghostty {
|
||||
let pubResign = center.publisher(for: NSWindow.didResignKeyNotification)
|
||||
#endif
|
||||
|
||||
Surface(view: surfaceView, size: geo.size)
|
||||
SurfaceRepresentable(view: surfaceView, size: geo.size)
|
||||
.focused($surfaceFocus)
|
||||
.focusedValue(\.ghosttySurfacePwd, surfaceView.pwd)
|
||||
.focusedValue(\.ghosttySurfaceView, surfaceView)
|
||||
@ -381,7 +381,7 @@ extension Ghostty {
|
||||
/// We just wrap an AppKit NSView here at the moment so that we can behave as low level as possible
|
||||
/// since that is what the Metal renderer in Ghostty expects. In the future, it may make more sense to
|
||||
/// wrap an MTKView and use that, but for legacy reasons we didn't do that to begin with.
|
||||
struct Surface: OSViewRepresentable {
|
||||
struct SurfaceRepresentable: OSViewRepresentable {
|
||||
/// The view to render for the terminal surface.
|
||||
let view: SurfaceView
|
||||
|
||||
@ -418,28 +418,48 @@ extension Ghostty {
|
||||
|
||||
/// Explicit command to set
|
||||
var command: String? = nil
|
||||
|
||||
/// Environment variables to set for the terminal
|
||||
var environmentVariables: [String: String] = [:]
|
||||
|
||||
/// Extra input to send as stdin
|
||||
var initialInput: String? = nil
|
||||
|
||||
init() {}
|
||||
|
||||
init(from config: ghostty_surface_config_s) {
|
||||
self.fontSize = config.font_size
|
||||
self.workingDirectory = String.init(cString: config.working_directory, encoding: .utf8)
|
||||
self.command = String.init(cString: config.command, encoding: .utf8)
|
||||
if let workingDirectory = config.working_directory {
|
||||
self.workingDirectory = String.init(cString: workingDirectory, encoding: .utf8)
|
||||
}
|
||||
if let command = config.command {
|
||||
self.command = String.init(cString: command, encoding: .utf8)
|
||||
}
|
||||
|
||||
// Convert the C env vars to Swift dictionary
|
||||
if config.env_var_count > 0, let envVars = config.env_vars {
|
||||
for i in 0..<config.env_var_count {
|
||||
let envVar = envVars[i]
|
||||
if let key = String(cString: envVar.key, encoding: .utf8),
|
||||
let value = String(cString: envVar.value, encoding: .utf8) {
|
||||
self.environmentVariables[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the ghostty configuration for this surface configuration struct. The memory
|
||||
/// in the returned struct is only valid as long as this struct is retained.
|
||||
func ghosttyConfig(view: SurfaceView) -> ghostty_surface_config_s {
|
||||
/// Provides a C-compatible ghostty configuration within a closure. The configuration
|
||||
/// and all its string pointers are only valid within the closure.
|
||||
func withCValue<T>(view: SurfaceView, _ body: (inout ghostty_surface_config_s) throws -> T) rethrows -> T {
|
||||
var config = ghostty_surface_config_new()
|
||||
config.userdata = Unmanaged.passUnretained(view).toOpaque()
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
config.platform_tag = GHOSTTY_PLATFORM_MACOS
|
||||
config.platform = ghostty_platform_u(macos: ghostty_platform_macos_s(
|
||||
nsview: Unmanaged.passUnretained(view).toOpaque()
|
||||
))
|
||||
config.scale_factor = NSScreen.main!.backingScaleFactor
|
||||
|
||||
#elseif os(iOS)
|
||||
#elseif os(iOS)
|
||||
config.platform_tag = GHOSTTY_PLATFORM_IOS
|
||||
config.platform = ghostty_platform_u(ios: ghostty_platform_ios_s(
|
||||
uiview: Unmanaged.passUnretained(view).toOpaque()
|
||||
@ -449,19 +469,50 @@ extension Ghostty {
|
||||
// probably set this to some default, then modify the scale factor through
|
||||
// libghostty APIs when a UIView is attached to a window/scene. TODO.
|
||||
config.scale_factor = UIScreen.main.scale
|
||||
#else
|
||||
#error("unsupported target")
|
||||
#endif
|
||||
#else
|
||||
#error("unsupported target")
|
||||
#endif
|
||||
|
||||
if let fontSize = fontSize { config.font_size = fontSize }
|
||||
if let workingDirectory = workingDirectory {
|
||||
config.working_directory = (workingDirectory as NSString).utf8String
|
||||
}
|
||||
if let command = command {
|
||||
config.command = (command as NSString).utf8String
|
||||
}
|
||||
// Zero is our default value that means to inherit the font size.
|
||||
config.font_size = fontSize ?? 0
|
||||
|
||||
return config
|
||||
// Use withCString to ensure strings remain valid for the duration of the closure
|
||||
return try workingDirectory.withCString { cWorkingDir in
|
||||
config.working_directory = cWorkingDir
|
||||
|
||||
return try command.withCString { cCommand in
|
||||
config.command = cCommand
|
||||
|
||||
return try initialInput.withCString { cInput in
|
||||
config.initial_input = cInput
|
||||
|
||||
// Convert dictionary to arrays for easier processing
|
||||
let keys = Array(environmentVariables.keys)
|
||||
let values = Array(environmentVariables.values)
|
||||
|
||||
// Create C strings for all keys and values
|
||||
return try keys.withCStrings { keyCStrings in
|
||||
return try values.withCStrings { valueCStrings in
|
||||
// Create array of ghostty_env_var_s
|
||||
var envVars = Array<ghostty_env_var_s>()
|
||||
envVars.reserveCapacity(environmentVariables.count)
|
||||
for i in 0..<environmentVariables.count {
|
||||
envVars.append(ghostty_env_var_s(
|
||||
key: keyCStrings[i],
|
||||
value: valueCStrings[i]
|
||||
))
|
||||
}
|
||||
|
||||
return try envVars.withUnsafeMutableBufferPointer { buffer in
|
||||
config.env_vars = buffer.baseAddress
|
||||
config.env_var_count = environmentVariables.count
|
||||
return try body(&config)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,10 +115,20 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the data model for this surface.
|
||||
///
|
||||
/// Note: eventually, all surface access will be through this, but presently its in a transition
|
||||
/// state so we're mixing this with direct surface access.
|
||||
private(set) var surfaceModel: Ghostty.Surface?
|
||||
|
||||
/// Returns the underlying C value for the surface. See "note" on surfaceModel.
|
||||
var surface: ghostty_surface_t? {
|
||||
surfaceModel?.unsafeCValue
|
||||
}
|
||||
|
||||
// Notification identifiers associated with this surface
|
||||
var notificationIdentifiers: Set<String> = []
|
||||
|
||||
private(set) var surface: ghostty_surface_t?
|
||||
private var markedText: NSMutableAttributedString
|
||||
private(set) var focused: Bool = true
|
||||
private var prevPressureStage: Int = 0
|
||||
@ -139,7 +149,8 @@ extension Ghostty {
|
||||
private var titleFromTerminal: String?
|
||||
|
||||
// The cached contents of the screen.
|
||||
private var cachedScreenContents: CachedValue<String>
|
||||
private(set) var cachedScreenContents: CachedValue<String>
|
||||
private(set) var cachedVisibleContents: CachedValue<String>
|
||||
|
||||
/// Event monitor (see individual events for why)
|
||||
private var eventMonitor: Any? = nil
|
||||
@ -147,10 +158,6 @@ extension Ghostty {
|
||||
// We need to support being a first responder so that we can get input events
|
||||
override var acceptsFirstResponder: Bool { return true }
|
||||
|
||||
// I don't think we need this but this lets us know we should redraw our layer
|
||||
// so we'll use that to tell ghostty to refresh.
|
||||
override var wantsUpdateLayer: Bool { return true }
|
||||
|
||||
init(_ app: ghostty_app_t, baseConfig: SurfaceConfiguration? = nil, uuid: UUID? = nil) {
|
||||
self.markedText = NSMutableAttributedString()
|
||||
self.uuid = uuid ?? .init()
|
||||
@ -166,6 +173,7 @@ extension Ghostty {
|
||||
// it back up later so we can reference `self`. This is a hack we should
|
||||
// fix at some point.
|
||||
self.cachedScreenContents = .init(duration: .milliseconds(500)) { "" }
|
||||
self.cachedVisibleContents = self.cachedScreenContents
|
||||
|
||||
// Initialize with some default frame size. The important thing is that this
|
||||
// is non-zero so that our layer bounds are non-zero so that our renderer
|
||||
@ -193,6 +201,26 @@ extension Ghostty {
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
return String(cString: text.text)
|
||||
}
|
||||
cachedVisibleContents = .init(duration: .milliseconds(500)) { [weak self] in
|
||||
guard let self else { return "" }
|
||||
guard let surface = self.surface else { return "" }
|
||||
var text = ghostty_text_s()
|
||||
let sel = ghostty_selection_s(
|
||||
top_left: ghostty_point_s(
|
||||
tag: GHOSTTY_POINT_VIEWPORT,
|
||||
coord: GHOSTTY_POINT_COORD_TOP_LEFT,
|
||||
x: 0,
|
||||
y: 0),
|
||||
bottom_right: ghostty_point_s(
|
||||
tag: GHOSTTY_POINT_VIEWPORT,
|
||||
coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
|
||||
x: 0,
|
||||
y: 0),
|
||||
rectangle: false)
|
||||
guard ghostty_surface_read_text(surface, sel, &text) else { return "" }
|
||||
defer { ghostty_surface_free_text(surface, &text) }
|
||||
return String(cString: text.text)
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -258,12 +286,14 @@ extension Ghostty {
|
||||
|
||||
// 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)
|
||||
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
|
||||
self.error = AppError.surfaceCreateError
|
||||
let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
|
||||
ghostty_surface_new(app, &surface_cfg_c)
|
||||
}
|
||||
guard let surface = surface else {
|
||||
self.error = Ghostty.Error.apiFailed
|
||||
return
|
||||
}
|
||||
self.surface = surface;
|
||||
self.surfaceModel = Ghostty.Surface(cSurface: surface)
|
||||
|
||||
// Setup our tracking area so we get mouse moved events
|
||||
updateTrackingAreas()
|
||||
@ -318,11 +348,6 @@ extension Ghostty {
|
||||
// Remove any notifications associated with this surface
|
||||
let identifiers = Array(self.notificationIdentifiers)
|
||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
|
||||
|
||||
// Free our core surface resources
|
||||
if let surface = self.surface {
|
||||
ghostty_surface_free(surface)
|
||||
}
|
||||
}
|
||||
|
||||
func focusDidChange(_ focused: Bool) {
|
||||
@ -703,11 +728,6 @@ extension Ghostty {
|
||||
setSurfaceSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
|
||||
}
|
||||
|
||||
override func updateLayer() {
|
||||
guard let surface = self.surface else { return }
|
||||
ghostty_surface_draw(surface);
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
@ -781,19 +801,23 @@ extension Ghostty {
|
||||
override func mouseEntered(with event: NSEvent) {
|
||||
super.mouseEntered(with: event)
|
||||
|
||||
guard let surface = self.surface else { return }
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// On mouse enter we need to reset our cursor position. This is
|
||||
// super important because we set it to -1/-1 on mouseExit and
|
||||
// lots of mouse logic (i.e. whether to send mouse reports) depend
|
||||
// on the position being in the viewport if it is.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
|
||||
let mouseEvent = Ghostty.Input.MousePosEvent(
|
||||
x: pos.x,
|
||||
y: frame.height - pos.y,
|
||||
mods: .init(nsFlags: event.modifierFlags)
|
||||
)
|
||||
surfaceModel.sendMousePos(mouseEvent)
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// If the mouse is being dragged then we don't have to emit
|
||||
// this because we get mouse drag events even if we've already
|
||||
@ -803,17 +827,25 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
// Negative values indicate cursor has left the viewport
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, -1, -1, mods)
|
||||
let mouseEvent = Ghostty.Input.MousePosEvent(
|
||||
x: -1,
|
||||
y: -1,
|
||||
mods: .init(nsFlags: event.modifierFlags)
|
||||
)
|
||||
surfaceModel.sendMousePos(mouseEvent)
|
||||
}
|
||||
|
||||
override func mouseMoved(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// Convert window position to view position. Note (0, 0) is bottom left.
|
||||
let pos = self.convert(event.locationInWindow, from: nil)
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_pos(surface, pos.x, frame.height - pos.y, mods)
|
||||
let mouseEvent = Ghostty.Input.MousePosEvent(
|
||||
x: pos.x,
|
||||
y: frame.height - pos.y,
|
||||
mods: .init(nsFlags: event.modifierFlags)
|
||||
)
|
||||
surfaceModel.sendMousePos(mouseEvent)
|
||||
|
||||
// Handle focus-follows-mouse
|
||||
if let window,
|
||||
@ -839,16 +871,13 @@ extension Ghostty {
|
||||
}
|
||||
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
guard let surface = self.surface else { return }
|
||||
|
||||
// Builds up the "input.ScrollMods" bitmask
|
||||
var mods: Int32 = 0
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
var x = event.scrollingDeltaX
|
||||
var y = event.scrollingDeltaY
|
||||
if event.hasPreciseScrollingDeltas {
|
||||
mods = 1
|
||||
|
||||
let precision = event.hasPreciseScrollingDeltas
|
||||
|
||||
if precision {
|
||||
// We do a 2x speed multiplier. This is subjective, it "feels" better to me.
|
||||
x *= 2;
|
||||
y *= 2;
|
||||
@ -856,29 +885,12 @@ extension Ghostty {
|
||||
// TODO(mitchellh): do we have to scale the x/y here by window scale factor?
|
||||
}
|
||||
|
||||
// Determine our momentum value
|
||||
var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
|
||||
switch (event.momentumPhase) {
|
||||
case .began:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN
|
||||
case .stationary:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
|
||||
case .changed:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
|
||||
case .ended:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
|
||||
case .cancelled:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
|
||||
case .mayBegin:
|
||||
momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
// Pack our momentum value into the mods bitmask
|
||||
mods |= Int32(momentum.rawValue) << 1
|
||||
|
||||
ghostty_surface_mouse_scroll(surface, x, y, mods)
|
||||
let scrollEvent = Ghostty.Input.MouseScrollEvent(
|
||||
x: x,
|
||||
y: y,
|
||||
mods: .init(precision: precision, momentum: .init(event.momentumPhase))
|
||||
)
|
||||
surfaceModel.sendMouseScroll(scrollEvent)
|
||||
}
|
||||
|
||||
override func pressureChange(with event: NSEvent) {
|
||||
@ -1285,8 +1297,8 @@ extension Ghostty {
|
||||
// In this case, AppKit calls menu BEFORE calling any mouse events.
|
||||
// If mouse capturing is enabled then we never show the context menu
|
||||
// so that we can handle ctrl+left-click in the terminal app.
|
||||
guard let surface = self.surface else { return nil }
|
||||
if ghostty_surface_mouse_captured(surface) {
|
||||
guard let surfaceModel else { return nil }
|
||||
if surfaceModel.mouseCaptured {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1296,13 +1308,10 @@ extension Ghostty {
|
||||
//
|
||||
// Note this never sounds a right mouse up event but that's the
|
||||
// same as normal right-click with capturing disabled from AppKit.
|
||||
let mods = Ghostty.ghosttyMods(event.modifierFlags)
|
||||
ghostty_surface_mouse_button(
|
||||
surface,
|
||||
GHOSTTY_MOUSE_PRESS,
|
||||
GHOSTTY_MOUSE_RIGHT,
|
||||
mods
|
||||
)
|
||||
surfaceModel.sendMouseButton(.init(
|
||||
action: .press,
|
||||
button: .right,
|
||||
mods: .init(nsFlags: event.modifierFlags)))
|
||||
|
||||
default:
|
||||
return nil
|
||||
@ -1673,7 +1682,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
func insertText(_ string: Any, replacementRange: NSRange) {
|
||||
// We must have an associated event
|
||||
guard NSApp.currentEvent != nil else { return }
|
||||
guard let surface = self.surface else { return }
|
||||
guard let surfaceModel else { return }
|
||||
|
||||
// We want the string view of the any value
|
||||
var chars = ""
|
||||
@ -1697,13 +1706,7 @@ extension Ghostty.SurfaceView: NSTextInputClient {
|
||||
return
|
||||
}
|
||||
|
||||
let len = chars.utf8CString.count
|
||||
if (len == 0) { return }
|
||||
|
||||
chars.withCString { ptr in
|
||||
// len includes the null terminator so we do len - 1
|
||||
ghostty_surface_text(surface, ptr, UInt(len - 1))
|
||||
}
|
||||
surfaceModel.sendText(chars)
|
||||
}
|
||||
|
||||
/// This function needs to exist for two reasons:
|
||||
@ -1979,7 +1982,7 @@ extension Ghostty.SurfaceView {
|
||||
/// Caches a value for some period of time, evicting it automatically when that time expires.
|
||||
/// We use this to cache our surface content. This probably should be extracted some day
|
||||
/// to a more generic helper.
|
||||
fileprivate class CachedValue<T> {
|
||||
class CachedValue<T> {
|
||||
private var value: T?
|
||||
private let fetch: () -> T
|
||||
private let duration: Duration
|
||||
|
@ -57,8 +57,10 @@ extension Ghostty {
|
||||
|
||||
// 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)
|
||||
guard let surface = ghostty_surface_new(app, &surface_cfg_c) else {
|
||||
let surface = surface_cfg.withCValue(view: self) { surface_cfg_c in
|
||||
ghostty_surface_new(app, &surface_cfg_c)
|
||||
}
|
||||
guard let surface = surface else {
|
||||
// TODO
|
||||
return
|
||||
}
|
||||
|
@ -8,37 +8,3 @@ func isRunningInXcode() -> Bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/// True if we have liquid glass available.
|
||||
func hasLiquidGlass() -> Bool {
|
||||
// Can't have liquid glass unless we're in macOS 26+
|
||||
if #unavailable(macOS 26.0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// If we aren't running SDK 26.0 or later then we definitely
|
||||
// do not have liquid glass.
|
||||
guard let sdkName = Bundle.main.infoDictionary?["DTSDKName"] as? String else {
|
||||
// If we don't have this, we assume we're built against the latest
|
||||
// since we're on macOS 26+
|
||||
return true
|
||||
}
|
||||
|
||||
// If the SDK doesn't start with macosx then we just assume we
|
||||
// have it because we already verified we're on macOS above.
|
||||
guard sdkName.hasPrefix("macosx") else {
|
||||
return true
|
||||
}
|
||||
|
||||
// The SDK version must be at least 26
|
||||
let versionString = String(sdkName.dropFirst("macosx".count))
|
||||
guard let major = if let dotIndex = versionString.firstIndex(of: ".") {
|
||||
Int(String(versionString[..<dotIndex]))
|
||||
} else {
|
||||
Int(versionString)
|
||||
} else { return true }
|
||||
|
||||
// Note: we could also check for the UIDesignRequiresCompatibility key
|
||||
// but our project doesn't use it so there's no point.
|
||||
return major >= 26
|
||||
}
|
||||
|
@ -21,3 +21,28 @@ extension Array {
|
||||
return i + 1
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == String {
|
||||
/// Executes a closure with an array of C string pointers.
|
||||
func withCStrings<T>(_ body: ([UnsafePointer<Int8>?]) throws -> T) rethrows -> T {
|
||||
// Handle empty array
|
||||
if isEmpty {
|
||||
return try body([])
|
||||
}
|
||||
|
||||
// Recursive helper to process strings
|
||||
func helper(index: Int, accumulated: [UnsafePointer<Int8>?], body: ([UnsafePointer<Int8>?]) throws -> T) rethrows -> T {
|
||||
if index == count {
|
||||
return try body(accumulated)
|
||||
}
|
||||
|
||||
return try self[index].withCString { cStr in
|
||||
var newAccumulated = accumulated
|
||||
newAccumulated.append(cStr)
|
||||
return try helper(index: index + 1, accumulated: newAccumulated, body: body)
|
||||
}
|
||||
}
|
||||
|
||||
return try helper(index: 0, accumulated: [], body: body)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
extension NSView {
|
||||
/// Returns true if this view is currently in the responder chain
|
||||
@ -15,6 +16,24 @@ extension NSView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Screenshot
|
||||
|
||||
extension NSView {
|
||||
/// Take a screenshot of just this view.
|
||||
func screenshot() -> NSImage? {
|
||||
guard let bitmapRep = bitmapImageRepForCachingDisplay(in: bounds) else { return nil }
|
||||
cacheDisplay(in: bounds, to: bitmapRep)
|
||||
let image = NSImage(size: bounds.size)
|
||||
image.addRepresentation(bitmapRep)
|
||||
return image
|
||||
}
|
||||
|
||||
func screenshot() -> Image? {
|
||||
guard let nsImage: NSImage = self.screenshot() else { return nil }
|
||||
return Image(nsImage: nsImage)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: View Traversal and Search
|
||||
|
||||
extension NSView {
|
||||
|
@ -9,4 +9,10 @@ extension NSWindow {
|
||||
guard windowNumber > 0 else { return nil }
|
||||
return CGWindowID(windowNumber)
|
||||
}
|
||||
|
||||
/// True if this is the first window in the tab group.
|
||||
var isFirstWindowInTabGroup: Bool {
|
||||
guard let firstWindow = tabGroup?.windows.first else { return true }
|
||||
return firstWindow === self
|
||||
}
|
||||
}
|
||||
|
10
macos/Sources/Helpers/Extensions/Optional+Extension.swift
Normal file
@ -0,0 +1,10 @@
|
||||
extension Optional where Wrapped == String {
|
||||
/// Executes a closure with a C string pointer, handling nil gracefully.
|
||||
func withCString<T>(_ body: (UnsafePointer<Int8>?) throws -> T) rethrows -> T {
|
||||
if let string = self {
|
||||
return try string.withCString(body)
|
||||
} else {
|
||||
return try body(nil)
|
||||
}
|
||||
}
|
||||
}
|
213
macos/Sources/Helpers/PermissionRequest.swift
Normal file
@ -0,0 +1,213 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
/// Displays a permission request dialog with optional caching of user decisions
|
||||
class PermissionRequest {
|
||||
/// Specifies how long a permission decision should be cached
|
||||
enum AllowDuration {
|
||||
case once
|
||||
case forever
|
||||
case duration(Duration)
|
||||
}
|
||||
|
||||
/// Shows a permission request dialog with customizable caching behavior
|
||||
/// - Parameters:
|
||||
/// - key: Unique identifier for storing/retrieving cached decisions in UserDefaults
|
||||
/// - message: The message to display in the alert dialog
|
||||
/// - allowText: Custom text for the allow button (defaults to "Allow")
|
||||
/// - allowDuration: If provided, automatically cache "Allow" responses for this duration
|
||||
/// - rememberDuration: If provided, shows a checkbox to remember the decision for this duration
|
||||
/// - window: If provided, shows the alert as a sheet attached to this window
|
||||
/// - completion: Called with the user's decision (true for allow, false for deny)
|
||||
///
|
||||
/// Caching behavior:
|
||||
/// - If rememberDuration is provided and user checks "Remember my decision", both allow/deny are cached for that duration
|
||||
/// - If allowDuration is provided and user selects allow (without checkbox), decision is cached for that duration
|
||||
/// - Cached decisions are automatically returned without showing the dialog
|
||||
@MainActor
|
||||
static func show(
|
||||
_ key: String,
|
||||
message: String,
|
||||
informative: String = "",
|
||||
allowText: String = "Allow",
|
||||
allowDuration: AllowDuration = .once,
|
||||
rememberDuration: Duration? = .seconds(86400),
|
||||
window: NSWindow? = nil,
|
||||
completion: @escaping (Bool) -> Void
|
||||
) {
|
||||
// Check if we have a stored decision that hasn't expired
|
||||
if let storedResult = getStoredResult(for: key) {
|
||||
completion(storedResult)
|
||||
return
|
||||
}
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.messageText = message
|
||||
alert.informativeText = informative
|
||||
alert.alertStyle = .informational
|
||||
|
||||
// Add buttons (they appear in reverse order)
|
||||
alert.addButton(withTitle: allowText)
|
||||
alert.addButton(withTitle: "Don't Allow")
|
||||
|
||||
// Create checkbox for remembering if duration is provided
|
||||
var checkbox: NSButton?
|
||||
if let rememberDuration = rememberDuration {
|
||||
let checkboxTitle = formatRememberText(for: rememberDuration)
|
||||
checkbox = NSButton(
|
||||
checkboxWithTitle: checkboxTitle,
|
||||
target: nil,
|
||||
action: nil)
|
||||
checkbox!.state = .off
|
||||
|
||||
// Set checkbox as accessory view
|
||||
alert.accessoryView = checkbox
|
||||
}
|
||||
|
||||
// Show the alert
|
||||
if let window = window {
|
||||
alert.beginSheetModal(for: window) { response in
|
||||
handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion)
|
||||
}
|
||||
} else {
|
||||
let response = alert.runModal()
|
||||
handleResponse(response, rememberDecision: checkbox?.state == .on, key: key, allowDuration: allowDuration, rememberDuration: rememberDuration, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles the alert response and processes caching logic
|
||||
/// - Parameters:
|
||||
/// - response: The alert response from the user
|
||||
/// - rememberDecision: Whether the remember checkbox was checked
|
||||
/// - key: The UserDefaults key for caching
|
||||
/// - allowDuration: Optional duration for auto-caching allow responses
|
||||
/// - rememberDuration: Optional duration for the remember checkbox
|
||||
/// - completion: Completion handler to call with the result
|
||||
private static func handleResponse(
|
||||
_ response: NSApplication.ModalResponse,
|
||||
rememberDecision: Bool,
|
||||
key: String,
|
||||
allowDuration: AllowDuration,
|
||||
rememberDuration: Duration?,
|
||||
completion: @escaping (Bool) -> Void) {
|
||||
|
||||
let result: Bool
|
||||
switch response {
|
||||
case .alertFirstButtonReturn: // Allow
|
||||
result = true
|
||||
case .alertSecondButtonReturn: // Don't Allow
|
||||
result = false
|
||||
default:
|
||||
result = false
|
||||
}
|
||||
|
||||
// Store the result if checkbox is checked or if "Allow" was selected and allowDuration is set
|
||||
if rememberDecision, let rememberDuration = rememberDuration {
|
||||
storeResult(result, for: key, duration: rememberDuration)
|
||||
} else if result {
|
||||
switch allowDuration {
|
||||
case .once:
|
||||
// Don't store anything for once
|
||||
break
|
||||
case .forever:
|
||||
// Store for a very long time (100 years). When the bug comes in that
|
||||
// 100 years has passed and their forever permission expired I'll be
|
||||
// dead so it won't be my problem.
|
||||
storeResult(result, for: key, duration: .seconds(3153600000))
|
||||
case .duration(let duration):
|
||||
storeResult(result, for: key, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
completion(result)
|
||||
}
|
||||
|
||||
/// Retrieves a cached permission decision if it hasn't expired
|
||||
/// - Parameter key: The UserDefaults key to check
|
||||
/// - Returns: The cached decision, or nil if no valid cached decision exists
|
||||
private static func getStoredResult(for key: String) -> Bool? {
|
||||
let userDefaults = UserDefaults.standard
|
||||
guard let data = userDefaults.data(forKey: key),
|
||||
let storedPermission = try? NSKeyedUnarchiver.unarchivedObject(
|
||||
ofClass: StoredPermission.self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if Date() > storedPermission.expiry {
|
||||
// Decision has expired, remove stored value
|
||||
userDefaults.removeObject(forKey: key)
|
||||
return nil
|
||||
}
|
||||
|
||||
return storedPermission.result
|
||||
}
|
||||
|
||||
/// Stores a permission decision in UserDefaults with an expiration date
|
||||
/// - Parameters:
|
||||
/// - result: The permission decision to store
|
||||
/// - key: The UserDefaults key to store under
|
||||
/// - duration: How long the decision should be cached
|
||||
private static func storeResult(_ result: Bool, for key: String, duration: Duration) {
|
||||
let expiryDate = Date().addingTimeInterval(duration.timeInterval)
|
||||
let storedPermission = StoredPermission(result: result, expiry: expiryDate)
|
||||
if let data = try? NSKeyedArchiver.archivedData(withRootObject: storedPermission, requiringSecureCoding: true) {
|
||||
let userDefaults = UserDefaults.standard
|
||||
userDefaults.set(data, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats the remember checkbox text based on the duration
|
||||
/// - Parameter duration: The duration to format
|
||||
/// - Returns: A human-readable string for the checkbox
|
||||
private static func formatRememberText(for duration: Duration) -> String {
|
||||
let seconds = duration.timeInterval
|
||||
|
||||
// Warning: this probably isn't localization friendly at all so we're
|
||||
// going to have to redo this for that.
|
||||
switch seconds {
|
||||
case 0..<60:
|
||||
return "Remember my decision for \(Int(seconds)) seconds"
|
||||
case 60..<3600:
|
||||
let minutes = Int(seconds / 60)
|
||||
return "Remember my decision for \(minutes) minute\(minutes == 1 ? "" : "s")"
|
||||
case 3600..<86400:
|
||||
let hours = Int(seconds / 3600)
|
||||
return "Remember my decision for \(hours) hour\(hours == 1 ? "" : "s")"
|
||||
case 86400:
|
||||
return "Remember my decision for one day"
|
||||
default:
|
||||
let days = Int(seconds / 86400)
|
||||
return "Remember my decision for \(days) day\(days == 1 ? "" : "s")"
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal class for storing permission decisions with expiration dates in UserDefaults
|
||||
/// Conforms to NSSecureCoding for safe archiving/unarchiving
|
||||
@objc(StoredPermission)
|
||||
private class StoredPermission: NSObject, NSSecureCoding {
|
||||
static var supportsSecureCoding: Bool = true
|
||||
|
||||
let result: Bool
|
||||
let expiry: Date
|
||||
|
||||
init(result: Bool, expiry: Date) {
|
||||
self.result = result
|
||||
self.expiry = expiry
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
self.result = coder.decodeBool(forKey: "result")
|
||||
guard let expiry = coder.decodeObject(of: NSDate.self, forKey: "expiry") as? Date else {
|
||||
return nil
|
||||
}
|
||||
self.expiry = expiry
|
||||
super.init()
|
||||
}
|
||||
|
||||
func encode(with coder: NSCoder) {
|
||||
coder.encode(result, forKey: "result")
|
||||
coder.encode(expiry, forKey: "expiry")
|
||||
}
|
||||
}
|
||||
}
|
124
macos/Sources/Helpers/TabGroupCloseCoordinator.swift
Normal file
@ -0,0 +1,124 @@
|
||||
import AppKit
|
||||
|
||||
/// Coordinates close operations for windows that are part of a tab group.
|
||||
///
|
||||
/// This coordinator helps distinguish between closing a single tab versus closing
|
||||
/// an entire window (with all its tabs). When macOS native tabs are used, close
|
||||
/// operations can be ambiguous - this coordinator tracks close requests across
|
||||
/// multiple windows in a tab group to determine the user's intent.
|
||||
class TabGroupCloseCoordinator {
|
||||
/// The scope of a close operation.
|
||||
enum CloseScope {
|
||||
case tab
|
||||
case window
|
||||
}
|
||||
|
||||
/// Protocol that window controllers must implement to use the coordinator.
|
||||
protocol Controller {
|
||||
/// The tab group close coordinator instance for this controller.
|
||||
var tabGroupCloseCoordinator: TabGroupCloseCoordinator { get }
|
||||
}
|
||||
|
||||
/// Callback type for close operations.
|
||||
typealias Callback = (CloseScope) -> Void
|
||||
|
||||
// We use weak vars and ObjectIdentifiers below because we don't want to
|
||||
// create any strong reference cycles during coordination.
|
||||
|
||||
/// The tab group being coordinated. Weak reference to avoid cycles.
|
||||
private weak var tabGroup: NSWindowTabGroup?
|
||||
|
||||
/// Map of window identifiers to their close callbacks.
|
||||
private var closeRequests: [ObjectIdentifier: Callback] = [:]
|
||||
|
||||
/// Timer used to debounce close requests and determine intent.
|
||||
private var debounceTimer: Timer?
|
||||
|
||||
deinit {
|
||||
trigger(.tab)
|
||||
}
|
||||
|
||||
/// Call this from the windowShouldClose override in order to track whether
|
||||
/// a window close event is from a tab or a window. If this window already
|
||||
/// requested a close then only the latest will be called.
|
||||
func windowShouldClose(
|
||||
_ window: NSWindow,
|
||||
callback: @escaping Callback
|
||||
) {
|
||||
// If this window isn't part of a tab group we assume its a window
|
||||
// close for the window and let our timer keep running for the rest.
|
||||
guard let tabGroup = window.tabGroup else {
|
||||
callback(.window)
|
||||
return
|
||||
}
|
||||
|
||||
// Forward to the proper coordinator
|
||||
if let firstController = tabGroup.windows.first?.windowController as? Controller,
|
||||
firstController.tabGroupCloseCoordinator !== self {
|
||||
let coordinator = firstController.tabGroupCloseCoordinator
|
||||
coordinator.windowShouldClose(window, callback: callback)
|
||||
return
|
||||
}
|
||||
|
||||
// If our tab group is nil then we either are seeing this for the first
|
||||
// time or our weak ref expired and we should fire our callbacks.
|
||||
if self.tabGroup == nil {
|
||||
self.tabGroup = tabGroup
|
||||
debounceTimer?.fire()
|
||||
debounceTimer = nil
|
||||
}
|
||||
|
||||
// No matter what, we cancel our debounce and restart this. This opens
|
||||
// us up to a DoS if close requests are looped but this would only
|
||||
// happen in hostile scenarios that are self-inflicted.
|
||||
debounceTimer?.invalidate()
|
||||
debounceTimer = nil
|
||||
|
||||
// If this tab group doesn't match then I don't really know what to
|
||||
// do. This shouldn't happen. So we just assume it's a tab close
|
||||
// and trigger the rest. No right answer here as far as I know.
|
||||
if self.tabGroup != tabGroup {
|
||||
callback(.tab)
|
||||
trigger(.tab)
|
||||
return
|
||||
}
|
||||
|
||||
// Add the request
|
||||
closeRequests[ObjectIdentifier(window)] = callback
|
||||
|
||||
// If close requests matches all our windows then we are done.
|
||||
if closeRequests.count == tabGroup.windows.count {
|
||||
let allWindows = Set(tabGroup.windows.map { ObjectIdentifier($0) })
|
||||
if Set(closeRequests.keys) == allWindows {
|
||||
trigger(.window)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Setup our new timer
|
||||
debounceTimer = Timer.scheduledTimer(
|
||||
withTimeInterval: Duration.milliseconds(100).timeInterval,
|
||||
repeats: false
|
||||
) { [weak self] _ in
|
||||
self?.trigger(.tab)
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers all pending close callbacks with the given scope.
|
||||
///
|
||||
/// This method is called when the coordinator has determined the user's intent
|
||||
/// (either closing a tab or the entire window). It executes all pending callbacks
|
||||
/// and resets the coordinator's state.
|
||||
///
|
||||
/// - Parameter scope: The determined scope of the close operation.
|
||||
private func trigger(_ scope: CloseScope) {
|
||||
// Reset our state
|
||||
tabGroup = nil
|
||||
debounceTimer?.invalidate()
|
||||
debounceTimer = nil
|
||||
|
||||
// Trigger all of our callbacks
|
||||
closeRequests.forEach { $0.value(scope) }
|
||||
closeRequests = [:]
|
||||
}
|
||||
}
|
@ -164,11 +164,23 @@ fn buildLib(b: *std.Build, module: *std.Build.Module, options: anytype) !*std.Bu
|
||||
"-DHAVE_SYS_STATVFS_H",
|
||||
|
||||
"-DFC_CACHEDIR=\"/var/cache/fontconfig\"",
|
||||
"-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"",
|
||||
"-DFONTCONFIG_PATH=\"/etc/fonts\"",
|
||||
"-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"",
|
||||
"-DFC_DEFAULT_FONTS=\"<dir>/usr/share/fonts</dir><dir>/usr/local/share/fonts</dir>\"",
|
||||
});
|
||||
|
||||
if (target.result.os.tag == .freebsd) {
|
||||
try flags.appendSlice(&.{
|
||||
"-DFC_TEMPLATEDIR=\"/usr/local/etc/fonts/conf.avail\"",
|
||||
"-DFONTCONFIG_PATH=\"/usr/local/etc/fonts\"",
|
||||
"-DCONFIGDIR=\"/usr/local/etc/fonts/conf.d\"",
|
||||
});
|
||||
} else {
|
||||
try flags.appendSlice(&.{
|
||||
"-DFC_TEMPLATEDIR=\"/usr/share/fontconfig/conf.avail\"",
|
||||
"-DFONTCONFIG_PATH=\"/etc/fonts\"",
|
||||
"-DCONFIGDIR=\"/usr/local/fontconfig/conf.d\"",
|
||||
});
|
||||
}
|
||||
|
||||
if (target.result.os.tag == .linux) {
|
||||
try flags.appendSlice(&.{
|
||||
"-DHAVE_SYS_STATFS_H",
|
||||
|
@ -2,6 +2,8 @@ pub const c = @import("animation/c.zig").c;
|
||||
|
||||
/// https://developer.apple.com/documentation/quartzcore/calayer/contents_gravity_values?language=objc
|
||||
pub extern "c" const kCAGravityTopLeft: *anyopaque;
|
||||
pub extern "c" const kCAGravityBottomLeft: *anyopaque;
|
||||
pub extern "c" const kCAGravityCenter: *anyopaque;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
@ -33,6 +33,7 @@ pub fn build(b: *std.Build) !void {
|
||||
lib.linkFramework("CoreText");
|
||||
lib.linkFramework("CoreVideo");
|
||||
lib.linkFramework("QuartzCore");
|
||||
lib.linkFramework("IOSurface");
|
||||
if (target.result.os.tag == .macos) {
|
||||
lib.linkFramework("Carbon");
|
||||
module.linkFramework("Carbon", .{});
|
||||
@ -44,6 +45,7 @@ pub fn build(b: *std.Build) !void {
|
||||
module.linkFramework("CoreText", .{});
|
||||
module.linkFramework("CoreVideo", .{});
|
||||
module.linkFramework("QuartzCore", .{});
|
||||
module.linkFramework("IOSurface", .{});
|
||||
|
||||
try apple_sdk.addPaths(b, lib);
|
||||
}
|
||||
|
@ -3,6 +3,16 @@ pub const data = @import("dispatch/data.zig");
|
||||
pub const queue = @import("dispatch/queue.zig");
|
||||
pub const Data = data.Data;
|
||||
|
||||
pub extern "c" fn dispatch_sync(
|
||||
queue: *anyopaque,
|
||||
block: *anyopaque,
|
||||
) void;
|
||||
|
||||
pub extern "c" fn dispatch_async(
|
||||
queue: *anyopaque,
|
||||
block: *anyopaque,
|
||||
) void;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ pub const stringGetSurrogatePairForLongCharacter = string.stringGetSurrogatePair
|
||||
pub const URL = url.URL;
|
||||
pub const URLPathStyle = url.URLPathStyle;
|
||||
pub const CFRelease = typepkg.CFRelease;
|
||||
pub const CFRetain = typepkg.CFRetain;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
@ -1 +1,2 @@
|
||||
pub extern "c" fn CFRelease(*anyopaque) void;
|
||||
pub extern "c" fn CFRetain(*anyopaque) void;
|
||||
|
8
pkg/macos/iosurface.zig
Normal file
@ -0,0 +1,8 @@
|
||||
const iosurface = @import("iosurface/iosurface.zig");
|
||||
|
||||
pub const c = @import("iosurface/c.zig").c;
|
||||
pub const IOSurface = iosurface.IOSurface;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
1
pkg/macos/iosurface/c.zig
Normal file
@ -0,0 +1 @@
|
||||
pub const c = @import("../main.zig").c;
|
136
pkg/macos/iosurface/iosurface.zig
Normal file
@ -0,0 +1,136 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const c = @import("c.zig").c;
|
||||
const foundation = @import("../foundation.zig");
|
||||
const graphics = @import("../graphics.zig");
|
||||
const video = @import("../video.zig");
|
||||
|
||||
pub const IOSurface = opaque {
|
||||
pub const Error = error{
|
||||
InvalidOperation,
|
||||
};
|
||||
|
||||
pub const Properties = struct {
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: video.PixelFormat,
|
||||
bytes_per_element: c_int,
|
||||
colorspace: ?*graphics.ColorSpace,
|
||||
};
|
||||
|
||||
pub fn init(properties: Properties) Allocator.Error!*IOSurface {
|
||||
var w = try foundation.Number.create(.int, &properties.width);
|
||||
defer w.release();
|
||||
var h = try foundation.Number.create(.int, &properties.height);
|
||||
defer h.release();
|
||||
var pf = try foundation.Number.create(.int, &@as(c_int, @intFromEnum(properties.pixel_format)));
|
||||
defer pf.release();
|
||||
var bpe = try foundation.Number.create(.int, &properties.bytes_per_element);
|
||||
defer bpe.release();
|
||||
|
||||
var properties_dict = try foundation.Dictionary.create(
|
||||
&[_]?*const anyopaque{
|
||||
c.kIOSurfaceWidth,
|
||||
c.kIOSurfaceHeight,
|
||||
c.kIOSurfacePixelFormat,
|
||||
c.kIOSurfaceBytesPerElement,
|
||||
},
|
||||
&[_]?*const anyopaque{ w, h, pf, bpe },
|
||||
);
|
||||
defer properties_dict.release();
|
||||
|
||||
var surface = @as(?*IOSurface, @ptrFromInt(@intFromPtr(
|
||||
c.IOSurfaceCreate(@ptrCast(properties_dict)),
|
||||
))) orelse return error.OutOfMemory;
|
||||
|
||||
if (properties.colorspace) |space| {
|
||||
surface.setColorSpace(space);
|
||||
}
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *IOSurface) void {
|
||||
// We mark it purgeable so that it is immediately unloaded, so that we
|
||||
// don't have to wait for CoreFoundation garbage collection to trigger.
|
||||
_ = c.IOSurfaceSetPurgeable(
|
||||
@ptrCast(self),
|
||||
c.kIOSurfacePurgeableEmpty,
|
||||
null,
|
||||
);
|
||||
foundation.CFRelease(self);
|
||||
}
|
||||
|
||||
pub fn retain(self: *IOSurface) void {
|
||||
foundation.CFRetain(self);
|
||||
}
|
||||
|
||||
pub fn release(self: *IOSurface) void {
|
||||
foundation.CFRelease(self);
|
||||
}
|
||||
|
||||
pub fn setColorSpace(self: *IOSurface, colorspace: *graphics.ColorSpace) void {
|
||||
const serialized_colorspace = graphics.c.CGColorSpaceCopyPropertyList(
|
||||
@ptrCast(colorspace),
|
||||
).?;
|
||||
defer foundation.CFRelease(@constCast(serialized_colorspace));
|
||||
|
||||
c.IOSurfaceSetValue(
|
||||
@ptrCast(self),
|
||||
c.kIOSurfaceColorSpace,
|
||||
@ptrCast(serialized_colorspace),
|
||||
);
|
||||
}
|
||||
|
||||
pub inline fn lock(self: *IOSurface) void {
|
||||
c.IOSurfaceLock(
|
||||
@ptrCast(self),
|
||||
0,
|
||||
null,
|
||||
);
|
||||
}
|
||||
pub inline fn unlock(self: *IOSurface) void {
|
||||
c.IOSurfaceUnlock(
|
||||
@ptrCast(self),
|
||||
0,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
pub inline fn getAllocSize(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetAllocSize(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getWidth(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetWidth(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getHeight(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetHeight(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getBytesPerElement(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetBytesPerElement(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getBytesPerRow(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetBytesPerRow(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getBaseAddress(self: *IOSurface) ?[*]u8 {
|
||||
return @ptrCast(c.IOSurfaceGetBaseAddress(@ptrCast(self)));
|
||||
}
|
||||
|
||||
pub inline fn getElementWidth(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetElementWidth(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getElementHeight(self: *IOSurface) usize {
|
||||
return c.IOSurfaceGetElementHeight(@ptrCast(self));
|
||||
}
|
||||
|
||||
pub inline fn getPixelFormat(self: *IOSurface) video.PixelFormat {
|
||||
return @enumFromInt(c.IOSurfaceGetPixelFormat(@ptrCast(self)));
|
||||
}
|
||||
};
|
@ -8,6 +8,7 @@ pub const graphics = @import("graphics.zig");
|
||||
pub const os = @import("os.zig");
|
||||
pub const text = @import("text.zig");
|
||||
pub const video = @import("video.zig");
|
||||
pub const iosurface = @import("iosurface.zig");
|
||||
|
||||
// All of our C imports consolidated into one place. We used to
|
||||
// import them one by one in each package but Zig 0.14 has some
|
||||
@ -17,7 +18,9 @@ pub const c = @cImport({
|
||||
@cInclude("CoreGraphics/CoreGraphics.h");
|
||||
@cInclude("CoreText/CoreText.h");
|
||||
@cInclude("CoreVideo/CoreVideo.h");
|
||||
@cInclude("CoreVideo/CVPixelBuffer.h");
|
||||
@cInclude("QuartzCore/CALayer.h");
|
||||
@cInclude("IOSurface/IOSurfaceRef.h");
|
||||
@cInclude("dispatch/dispatch.h");
|
||||
@cInclude("os/log.h");
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
const display_link = @import("video/display_link.zig");
|
||||
const pixel_format = @import("video/pixel_format.zig");
|
||||
|
||||
pub const c = @import("video/c.zig").c;
|
||||
pub const DisplayLink = display_link.DisplayLink;
|
||||
pub const PixelFormat = pixel_format.PixelFormat;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
171
pkg/macos/video/pixel_format.zig
Normal file
@ -0,0 +1,171 @@
|
||||
const c = @import("c.zig").c;
|
||||
|
||||
pub const PixelFormat = enum(c_int) {
|
||||
/// 1 bit indexed
|
||||
@"1Monochrome" = c.kCVPixelFormatType_1Monochrome,
|
||||
/// 2 bit indexed
|
||||
@"2Indexed" = c.kCVPixelFormatType_2Indexed,
|
||||
/// 4 bit indexed
|
||||
@"4Indexed" = c.kCVPixelFormatType_4Indexed,
|
||||
/// 8 bit indexed
|
||||
@"8Indexed" = c.kCVPixelFormatType_8Indexed,
|
||||
/// 1 bit indexed gray, white is zero
|
||||
@"1IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_1IndexedGray_WhiteIsZero,
|
||||
/// 2 bit indexed gray, white is zero
|
||||
@"2IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_2IndexedGray_WhiteIsZero,
|
||||
/// 4 bit indexed gray, white is zero
|
||||
@"4IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_4IndexedGray_WhiteIsZero,
|
||||
/// 8 bit indexed gray, white is zero
|
||||
@"8IndexedGray_WhiteIsZero" = c.kCVPixelFormatType_8IndexedGray_WhiteIsZero,
|
||||
/// 16 bit BE RGB 555
|
||||
@"16BE555" = c.kCVPixelFormatType_16BE555,
|
||||
/// 16 bit LE RGB 555
|
||||
@"16LE555" = c.kCVPixelFormatType_16LE555,
|
||||
/// 16 bit LE RGB 5551
|
||||
@"16LE5551" = c.kCVPixelFormatType_16LE5551,
|
||||
/// 16 bit BE RGB 565
|
||||
@"16BE565" = c.kCVPixelFormatType_16BE565,
|
||||
/// 16 bit LE RGB 565
|
||||
@"16LE565" = c.kCVPixelFormatType_16LE565,
|
||||
/// 24 bit RGB
|
||||
@"24RGB" = c.kCVPixelFormatType_24RGB,
|
||||
/// 24 bit BGR
|
||||
@"24BGR" = c.kCVPixelFormatType_24BGR,
|
||||
/// 32 bit ARGB
|
||||
@"32ARGB" = c.kCVPixelFormatType_32ARGB,
|
||||
/// 32 bit BGRA
|
||||
@"32BGRA" = c.kCVPixelFormatType_32BGRA,
|
||||
/// 32 bit ABGR
|
||||
@"32ABGR" = c.kCVPixelFormatType_32ABGR,
|
||||
/// 32 bit RGBA
|
||||
@"32RGBA" = c.kCVPixelFormatType_32RGBA,
|
||||
/// 64 bit ARGB, 16-bit big-endian samples
|
||||
@"64ARGB" = c.kCVPixelFormatType_64ARGB,
|
||||
/// 64 bit RGBA, 16-bit little-endian full-range (0-65535) samples
|
||||
@"64RGBALE" = c.kCVPixelFormatType_64RGBALE,
|
||||
/// 48 bit RGB, 16-bit big-endian samples
|
||||
@"48RGB" = c.kCVPixelFormatType_48RGB,
|
||||
/// 32 bit AlphaGray, 16-bit big-endian samples, black is zero
|
||||
@"32AlphaGray" = c.kCVPixelFormatType_32AlphaGray,
|
||||
/// 16 bit Grayscale, 16-bit big-endian samples, black is zero
|
||||
@"16Gray" = c.kCVPixelFormatType_16Gray,
|
||||
/// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at least significant end).
|
||||
@"30RGB" = c.kCVPixelFormatType_30RGB,
|
||||
/// 30 bit RGB, 10-bit big-endian samples, 2 unused padding bits (at most significant end), video-range (64-940).
|
||||
@"30RGB_r210" = c.kCVPixelFormatType_30RGB_r210,
|
||||
/// Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1
|
||||
@"422YpCbCr8" = c.kCVPixelFormatType_422YpCbCr8,
|
||||
/// Component Y'CbCrA 8-bit 4:4:4:4, ordered Cb Y' Cr A
|
||||
@"4444YpCbCrA8" = c.kCVPixelFormatType_4444YpCbCrA8,
|
||||
/// Component Y'CbCrA 8-bit 4:4:4:4, rendering format. full range alpha, zero biased YUV, ordered A Y' Cb Cr
|
||||
@"4444YpCbCrA8R" = c.kCVPixelFormatType_4444YpCbCrA8R,
|
||||
/// Component Y'CbCrA 8-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr.
|
||||
@"4444AYpCbCr8" = c.kCVPixelFormatType_4444AYpCbCr8,
|
||||
/// Component Y'CbCrA 16-bit 4:4:4:4, ordered A Y' Cb Cr, full range alpha, video range Y'CbCr, 16-bit little-endian samples.
|
||||
@"4444AYpCbCr16" = c.kCVPixelFormatType_4444AYpCbCr16,
|
||||
/// Component AY'CbCr single precision floating-point 4:4:4:4
|
||||
@"4444AYpCbCrFloat" = c.kCVPixelFormatType_4444AYpCbCrFloat,
|
||||
/// Component Y'CbCr 8-bit 4:4:4, ordered Cr Y' Cb, video range Y'CbCr
|
||||
@"444YpCbCr8" = c.kCVPixelFormatType_444YpCbCr8,
|
||||
/// Component Y'CbCr 10,12,14,16-bit 4:2:2
|
||||
@"422YpCbCr16" = c.kCVPixelFormatType_422YpCbCr16,
|
||||
/// Component Y'CbCr 10-bit 4:2:2
|
||||
@"422YpCbCr10" = c.kCVPixelFormatType_422YpCbCr10,
|
||||
/// Component Y'CbCr 10-bit 4:4:4
|
||||
@"444YpCbCr10" = c.kCVPixelFormatType_444YpCbCr10,
|
||||
/// Planar Component Y'CbCr 8-bit 4:2:0. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
|
||||
@"420YpCbCr8Planar" = c.kCVPixelFormatType_420YpCbCr8Planar,
|
||||
/// Planar Component Y'CbCr 8-bit 4:2:0, full range. baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrPlanar struct
|
||||
@"420YpCbCr8PlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8PlanarFullRange,
|
||||
/// First plane: Video-range Component Y'CbCr 8-bit 4:2:2, ordered Cb Y'0 Cr Y'1; second plane: alpha 8-bit 0-255
|
||||
@"422YpCbCr_4A_8BiPlanar" = c.kCVPixelFormatType_422YpCbCr_4A_8BiPlanar,
|
||||
/// Bi-Planar Component Y'CbCr 8-bit 4:2:0, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
|
||||
@"420YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,
|
||||
/// Bi-Planar Component Y'CbCr 8-bit 4:2:0, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
|
||||
@"420YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
/// Bi-Planar Component Y'CbCr 8-bit 4:2:2, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
|
||||
@"422YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange,
|
||||
/// Bi-Planar Component Y'CbCr 8-bit 4:2:2, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
|
||||
@"422YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr8BiPlanarFullRange,
|
||||
/// Bi-Planar Component Y'CbCr 8-bit 4:4:4, video-range (luma=[16,235] chroma=[16,240]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
|
||||
@"444YpCbCr8BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange,
|
||||
/// Bi-Planar Component Y'CbCr 8-bit 4:4:4, full-range (luma=[0,255] chroma=[1,255]). baseAddr points to a big-endian CVPlanarPixelBufferInfo_YCbCrBiPlanar struct
|
||||
@"444YpCbCr8BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr8BiPlanarFullRange,
|
||||
/// Component Y'CbCr 8-bit 4:2:2, ordered Y'0 Cb Y'1 Cr
|
||||
@"422YpCbCr8_yuvs" = c.kCVPixelFormatType_422YpCbCr8_yuvs,
|
||||
/// Component Y'CbCr 8-bit 4:2:2, full range, ordered Y'0 Cb Y'1 Cr
|
||||
@"422YpCbCr8FullRange" = c.kCVPixelFormatType_422YpCbCr8FullRange,
|
||||
/// 8 bit one component, black is zero
|
||||
OneComponent8 = c.kCVPixelFormatType_OneComponent8,
|
||||
/// 8 bit two component, black is zero
|
||||
TwoComponent8 = c.kCVPixelFormatType_TwoComponent8,
|
||||
/// little-endian RGB101010, 2 MSB are ignored, wide-gamut (384-895)
|
||||
@"30RGBLEPackedWideGamut" = c.kCVPixelFormatType_30RGBLEPackedWideGamut,
|
||||
/// little-endian ARGB2101010 full-range ARGB
|
||||
ARGB2101010LEPacked = c.kCVPixelFormatType_ARGB2101010LEPacked,
|
||||
/// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha)
|
||||
@"40ARGBLEWideGamut" = c.kCVPixelFormatType_40ARGBLEWideGamut,
|
||||
/// little-endian ARGB10101010, each 10 bits in the MSBs of 16bits, wide-gamut (384-895, including alpha). Alpha premultiplied
|
||||
@"40ARGBLEWideGamutPremultiplied" = c.kCVPixelFormatType_40ARGBLEWideGamutPremultiplied,
|
||||
/// 10 bit little-endian one component, stored as 10 MSBs of 16 bits, black is zero
|
||||
OneComponent10 = c.kCVPixelFormatType_OneComponent10,
|
||||
/// 12 bit little-endian one component, stored as 12 MSBs of 16 bits, black is zero
|
||||
OneComponent12 = c.kCVPixelFormatType_OneComponent12,
|
||||
/// 16 bit little-endian one component, black is zero
|
||||
OneComponent16 = c.kCVPixelFormatType_OneComponent16,
|
||||
/// 16 bit little-endian two component, black is zero
|
||||
TwoComponent16 = c.kCVPixelFormatType_TwoComponent16,
|
||||
/// 16 bit one component IEEE half-precision float, 16-bit little-endian samples
|
||||
OneComponent16Half = c.kCVPixelFormatType_OneComponent16Half,
|
||||
/// 32 bit one component IEEE float, 32-bit little-endian samples
|
||||
OneComponent32Float = c.kCVPixelFormatType_OneComponent32Float,
|
||||
/// 16 bit two component IEEE half-precision float, 16-bit little-endian samples
|
||||
TwoComponent16Half = c.kCVPixelFormatType_TwoComponent16Half,
|
||||
/// 32 bit two component IEEE float, 32-bit little-endian samples
|
||||
TwoComponent32Float = c.kCVPixelFormatType_TwoComponent32Float,
|
||||
/// 64 bit RGBA IEEE half-precision float, 16-bit little-endian samples
|
||||
@"64RGBAHalf" = c.kCVPixelFormatType_64RGBAHalf,
|
||||
/// 128 bit RGBA IEEE float, 32-bit little-endian samples
|
||||
@"128RGBAFloat" = c.kCVPixelFormatType_128RGBAFloat,
|
||||
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G R G R... alternating with B G B G...
|
||||
@"14Bayer_GRBG" = c.kCVPixelFormatType_14Bayer_GRBG,
|
||||
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered R G R G... alternating with G B G B...
|
||||
@"14Bayer_RGGB" = c.kCVPixelFormatType_14Bayer_RGGB,
|
||||
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered B G B G... alternating with G R G R...
|
||||
@"14Bayer_BGGR" = c.kCVPixelFormatType_14Bayer_BGGR,
|
||||
/// Bayer 14-bit Little-Endian, packed in 16-bits, ordered G B G B... alternating with R G R G...
|
||||
@"14Bayer_GBRG" = c.kCVPixelFormatType_14Bayer_GBRG,
|
||||
/// IEEE754-2008 binary16 (half float), describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
|
||||
DisparityFloat16 = c.kCVPixelFormatType_DisparityFloat16,
|
||||
/// IEEE754-2008 binary32 float, describing the normalized shift when comparing two images. Units are 1/meters: ( pixelShift / (pixelFocalLength * baselineInMeters) )
|
||||
DisparityFloat32 = c.kCVPixelFormatType_DisparityFloat32,
|
||||
/// IEEE754-2008 binary16 (half float), describing the depth (distance to an object) in meters
|
||||
DepthFloat16 = c.kCVPixelFormatType_DepthFloat16,
|
||||
/// IEEE754-2008 binary32 float, describing the depth (distance to an object) in meters
|
||||
DepthFloat32 = c.kCVPixelFormatType_DepthFloat32,
|
||||
/// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
|
||||
@"420YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange,
|
||||
/// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
|
||||
@"422YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange,
|
||||
/// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, video-range (luma=[64,940] chroma=[64,960])
|
||||
@"444YpCbCr10BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange,
|
||||
/// 2 plane YCbCr10 4:2:0, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
|
||||
@"420YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_420YpCbCr10BiPlanarFullRange,
|
||||
/// 2 plane YCbCr10 4:2:2, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
|
||||
@"422YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_422YpCbCr10BiPlanarFullRange,
|
||||
/// 2 plane YCbCr10 4:4:4, each 10 bits in the MSBs of 16bits, full-range (Y range 0-1023)
|
||||
@"444YpCbCr10BiPlanarFullRange" = c.kCVPixelFormatType_444YpCbCr10BiPlanarFullRange,
|
||||
/// first and second planes as per 420YpCbCr8BiPlanarVideoRange (420v), alpha 8 bits in third plane full-range. No CVPlanarPixelBufferInfo struct.
|
||||
@"420YpCbCr8VideoRange_8A_TriPlanar" = c.kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar,
|
||||
/// Single plane Bayer 16-bit little-endian sensor element ("sensel".*) samples from full-size decoding of ProRes RAW images; Bayer pattern (sensel ordering) and other raw conversion information is described via buffer attachments
|
||||
@"16VersatileBayer" = c.kCVPixelFormatType_16VersatileBayer,
|
||||
/// Single plane 64-bit RGBA (16-bit little-endian samples) from downscaled decoding of ProRes RAW images; components--which may not be co-sited with one another--are sensel values and require raw conversion, information for which is described via buffer attachments
|
||||
@"64RGBA_DownscaledProResRAW" = c.kCVPixelFormatType_64RGBA_DownscaledProResRAW,
|
||||
/// 2 plane YCbCr16 4:2:2, video-range (luma=[4096,60160] chroma=[4096,61440])
|
||||
@"422YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_422YpCbCr16BiPlanarVideoRange,
|
||||
/// 2 plane YCbCr16 4:4:4, video-range (luma=[4096,60160] chroma=[4096,61440])
|
||||
@"444YpCbCr16BiPlanarVideoRange" = c.kCVPixelFormatType_444YpCbCr16BiPlanarVideoRange,
|
||||
/// 3 plane video-range YCbCr16 4:4:4 with 16-bit full-range alpha (luma=[4096,60160] chroma=[4096,61440] alpha=[0,65535]). No CVPlanarPixelBufferInfo struct.
|
||||
@"444YpCbCr16VideoRange_16A_TriPlanar" = c.kCVPixelFormatType_444YpCbCr16VideoRange_16A_TriPlanar,
|
||||
_,
|
||||
};
|
@ -51,7 +51,7 @@ pub const Binding = struct {
|
||||
data: anytype,
|
||||
usage: Usage,
|
||||
) !void {
|
||||
const info = dataInfo(&data);
|
||||
const info = dataInfo(data);
|
||||
glad.context.BufferData.?(
|
||||
@intFromEnum(b.target),
|
||||
info.size,
|
||||
@ -136,10 +136,6 @@ pub const Binding = struct {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
|
||||
glad.context.EnableVertexAttribArray.?(idx);
|
||||
}
|
||||
|
||||
/// Shorthand for vertexAttribPointer that is specialized towards the
|
||||
/// common use case of specifying an array of homogeneous types that
|
||||
/// don't need normalization. This also enables the attribute at idx.
|
||||
@ -230,6 +226,7 @@ pub const Target = enum(c_uint) {
|
||||
array = c.GL_ARRAY_BUFFER,
|
||||
element_array = c.GL_ELEMENT_ARRAY_BUFFER,
|
||||
uniform = c.GL_UNIFORM_BUFFER,
|
||||
storage = c.GL_SHADER_STORAGE_BUFFER,
|
||||
_,
|
||||
};
|
||||
|
||||
|
@ -5,6 +5,7 @@ const c = @import("c.zig").c;
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
const Texture = @import("Texture.zig");
|
||||
const Renderbuffer = @import("Renderbuffer.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
@ -86,6 +87,29 @@ pub const Binding = struct {
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn renderbuffer(
|
||||
self: Binding,
|
||||
attachment: Attachment,
|
||||
buffer: Renderbuffer,
|
||||
) !void {
|
||||
glad.context.FramebufferRenderbuffer.?(
|
||||
@intFromEnum(self.target),
|
||||
@intFromEnum(attachment),
|
||||
c.GL_RENDERBUFFER,
|
||||
buffer.id,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn drawBuffers(
|
||||
self: Binding,
|
||||
bufs: []Attachment,
|
||||
) !void {
|
||||
_ = self;
|
||||
glad.context.DrawBuffers.?(@intCast(bufs.len), bufs.ptr);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn checkStatus(self: Binding) Status {
|
||||
return @enumFromInt(glad.context.CheckFramebufferStatus.?(@intFromEnum(self.target)));
|
||||
}
|
||||
|
56
pkg/opengl/Renderbuffer.zig
Normal file
@ -0,0 +1,56 @@
|
||||
const Renderbuffer = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const c = @import("c.zig").c;
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
|
||||
const Texture = @import("Texture.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
/// Create a single buffer.
|
||||
pub fn create() !Renderbuffer {
|
||||
var rbo: c.GLuint = undefined;
|
||||
glad.context.GenRenderbuffers.?(1, &rbo);
|
||||
return .{ .id = rbo };
|
||||
}
|
||||
|
||||
pub fn destroy(v: Renderbuffer) void {
|
||||
glad.context.DeleteRenderbuffers.?(1, &v.id);
|
||||
}
|
||||
|
||||
pub fn bind(v: Renderbuffer) !Binding {
|
||||
// Keep track of the previous binding so we can restore it in unbind.
|
||||
var current: c.GLint = undefined;
|
||||
glad.context.GetIntegerv.?(c.GL_RENDERBUFFER_BINDING, ¤t);
|
||||
glad.context.BindRenderbuffer.?(c.GL_RENDERBUFFER, v.id);
|
||||
return .{ .previous = @intCast(current) };
|
||||
}
|
||||
|
||||
pub const Binding = struct {
|
||||
previous: c.GLuint,
|
||||
|
||||
pub fn unbind(self: Binding) void {
|
||||
glad.context.BindRenderbuffer.?(
|
||||
c.GL_RENDERBUFFER,
|
||||
self.previous,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn storage(
|
||||
self: Binding,
|
||||
format: Texture.InternalFormat,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
) !void {
|
||||
_ = self;
|
||||
glad.context.RenderbufferStorage.?(
|
||||
c.GL_RENDERBUFFER,
|
||||
@intCast(@intFromEnum(format)),
|
||||
width,
|
||||
height,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
};
|
@ -7,15 +7,16 @@ const glad = @import("glad.zig");
|
||||
|
||||
id: c.GLuint,
|
||||
|
||||
pub fn active(target: c.GLenum) !void {
|
||||
glad.context.ActiveTexture.?(target);
|
||||
pub fn active(index: c_uint) errors.Error!void {
|
||||
glad.context.ActiveTexture.?(index + c.GL_TEXTURE0);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
/// Create a single texture.
|
||||
pub fn create() !Texture {
|
||||
pub fn create() errors.Error!Texture {
|
||||
var id: c.GLuint = undefined;
|
||||
glad.context.GenTextures.?(1, &id);
|
||||
try errors.getError();
|
||||
return .{ .id = id };
|
||||
}
|
||||
|
||||
@ -30,7 +31,7 @@ pub fn destroy(v: Texture) void {
|
||||
glad.context.DeleteTextures.?(1, &v.id);
|
||||
}
|
||||
|
||||
/// Enun for possible texture binding targets.
|
||||
/// Enum for possible texture binding targets.
|
||||
pub const Target = enum(c_uint) {
|
||||
@"1D" = c.GL_TEXTURE_1D,
|
||||
@"2D" = c.GL_TEXTURE_2D,
|
||||
@ -67,11 +68,11 @@ pub const Parameter = enum(c_uint) {
|
||||
/// Internal format enum for texture images.
|
||||
pub const InternalFormat = enum(c_int) {
|
||||
red = c.GL_RED,
|
||||
rgb = c.GL_RGB,
|
||||
rgba = c.GL_RGBA,
|
||||
rgb = c.GL_RGB8,
|
||||
rgba = c.GL_RGBA8,
|
||||
|
||||
srgb = c.GL_SRGB,
|
||||
srgba = c.GL_SRGB_ALPHA,
|
||||
srgb = c.GL_SRGB8,
|
||||
srgba = c.GL_SRGB8_ALPHA8,
|
||||
|
||||
// There are so many more that I haven't filled in.
|
||||
_,
|
||||
@ -107,7 +108,7 @@ pub const Binding = struct {
|
||||
glad.context.GenerateMipmap.?(@intFromEnum(b.target));
|
||||
}
|
||||
|
||||
pub fn parameter(b: Binding, name: Parameter, value: anytype) !void {
|
||||
pub fn parameter(b: Binding, name: Parameter, value: anytype) errors.Error!void {
|
||||
switch (@TypeOf(value)) {
|
||||
c.GLint => glad.context.TexParameteri.?(
|
||||
@intFromEnum(b.target),
|
||||
@ -116,6 +117,7 @@ pub const Binding = struct {
|
||||
),
|
||||
else => unreachable,
|
||||
}
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn image2D(
|
||||
@ -128,7 +130,7 @@ pub const Binding = struct {
|
||||
format: Format,
|
||||
typ: DataType,
|
||||
data: ?*const anyopaque,
|
||||
) !void {
|
||||
) errors.Error!void {
|
||||
glad.context.TexImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
@ -140,6 +142,7 @@ pub const Binding = struct {
|
||||
@intFromEnum(typ),
|
||||
data,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn subImage2D(
|
||||
@ -152,7 +155,7 @@ pub const Binding = struct {
|
||||
format: Format,
|
||||
typ: DataType,
|
||||
data: ?*const anyopaque,
|
||||
) !void {
|
||||
) errors.Error!void {
|
||||
glad.context.TexSubImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
@ -164,6 +167,7 @@ pub const Binding = struct {
|
||||
@intFromEnum(typ),
|
||||
data,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn copySubImage2D(
|
||||
@ -175,7 +179,17 @@ pub const Binding = struct {
|
||||
y: c.GLint,
|
||||
width: c.GLsizei,
|
||||
height: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.CopyTexSubImage2D.?(@intFromEnum(b.target), level, xoffset, yoffset, x, y, width, height);
|
||||
) errors.Error!void {
|
||||
glad.context.CopyTexSubImage2D.?(
|
||||
@intFromEnum(b.target),
|
||||
level,
|
||||
xoffset,
|
||||
yoffset,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
};
|
||||
|
@ -29,4 +29,88 @@ pub const Binding = struct {
|
||||
_ = self;
|
||||
glad.context.BindVertexArray.?(0);
|
||||
}
|
||||
|
||||
pub fn enableAttribArray(_: Binding, idx: c.GLuint) !void {
|
||||
glad.context.EnableVertexAttribArray.?(idx);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn bindingDivisor(_: Binding, idx: c.GLuint, divisor: c.GLuint) !void {
|
||||
glad.context.VertexBindingDivisor.?(idx, divisor);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn attributeBinding(
|
||||
_: Binding,
|
||||
attrib_idx: c.GLuint,
|
||||
binding_idx: c.GLuint,
|
||||
) !void {
|
||||
glad.context.VertexAttribBinding.?(attrib_idx, binding_idx);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn attributeFormat(
|
||||
_: Binding,
|
||||
idx: c.GLuint,
|
||||
size: c.GLint,
|
||||
typ: c.GLenum,
|
||||
normalized: bool,
|
||||
offset: c.GLuint,
|
||||
) !void {
|
||||
glad.context.VertexAttribFormat.?(
|
||||
idx,
|
||||
size,
|
||||
typ,
|
||||
@intCast(@intFromBool(normalized)),
|
||||
offset,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn attributeIFormat(
|
||||
_: Binding,
|
||||
idx: c.GLuint,
|
||||
size: c.GLint,
|
||||
typ: c.GLenum,
|
||||
offset: c.GLuint,
|
||||
) !void {
|
||||
glad.context.VertexAttribIFormat.?(
|
||||
idx,
|
||||
size,
|
||||
typ,
|
||||
offset,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn attributeLFormat(
|
||||
_: Binding,
|
||||
idx: c.GLuint,
|
||||
size: c.GLint,
|
||||
offset: c.GLuint,
|
||||
) !void {
|
||||
glad.context.VertexAttribLFormat.?(
|
||||
idx,
|
||||
size,
|
||||
c.GL_DOUBLE,
|
||||
offset,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn bindVertexBuffer(
|
||||
_: Binding,
|
||||
idx: c.GLuint,
|
||||
buffer: c.GLuint,
|
||||
offset: c.GLintptr,
|
||||
stride: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.BindVertexBuffer.?(
|
||||
idx,
|
||||
buffer,
|
||||
offset,
|
||||
stride,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
const c = @import("c.zig").c;
|
||||
const errors = @import("errors.zig");
|
||||
const glad = @import("glad.zig");
|
||||
const Primitive = @import("primitives.zig").Primitive;
|
||||
|
||||
pub fn clearColor(r: f32, g: f32, b: f32, a: f32) void {
|
||||
glad.context.ClearColor.?(r, g, b, a);
|
||||
@ -15,6 +16,21 @@ pub fn drawArrays(mode: c.GLenum, first: c.GLint, count: c.GLsizei) !void {
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn drawArraysInstanced(
|
||||
mode: Primitive,
|
||||
first: c.GLint,
|
||||
count: c.GLsizei,
|
||||
primcount: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.DrawArraysInstanced.?(
|
||||
@intCast(@intFromEnum(mode)),
|
||||
first,
|
||||
count,
|
||||
primcount,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn drawElements(mode: c.GLenum, count: c.GLsizei, typ: c.GLenum, offset: usize) !void {
|
||||
const offsetPtr = if (offset == 0) null else @as(*const anyopaque, @ptrFromInt(offset));
|
||||
glad.context.DrawElements.?(mode, count, typ, offsetPtr);
|
||||
@ -25,9 +41,15 @@ pub fn drawElementsInstanced(
|
||||
mode: c.GLenum,
|
||||
count: c.GLsizei,
|
||||
typ: c.GLenum,
|
||||
primcount: usize,
|
||||
primcount: c.GLsizei,
|
||||
) !void {
|
||||
glad.context.DrawElementsInstanced.?(mode, count, typ, null, @intCast(primcount));
|
||||
glad.context.DrawElementsInstanced.?(
|
||||
mode,
|
||||
count,
|
||||
typ,
|
||||
null,
|
||||
primcount,
|
||||
);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
@ -36,6 +58,11 @@ pub fn enable(cap: c.GLenum) !void {
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn disable(cap: c.GLenum) !void {
|
||||
glad.context.Disable.?(cap);
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn frontFace(mode: c.GLenum) !void {
|
||||
glad.context.FrontFace.?(mode);
|
||||
try errors.getError();
|
||||
@ -57,3 +84,11 @@ pub fn pixelStore(mode: c.GLenum, value: anytype) !void {
|
||||
}
|
||||
try errors.getError();
|
||||
}
|
||||
|
||||
pub fn finish() void {
|
||||
glad.context.Finish.?();
|
||||
}
|
||||
|
||||
pub fn flush() void {
|
||||
glad.context.Flush.?();
|
||||
}
|
||||
|
@ -16,20 +16,29 @@ pub const glad = @import("glad.zig");
|
||||
pub const ext = @import("extensions.zig");
|
||||
pub const Buffer = @import("Buffer.zig");
|
||||
pub const Framebuffer = @import("Framebuffer.zig");
|
||||
pub const Renderbuffer = @import("Renderbuffer.zig");
|
||||
pub const Program = @import("Program.zig");
|
||||
pub const Shader = @import("Shader.zig");
|
||||
pub const Texture = @import("Texture.zig");
|
||||
pub const VertexArray = @import("VertexArray.zig");
|
||||
|
||||
pub const errors = @import("errors.zig");
|
||||
|
||||
pub const Primitive = @import("primitives.zig").Primitive;
|
||||
|
||||
const draw = @import("draw.zig");
|
||||
|
||||
pub const blendFunc = draw.blendFunc;
|
||||
pub const clear = draw.clear;
|
||||
pub const clearColor = draw.clearColor;
|
||||
pub const drawArrays = draw.drawArrays;
|
||||
pub const drawArraysInstanced = draw.drawArraysInstanced;
|
||||
pub const drawElements = draw.drawElements;
|
||||
pub const drawElementsInstanced = draw.drawElementsInstanced;
|
||||
pub const enable = draw.enable;
|
||||
pub const disable = draw.disable;
|
||||
pub const frontFace = draw.frontFace;
|
||||
pub const pixelStore = draw.pixelStore;
|
||||
pub const viewport = draw.viewport;
|
||||
pub const flush = draw.flush;
|
||||
pub const finish = draw.finish;
|
||||
|
18
pkg/opengl/primitives.zig
Normal file
@ -0,0 +1,18 @@
|
||||
pub const c = @import("c.zig").c;
|
||||
|
||||
pub const Primitive = enum(c_int) {
|
||||
point = c.GL_POINTS,
|
||||
line = c.GL_LINES,
|
||||
line_strip = c.GL_LINE_STRIP,
|
||||
triangle = c.GL_TRIANGLES,
|
||||
triangle_strip = c.GL_TRIANGLE_STRIP,
|
||||
|
||||
// Commented out primitive types are excluded for parity with Metal.
|
||||
//
|
||||
// line_loop = c.GL_LINE_LOOP,
|
||||
// line_adjacency = c.GL_LINES_ADJACENCY,
|
||||
// line_strip_adjacency = c.GL_LINE_STRIP_ADJACENCY,
|
||||
// triangle_fan = c.GL_TRIANGLE_FAN,
|
||||
// triangle_adjacency = c.GL_TRIANGLES_ADJACENCY,
|
||||
// triangle_strip_adjacency = c.GL_TRIANGLE_STRIP_ADJACENCY,
|
||||
};
|
@ -9,8 +9,8 @@ msgstr ""
|
||||
"Project-Id-Version: com.mitchellh.ghostty\n"
|
||||
"Report-Msgid-Bugs-To: m@mitchellh.com\n"
|
||||
"POT-Creation-Date: 2025-04-22 08:57-0700\n"
|
||||
"PO-Revision-Date: 2025-03-28 11:04-0300\n"
|
||||
"Last-Translator: Gustavo Peres <gsodevel@gmail.com>\n"
|
||||
"PO-Revision-Date: 2025-06-20 10:19-0300\n"
|
||||
"Last-Translator: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>\n"
|
||||
"Language-Team: Brazilian Portuguese <ldpbr-translation@lists.sourceforge."
|
||||
"net>\n"
|
||||
"Language: pt_BR\n"
|
||||
@ -217,7 +217,7 @@ msgstr "Visualizar abas abertas"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:249
|
||||
msgid "New Split"
|
||||
msgstr ""
|
||||
msgstr "Nova divisão"
|
||||
|
||||
#: src/apprt/gtk/Window.zig:312
|
||||
msgid ""
|
||||
|
@ -72,8 +72,6 @@ parts:
|
||||
build-packages:
|
||||
- libgtk-4-dev
|
||||
- libadwaita-1-dev
|
||||
# TODO: Add when the Snap is updated to Ubuntu 24.10+
|
||||
# - gtk4-layer-shell
|
||||
- libxml2-utils
|
||||
- git
|
||||
- patchelf
|
||||
@ -82,7 +80,10 @@ parts:
|
||||
craftctl set version=$(cat VERSION)
|
||||
$CRAFT_PART_SRC/../../zig/src/zig build -Dpatch-rpath=\$ORIGIN/../usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:/snap/core24/current/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR -Doptimize=ReleaseFast -Dcpu=baseline
|
||||
cp -rp zig-out/* $CRAFT_PART_INSTALL/
|
||||
sed -i 's|Icon=com.mitchellh.ghostty|Icon=/snap/ghostty/current/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
|
||||
# Install libgtk4-layer-shell.so
|
||||
mkdir -p $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR
|
||||
cp .zig-cache/*/*/libgtk4-layer-shell.so $CRAFT_PART_INSTALL/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/
|
||||
sed -i 's|Icon=com.mitchellh.ghostty|Icon=${SNAP}/share/icons/hicolor/512x512/apps/com.mitchellh.ghostty.png|g' $CRAFT_PART_INSTALL/share/applications/com.mitchellh.ghostty.desktop
|
||||
|
||||
libs:
|
||||
plugin: nil
|
||||
|
@ -323,7 +323,7 @@ fn setupFd(src: File.Handle, target: i32) !void {
|
||||
}
|
||||
}
|
||||
},
|
||||
.ios, .macos => {
|
||||
.freebsd, .ios, .macos => {
|
||||
// Mac doesn't support dup3 so we use dup2. We purposely clear
|
||||
// CLO_ON_EXEC for this fd.
|
||||
const flags = try posix.fcntl(src, posix.F.GETFD, 0);
|
||||
|
@ -468,6 +468,7 @@ pub fn init(
|
||||
.size = size,
|
||||
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
|
||||
.rt_surface = rt_surface,
|
||||
.thread = &self.renderer_thread,
|
||||
});
|
||||
errdefer renderer_impl.deinit();
|
||||
|
||||
@ -726,7 +727,9 @@ pub fn close(self: *Surface) void {
|
||||
/// is in the middle of animation (such as a resize, etc.) or when
|
||||
/// the render timer is managed manually by the apprt.
|
||||
pub fn draw(self: *Surface) !void {
|
||||
try self.renderer_thread.draw_now.notify();
|
||||
// Renderers are required to support `drawFrame` being called from
|
||||
// the main thread, so that they can update contents during resize.
|
||||
try self.renderer.drawFrame(true);
|
||||
}
|
||||
|
||||
/// Activate the inspector. This will begin collecting inspection data.
|
||||
|